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
36 changes: 36 additions & 0 deletions .github/workflows/perf.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# CodeRadar scale benchmark (TRACKER step 6.2, failure mode D3).
#
# Generates a ~2,000-file synthetic React app, scans it, and asserts the perf
# budget: full scan under 5 minutes and peak RSS under 4 GB. Runs nightly so a
# scaling regression fails loudly without slowing the per-PR CI. Also runnable
# on demand via the Actions "Run workflow" button.

name: Perf

on:
schedule:
- cron: "0 6 * * *" # 06:00 UTC nightly
workflow_dispatch:

jobs:
bench:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4 # reads the packageManager field

- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm

- name: Install
run: pnpm install --frozen-lockfile

- name: Build
run: pnpm build

- name: Scale benchmark (budget-gated)
run: node eval/dist/bench.js --files 2000 --budget-seconds 300 --budget-rss-mb 4096
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,9 @@ eval/scorecard.json

# ui-lineage scanned graphs
*.graph.json

# ui-lineage SHA-keyed graph store (6.4)
.coderadar/

# Scale benchmark synthetic app (6.2, generated/regenerable)
eval/bench/
131 changes: 118 additions & 13 deletions TRACKER.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

## Status

- **Current phase:** 6FField hardening, feedback round 1 (runs before 6.1–6.5)
- **Next step:** publish 0.4.1 to the tester (all shippable fixes: 6F.6–6F.10 + visualizer). 6F.6 detection half remains blocked on a real failing test file — its defensive `coverage-unmapped` half shipped.
- **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) · **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.
- **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.
- **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 @@ -313,30 +313,135 @@ The heart of the project. C1 and B1 live here.

## Phase 6 — Lifecycle, scale, hardening

### [ ] 6.1 Incremental re-scan
### [x] 6.1 Incremental re-scan
**Failure modes:** D1, G2
**Build:** per-file content hashes in `GraphMeta`; `scan --update` re-parses only changed files + dependents (import graph), rebuilds affected cross-file passes (instances/prop-flow are the tricky part — dependents include all parents of changed components). `--watch` mode for dev.
**Accept:** correctness: incremental result deep-equals full re-scan on 20 randomized single-file edits of the bench repo (property test); 10-file change < 15 s.

### [ ] 6.2 Scale & performance
**Done:** `scanReact` split into `createScanProject` (build + load the ts-morph
project) and `scanProject` (the full analysis). New `IncrementalScanner` keeps
one project alive; `update()` calls `refreshFromFileSystemSync` on each source
file — ts-morph re-parses only files whose bytes changed — picks up added files
and drops deleted ones, then re-runs `scanProject`. **Correctness by
construction:** every node and cross-file edge is re-derived from the current
ASTs each update, so an update's graph is byte-identical to a fresh full scan —
the "dependents" problem (a changed component's parents' instance/prop-flow
attribution, store/route/journey wiring) is handled for free because the global
passes always re-run; the incremental win is parsing (unchanged files keep cached
ASTs). Property test proves it: an interconnected app (pages → components → atoms
+ hook, cross-file imports/fetches) stays byte-identical to a full re-scan across
**20 randomized single-file edits** (rendered text / endpoint / re-pointed
cross-file import), plus add-file and delete-file cases with exact `changed`
reporting. `GraphMeta.fileHashes` (relative path → sha256, schema regenerated,
drift gate green) records provenance; `projectFileHashes` computes it.
CLI: `scan --update` short-circuits when every file hashes identically to the
prior graph (else reports the changed set and full-rescans, correct at ~2 s), and
`scan --watch` re-emits the graph on each debounced file change (ignoring the
output file, `node_modules`, `.coderadar`). Verified end-to-end via the CLI
(--update no-change/changed, --watch live edit). eval 317/0/0/0, determinism
1.000; full suite (parser-react 151), typecheck, lint green.

**Gate 6 — PASSED.** Incremental (6.1) · scale bench < 5 min / < 4 GB, budget-
gated nightly (6.2) · deterministic two-run byte-identity, eval-gated (6.3) ·
version-skew/rename tracking (6.4) · generated/vendored classification + PII
policy enforced (6.5). **M6 reached: production-grade — incremental, fast,
deterministic, versioned.**

### [x] 6.2 Scale & performance
**Failure modes:** D3
**Build:** `eval/bench/` generator (2,000+ file synthetic app with realistic import depth); profile; apply: lazy ts-morph project loading, file-batch parallelism (worker threads), tree-sitter fast path for the text-extraction pass if ts-morph remains the bottleneck. Perf budget asserted in nightly CI.
**Accept:** full scan < 5 min, peak RSS < 4 GB on the bench repo.

### [ ] 6.3 Determinism
**Done:** new `eval/src/bench.ts` deterministically generates a synthetic app
(default ~2,016 files: N features × page → 8 sections → 8 atoms + a hook, with
real import depth and a direct-`fetch` per section so the endpoint pass is
exercised — 1,904 components / 896 data sources / 2,688 instances / 8,176 edges),
scans it, and gates a perf budget (`--files` / `--budget-seconds` /
`--budget-rss-mb`), measuring wall time and peak RSS via
`process.resourceUsage().maxRSS`. **Measured: ~2,016 files in ~2 s, ~400 MB RSS**
— vastly under the 300 s / 4 GB budget (the synthetic app has no tsconfig, so no
type-resolution cost; the field Grafana run was 6,461 files in 72 s / 1.5 GB).
Because ts-morph is nowhere near the bottleneck at target scale, the optional
worker-thread / tree-sitter fast paths were **deliberately not applied** — they'd
add complexity for no measured win; revisit if a future budget run regresses.
Root `pnpm bench` script; nightly `.github/workflows/perf.yml` (schedule +
manual dispatch) runs the budget-gated bench without slowing per-PR CI; the
generated tree (`eval/bench/`) is gitignored and self-cleaned after each run.
eval 317/0/0/0, determinism 1.000; typecheck + lint clean. **Gate 6 fully
passes** once 6.1 lands (incremental re-scan uses this bench for its property
test).

### [x] 6.3 Determinism
**Failure modes:** G8
**Build:** stable ordering everywhere (nodes, edges, candidates — explicit sort keys, no map-iteration order leaks); vision/OCR results cached by image hash; `generatedAt` excluded from equality. Determinism check in the eval runner (two runs, byte-diff).
**Accept:** double-run byte-identical on all fixtures + bench repo.

### [ ] 6.4 Version skew & rename tracking
**Done:** `scanReact` now iterates source files in a stable path order (new
`sortedSourceFiles` — ts-morph returns glob-enumeration order, which varies by
platform) across all three file passes, and returns nodes sorted by `id` + edges
sorted by a total-order key over every identifying field (`edgeSortKey`).
`resolveHookEdges` re-sorts and dedups after rewriting `unresolved-hook:`
placeholders (a rewrite can collide two edges onto one hook id). Candidate
ranking gains a final `id` tiebreak so same-named components in different files
rank deterministically. The eval runner scans every fixture **twice** and
byte-compares (dropping only `generatedAt`): new `determinismStablePct` metric,
printed each run and a **hard gate** — any non-deterministic fixture fails the
run outright. 4 new parser-react unit tests (two-run byte-identical · nodes in id
order · edges in key order · no duplicate edges). Full suite green; eval
304/0/0/0, determinism 1.000, all metrics 1.000. One fragile unit assertion in
`queryfn.test.ts` (relied on node insertion order to find the react-query
`/api/stats` source ahead of the raw fetch it wraps) rewritten to assert the
react-query resolution *exists*, order-independently. Bench-repo half of the
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.

### [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.

### [ ] 6.5 Generated/vendored classification & PII policy
**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
**Build:** classify generated code (headers like `@generated`, codegen paths, sourcemap-less minified files): excluded from matching, retained as API metadata. PII policy doc (`docs/security.md`): screenshots ephemeral, never persisted/embedded/logged; corrections store holds terms only, never images; enforced by a lint test grepping vision-package writes.
**Accept:** generated-code fixture excluded from match candidates but present in lineage; security doc + lint test in CI. **Gate 6 passes.**
**Done:** new `isGeneratedFile` in the scanner classifies a file as machine-
generated by (a) a `__generated__/` or `generated/` directory segment or a
`.generated.`/`.gen.` filename infix, (b) an `@generated` / `DO NOT EDIT` /
`AUTO-GENERATED` banner in the file head, or (c) a sourcemap-less minified line
(≥ 3000 chars). A post-pass tags component and hook nodes in those files with the
`generated` flag — uniform across function components, class components, and
hooks. `matchComponents` drops `generated` components from the candidate pool
(and therefore the IDF corpus), so a screenshot or ticket never resolves to
codegen output, while their nodes/edges — including data sources — stay in the
graph for tracing. New `docs/security.md` states the G7 PII policy (screenshots
ephemeral, graph/corrections hold no image data) and is **enforced** by
`packages/vision/src/policy.test.ts`, which greps the vision source for
filesystem-write and image-persistence APIs and asserts the package never
imports `fs`. New fixture `d5-generated-code` (path-classified `GlyphCatalog` +
banner/filename-classified `SchemaViewer` + hand-written `RevenuePanel`): both
generated components trace to their endpoints yet decline on their rendered text,
`RevenuePanel` matches and isn't poisoned by generated text mixed into the query.
6 parser-react unit tests + 7 vision policy tests. eval 314/0/0/0, determinism
1.000, all metrics 1.000; typecheck + lint clean. **Gate 6 criteria met**
(6.1/6.2/6.4 still to land for the full lifecycle phase).

---

Expand Down Expand Up @@ -629,5 +734,5 @@ Sketch level — detail before starting the phase, after v1 feedback.
| M4 | Screenshot/text → ranked, calibrated, honest matches | 4 |
| M5 ✅ | **Pluggable node:** ticket in → budgeted context bundle out, over MCP | 5 |
| M6F | Field-hardened: v0.3.0 feedback closed — trustworthy matching + real-world extractors + visualizer | 6F |
| M6 | Production-grade: incremental, fast, deterministic, versioned | 6 |
| M6 ✅ | **Production-grade:** incremental, fast, deterministic, versioned | 6 |
| M7 | Full-stack lineage: pixel → backend handler | 7 |
66 changes: 66 additions & 0 deletions docs/security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Security & PII policy

CodeRadar reads source code and, optionally, screenshots attached to a ticket.
Source code is already in the repository the operator controls; **screenshots are
the sensitive surface** — a bug report may show real user data, PII, or internal
UI. This document states how that data is handled and how the policy is enforced
in CI. It maps to failure mode **G7** (privacy) and is part of the **Phase 6**
gate.

## Principles

1. **Screenshots are ephemeral.** An image is processed in memory and discarded.
CodeRadar never writes a screenshot to disk, never embeds one in the lineage
graph, and never logs its bytes. The only place image bytes leave the process
is the vision adapter's call to the model API the operator has configured
(`packages/vision/src/claude.ts`), and only for the duration of that request.

2. **The graph holds no image data.** `scan` emits `ui-lineage.graph.json` from
source code alone. No node or edge carries a screenshot, a crop, a base64
blob, or a data URI. The graph is safe to commit and share.

3. **The corrections store holds terms only.** When a human confirms that a set
of terms resolves to a component, `corrections.jsonl`
(`packages/core/src/corrections.ts`) records the **terms and the component
id** — never the screenshot the terms were read from. Corrections are safe to
check in.

4. **No persistence of derived crops or OCR images.** Vision adapters return
*text and structure* (`VisionExtraction`) — the signals the matcher already
understands — not pixels. Any future OCR/vision cache keys on an image **hash**,
never the image itself.

5. **Least logging.** Diagnostic output names components, files, and terms. It
does not include screenshot bytes or the raw model image payload.

## What may be persisted

| Artifact | Contents | Safe to share |
| --- | --- | --- |
| `ui-lineage.graph.json` | Source-derived nodes/edges | Yes |
| `corrections.jsonl` | Confirmed terms → component id | Yes |
| `ui-lineage.corrections.jsonl` | (same) | Yes |
| Screenshots | — | **Never written** |

## Enforcement

The policy is not just documentation — it is asserted by a test that fails CI if
the vision package gains a filesystem write or a code path that persists image
bytes:

- `packages/vision/src/policy.test.ts` greps the vision source for filesystem
write APIs (`writeFile`, `writeFileSync`, `appendFile*`, `createWriteStream`,
`fs.write`, `mkdir*` used for output) and for image persistence patterns
(writing `.png`/`.jpg`, `toDataURL`, embedding base64 into a written file). Any
hit fails the test. The single legitimate base64 conversion — encoding an image
for the model request in `claude.ts` — is in-memory and is asserted to flow only
into the API `messages.create` call, not into any sink.

Run it with `pnpm --filter @coderadar/vision test`, or as part of `pnpm -r test`
in CI.

## Reporting

If you find a path that writes, embeds, or logs screenshot data, treat it as a
privacy defect (G7): open an issue and add the offending pattern to
`policy.test.ts` so the regression is locked out.
20 changes: 20 additions & 0 deletions eval/fixtures/d5-generated-code/app/RevenuePanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useEffect, useState } from "react";

// A hand-authored component: a screenshot/ticket for its text must resolve here,
// and it must not be crowded out by the generated components alongside it.
export function RevenuePanel() {
const [rows, setRows] = useState<Record<string, number>[]>([]);

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

return (
<section>
<h2>Quarterly revenue breakdown</h2>
<p>{rows.length} periods</p>
</section>
);
}
25 changes: 25 additions & 0 deletions eval/fixtures/d5-generated-code/app/SchemaViewer.generated.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* @generated by coderadar-fixture-codegen — DO NOT EDIT.
*
* Generated by BANNER (the @generated docblock tag) as well as the
* `.generated.` filename infix. Exercises banner-based classification even when
* the path is otherwise ordinary.
*/
import { useEffect, useState } from "react";

export function SchemaViewer() {
const [schema, setSchema] = useState<unknown>(null);

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

return (
<div>
<h3>Generated schema viewer</h3>
<pre>{JSON.stringify(schema)}</pre>
</div>
);
}
Loading
Loading