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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,6 @@ eval/scorecard.json

# ui-lineage scanned graphs
*.graph.json

# ui-lineage SHA-keyed graph store (6.4)
.coderadar/
26 changes: 23 additions & 3 deletions TRACKER.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
## Status

- **Current phase:** 6 — Lifecycle, scale, hardening (6F field-hardening done bar the data-blocked 6F.6 detection half)
- **Next step:** 6.4 version skew, then 6.1 incremental re-scan, 6.2 scale (bench repo). 6.3 determinism + 6.5 generated-classification/PII landed first (6.3 de-risks the rest via a two-run byte-identity gate; 6.5 closes the D5/G7 gate criteria). Publish 0.4.1 to the tester still pending (needs npm creds).
- **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.3, 6.5** · **0.4.1 prepared** (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.
- **Next step:** 6.1 incremental re-scan, then 6.2 scale (bench repo) — the last two before Gate 6 is fully stamped. 6.3/6.4/6.5 landed (determinism gate, version-skew/rename tracking, generated-classification/PII). Publish 0.4.1 to the tester still pending (needs npm creds).
- **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.3, 6.4, 6.5** · **0.4.1 prepared** (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.
- **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 @@ -347,10 +347,30 @@ accept criterion lands with 6.2. Vision/OCR image-hash caching deferred — the
eval path exercises no live OCR, so it isn't a determinism risk today; revisit if
6.4/vision work reintroduces it.

### [ ] 6.4 Version skew & rename tracking
### [x] 6.4 Version skew & rename tracking
**Failure modes:** G3, A11
**Build:** graph store keyed by SHA (`.coderadar/graphs/<sha>.json` + `latest` pointer); `resolveContext` accepts `graphVersion`; cross-version diff maps renamed/moved definitions (same structure+text signature, different name/path) → bundle warning: "matched `InvoiceCard` in prod graph; renamed `BillingCard` on main".
**Accept:** fixture pair (pre/post rename): query against old graph + current code yields the rename warning with the new name.
**Done:** new core `diffRenames(fromGraph, toGraph)` pairs a definition that is
gone by identity (name+file) in the newer graph with a same-**body-signature**
definition that is new there — signature = structural fingerprint + normalized
rendered text + props + rendered-children, all stable across a rename or file
move. Confident 1:1 only: a signature unique among both the gone and the arrived
definitions; generic empty bodies and ambiguous many-to-many sets are skipped, so
no false renames. SHA-keyed store in core storage (`saveGraphToStore` /
`loadGraphFromStore` / `graphStoreDir`): `.coderadar/graphs/<sha>.json` (or
`working` outside git) + a `latest` pointer. `buildBundle` gains a `currentGraph`
option — when the matched definition was renamed/moved there it emits
`version skew — matched \`InvoiceCard\` (…); renamed \`BillingCard\` (…) in the
current graph`. CLI: `scan --store` writes the store; `bundle --against <sha|latest>`
loads the current graph from it (`resolveContext`/version selection surfaced at
the store boundary). Fixture pair `g3-version-skew/{old,new}` (InvoiceCard.tsx →
BillingCard.tsx, renamed **and** moved, identical body); its golden validates the
post-rename graph directly, the two-graph diff is covered by unit tests. Verified
end-to-end via the CLI: an old-graph ticket against `--against latest` warns with
the new name+file. 7 core + 4 store + 2 parser-react (real scan) + 3 agent-sdk
tests. `.coderadar/` gitignored. eval 317/0/0/0, determinism 1.000, all metrics
1.000; typecheck + lint clean.

### [x] 6.5 Generated/vendored classification & PII policy
**Failure modes:** D5, G7
Expand Down
10 changes: 10 additions & 0 deletions eval/fixtures/g3-version-skew/golden.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"failureMode": "G3",
"app": "./new",
"note": "Version skew & rename tracking (6.4): a definition renamed InvoiceCard -> BillingCard and moved InvoiceCard.tsx -> BillingCard.tsx between two graph versions. This fixture's golden validates the post-rename ('current') graph directly — BillingCard exists, traces /api/invoices, and matches its rendered text. The cross-version rename DIFF (old graph + new graph -> 'renamed BillingCard' warning) is a two-graph property, exercised by unit tests in core (diffRenames), parser-react (real scan of ./old and ./new), and agent-sdk (buildBundle currentGraph warning).",
"expect": {
"components": [{ "name": "BillingCard", "instances": 0 }],
"attributions": [{ "component": "BillingCard", "endpoints": ["/api/invoices"] }],
"queries": [{ "terms": ["Invoice summary"], "status": "ok", "top": "BillingCard" }]
}
}
22 changes: 22 additions & 0 deletions eval/fixtures/g3-version-skew/new/BillingCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useEffect, useState } from "react";

// Post-rename version (the "current"/main graph). Same body as
// ../old/InvoiceCard.tsx, but the component is renamed InvoiceCard -> BillingCard
// and the file moved InvoiceCard.tsx -> BillingCard.tsx. A ticket resolved
// against the old graph must warn that the live definition is now BillingCard.
export function BillingCard() {
const [rows, setRows] = useState<unknown[]>([]);

useEffect(() => {
fetch("/api/invoices")
.then((res) => res.json())
.then(setRows);
}, []);

return (
<article>
<h3>Invoice summary</h3>
<p>{rows.length} line items</p>
</article>
);
}
22 changes: 22 additions & 0 deletions eval/fixtures/g3-version-skew/old/InvoiceCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useEffect, useState } from "react";

// Pre-rename version (the "production" graph an agent resolves a ticket against).
// On main this definition is renamed AND moved — see ../new/BillingCard.tsx.
// The body is identical across versions, which is what lets diffRenames pair
// them by signature even though the name and file both change.
export function InvoiceCard() {
const [rows, setRows] = useState<unknown[]>([]);

useEffect(() => {
fetch("/api/invoices")
.then((res) => res.json())
.then(setRows);
}, []);

return (
<article>
<h3>Invoice summary</h3>
<p>{rows.length} line items</p>
</article>
);
}
60 changes: 60 additions & 0 deletions packages/agent-sdk/src/bundle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,66 @@ describe("buildBundle (TRACKER 5.2, F1)", () => {
});
});

describe("buildBundle version skew (6.4, G3/A11)", () => {
// A card that renders "Invoice summary" — identical body across versions, so
// only the name/file distinguish the old graph from the current one.
function cardGraph(name: string, file: string): LineageGraph {
const component: ComponentNode = {
id: nodeId("component", file, name),
kind: "component",
name,
loc: loc(file),
exportName: name,
props: [],
renderedText: [{ text: "Invoice summary", source: "jsx" }],
rendersComponents: [],
structure: {
table: 0,
columns: 0,
form: 0,
input: 0,
button: 0,
link: 0,
image: 0,
heading: 1,
list: 0,
repeated: 0,
},
};
return {
version: 2,
root: "/app",
generatedAt: "2026-01-01T00:00:00.000Z",
generator: "test",
nodes: [component],
edges: [],
};
}

const resolved = cardGraph("InvoiceCard", "InvoiceCard.tsx");
const current = cardGraph("BillingCard", "BillingCard.tsx");
const ticket = { text: "the Invoice summary card is wrong" };

it("warns with the new name+file when the matched definition was renamed/moved", () => {
const bundle = buildBundle(resolved, ticket, { currentGraph: current, budgetTokens: 8000 });
expect(bundle.match[0]?.component).toBe("InvoiceCard");
const skew = bundle.warnings.find((w) => w.startsWith("version skew"));
expect(skew).toBeDefined();
expect(skew).toContain("`BillingCard`");
expect(skew).toContain("BillingCard.tsx");
});

it("emits no version-skew warning when the current graph is unchanged", () => {
const bundle = buildBundle(resolved, ticket, { currentGraph: resolved, budgetTokens: 8000 });
expect(bundle.warnings.some((w) => w.startsWith("version skew"))).toBe(false);
});

it("emits no version-skew warning when no current graph is provided", () => {
const bundle = buildBundle(resolved, ticket, { budgetTokens: 8000 });
expect(bundle.warnings.some((w) => w.startsWith("version skew"))).toBe(false);
});
});

describe("coverage-unmapped downgrade (TRACKER 6F.6, F3)", () => {
// A graph where test files EXIST but almost none map to a component — the
// field-found signature (tests present, ~0 covered-by edges). The matched
Expand Down
27 changes: 27 additions & 0 deletions packages/agent-sdk/src/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import {
blastRadius,
type DataSourceNode,
diffRenames,
findRename,
type ImpactNode,
type JourneyPath,
journeys,
Expand Down Expand Up @@ -86,6 +88,13 @@ export interface BundleOptions {
journeyDepth?: number;
/** Max recent commits to include in the history section. Default 5. */
historyLimit?: number;
/**
* The current-code graph (e.g. main) to diff the resolved match against for
* version skew (6.4, G3/A11). When the matched definition was renamed or
* moved in this graph, the bundle warns with the new name/path so the agent
* edits the right file. Omit when resolving against up-to-date code.
*/
currentGraph?: LineageGraph;
}

/** Trim sections in this reverse-priority order until the bundle fits its budget. */
Expand Down Expand Up @@ -146,6 +155,24 @@ export function buildBundle(

const top = match.candidates[0]?.value;
if (top !== undefined) {
// Version skew (6.4, G3/A11): the match came from the resolved graph, which
// may lag the current code. If the top definition was renamed/moved on the
// current graph, surface the new identity so the agent edits the live file.
if (options.currentGraph !== undefined) {
const renamed = findRename(
diffRenames(graph, options.currentGraph),
top.component.name,
top.component.loc.file,
);
if (renamed !== undefined) {
const moved = renamed.to.file !== renamed.from.file;
bundle.warnings.push(
`version skew — matched \`${renamed.from.name}\` (${renamed.from.file}) in the resolved graph; ` +
`renamed \`${renamed.to.name}\`${moved ? ` (${renamed.to.file})` : ""} in the current graph`,
);
}
}

const definitionLineage = traceLineage(graph, top.component.id).candidates[0]?.value;
if (definitionLineage !== undefined) {
bundle.lineage.push({
Expand Down
54 changes: 43 additions & 11 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ import {
type LineageGraph,
loadCorrections,
loadGraph as loadGraphFile,
loadGraphFromStore,
matchComponents,
recordCorrection,
saveGraph,
saveGraphToStore,
traceLineage,
} from "@coderadar/core";
import { buildBundle, resolveContext } from "@coderadar/agent-sdk";
Expand All @@ -40,13 +42,21 @@ program
.argument("<dir>", "directory to scan")
.option("-o, --out <file>", "output file", "ui-lineage.graph.json")
.option("--openapi <file>", "OpenAPI 3 JSON spec (relative to <dir>) for response-schema linking")
.action((dir: string, opts: { out: string; openapi?: string }) => {
.option(
"--store",
"also save into the SHA-keyed store (.coderadar/graphs/<sha>.json + latest) for version-skew diffs (6.4)",
)
.action((dir: string, opts: { out: string; openapi?: string; store?: boolean }) => {
const meta = collectGraphMeta(path.resolve(dir));
const graph = {
...resolveHookEdges(scanReact({ root: dir, ...(opts.openapi ? { openapi: opts.openapi } : {}) })),
meta,
};
saveGraph(graph, opts.out);
if (opts.store === true) {
const stored = saveGraphToStore(graph, path.resolve(dir));
console.log(` stored: ${stored}`);
}
const counts = new Map<string, number>();
for (const node of graph.nodes) {
counts.set(node.kind, (counts.get(node.kind) ?? 0) + 1);
Expand Down Expand Up @@ -224,16 +234,38 @@ program
.option("-g, --graph <file>", "graph file", "ui-lineage.graph.json")
.option("-s, --screenshot", "the ticket has a screenshot attached")
.option("-b, --budget <n>", "token budget", "4000")
.action((text: string, opts: { graph: string; screenshot?: boolean; budget: string }) => {
const graph = loadGraph(opts.graph);
const budgetTokens = Number.parseInt(opts.budget, 10);
const bundle = buildBundle(
graph,
{ text, ...(opts.screenshot ? { screenshots: 1 } : {}) },
{ budgetTokens: Number.isNaN(budgetTokens) ? 4000 : budgetTokens },
);
console.log(JSON.stringify(bundle, null, 2));
});
.option(
"--against <version>",
"diff the match against a stored graph version (a commit SHA or 'latest') to warn on renamed/moved definitions (6.4)",
)
.action(
(
text: string,
opts: { graph: string; screenshot?: boolean; budget: string; against?: string },
) => {
const graph = loadGraph(opts.graph);
const budgetTokens = Number.parseInt(opts.budget, 10);
let currentGraph: LineageGraph | undefined;
if (opts.against !== undefined) {
try {
currentGraph = loadGraphFromStore(graph.root, opts.against);
} catch (err) {
console.error(`--against: ${(err as Error).message}`);
process.exitCode = 1;
return;
}
}
const bundle = buildBundle(
graph,
{ text, ...(opts.screenshot ? { screenshots: 1 } : {}) },
{
budgetTokens: Number.isNaN(budgetTokens) ? 4000 : budgetTokens,
...(currentGraph !== undefined ? { currentGraph } : {}),
},
);
console.log(JSON.stringify(bundle, null, 2));
},
);

program
.command("impact")
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export * from "./result.js";
export * from "./query.js";
export * from "./corrections.js";
export * from "./storage.js";
export * from "./renames.js";
export * from "./text.js";
Loading
Loading