Spec · v2 draft · hardened by a 5-lens adversarial panel · for red-pen
What did this change actually rewire?
An agent changed three files and opened a PR. The line-diff shows what text moved. It does not show what wired. Point Plato at the before & after commits and get the structural delta — the producer→consumer edges this change added and removed — grouped by the files you touched, dangerous ones first, with the same never-lie seal every Plato answer carries.
v2 — folded the panel’s findings (incl. one real never-lie blocker) · not approved · no building yet
What the adversarial panel changed (read first): a 5-lens opus panel ran before your red-pen. It caught a real never-lie hole — v1’s fence only covered one of an edge’s two endpoints, so a file that fails to parse in the new commit would have reported all its wires as confidently “removed.” Recomputed from raw & folded into v2 (full changelog at the bottom). Your biggest decision — no “changed” category — was confirmed stronger: the function that would vary it doesn’t exist in the code.
01
Decisions locked
Grounded, not assumed (frontier scan on disk + recompute-from-raw). v2 reprioritized what the differentiator actually is.
Approach
Build both maps in full, then set-diff
Field default — bazel-diff, NDepend, and JS/TS’s own dependency-cruiser all rebuild-full-then-analyze. Plato already builds the map at any commit; the build side is cheap.
The differentiator (v2)
Honest blind-spot handling — not rename-quiet
The property no git diff of a graph can offer: when an edge looks removed, Plato says whether it’s actually gone or the file just went blind. Requires two fence checks (decision 5). Rename-quiet is demoted to the noise floor that makes the signal legible.
Categories (confirmed from raw)
Added & removed only — no “changed”
The falsifier proved mergeRuntimeEdgesdoes not exist in the code — every edge is source: static / scip, constant. A “changed” axis would flag a difference that can never appear. Lands free when runtime-edges arrive.
02
The non-negotiable floor — now correctly two-sided
R1: the diff never reports a visibility change as a confirmed rewire — on either endpoint. An edge has two ends, and either can go blind: the consumer moved into a string registry (inbound — query #1 ships this), or the source file went blind — failed to parse, import unresolvable, or a dynamic import() (outbound — new work, the high-danger case: a parse failure makes all a file’s edges vanish). v1 only checked the first. Every delta touching a blind class on either axis, in either commit, is flagged a possible visibility change. A confident “you safely deleted this wire” that’s actually blind is the cardinal sin.
03
The journey — one delta, end to end
Follow a single change through. The load-bearing stop (the v2 fix) is in red. Each stop drills down to the machinery.
Two commits in.Name before (A) and after (B), or just B against HEAD — or one commit, which diffs against its parent (like git show). Fail-closed on a bad ref.
Build both maps.Plato builds its full producer→consumer graph at each commit (new public buildSnapshotAtCommit over the proven __pin path). A soft-warn fires if a cold build runs long, so it’s never a silent hang.
Rename-normalize (with -l0).Ask git which files were renamed and rewrite before’s node IDs into after’s names, so a pure rename is invisible churn. -l0 is load-bearing: without it git silently stops detecting renames above 100 changed files — so the diff surfaces a loud rename_detection: degraded flag if that ever happens.↘ go deeper
git diff -M -l0 --name-status <A> <B> → {oldPath→newPath, similarity}; capture stderr; if “skipping inexact rename detection” appears → degraded. Rewrite every edge.from/edge.to in A through the map (A→B). This rename-quiet step is Plato’s OWN requirement for human-readability — bazel-diff explicitly does delete+add; not a field default.
Set-diff the edges.After normalization: edges in B not A are added; in A not B are removed. Files genuinely added/deleted surface as added/removed nodes.
Fence BOTH axes — the v2 fix (R1).For each added/removed edge, check both endpoints in both commits: consumer-side blindness (string-registry / .astro) AND source-side blindness (parse-failed / unresolved / dynamic-sink). Any hit → tagged a possible visibility change, never a clean rewire.↘ go deeper
Consumer side: reuse classifyInboundBlindness(edge.to,…) (inbound-fence.mjs:238, exists). Source side: NEW classifyOutboundBlindness(edge.from, graphUnknown, graphRisk) — fires on why∈{parse-failed, file-too-large-to-parse}, unresolved-outbound, dynamic-sink. Needs NEW read-only accessors getGraphUnknown()/getGraphRisk()/getInboundIndex() on the shared VerifiedSnapshot class (today sealed private fields) — so this is net-new surface, not free reuse; blast-radius noted in rollback.
Orphan check (cheap consequence).For each removed edge whose producer now has zero remaining consumers in B → flag “may now be orphaned/dead.” The orphan thesis applied to a diff — a trivial post-pass over the existing backward index. (In v1 if you approve C4.)
Stamp the currency seal.Both commits are content-addressed — the diff is reproducible by construction. The seal carries both pins and each one’s honest relation to HEAD. A diverged (force-push) pair still computes but carries a “no common ancestor — churn advisory” caveat.
The answer.Grouped by the files you changed, caveated/orphaned edges first, a one-line headline, each edge rendered human-readably (page.tsx imports git/layer.mjs) — not a flat tuple dump.
04
Your calls — the red-pen targets
Six now (v2 added three the panel forced). Starred are the ones I most want you to set.
C1 ★
Confirm: added/removed only, no “changed.”
Verified from raw — the varying-source function doesn’t exist. Recommend: yes; “changed” lands free when runtime-edges arrive.
C4 ★
(Reframed) Pull the cheap blast-radius INTO v1?
The panel’s strongest point: a reviewer acts on consequence, not the bare edge delta. Nearly free: flag any removed edge that leaves its producer with zero consumers as “may now be orphaned.” Recommend: include it in v1; defer the fuller downstream blast-radius. The alternative ships “what moved” but not “what it means.”
Group by changed file, sort blind/orphaned edges first, headline line, human-readable edges. Recommend: yes — this is what makes it an inspector, not a graph-git-diff.
C5
(New) Working-tree scope.
v1 = committed pins only; uncommitted-changes diff is a named fast-follow. Recommend: defer (reproducibility stays clean). Flagged so it’s a decision, not a silent gap — your opening PR scenario is often still uncommitted.
C3
Surface shape + single-commit ergonomics.
plato_diff MCP + plato diff <A> <B>; and plato diff <commit> = diff against its parent (like git show), so you never hand-resolve a SHA. Recommend: yes.
05
Honest hard problems
renameLimit degradation
git silently stops detecting renames above 100 changed files. Handled by -l0 + a loud rename_detection: degraded flag — never silent phantom churn.
File split (x→y+z)
git -M matches one best target; the other half reads as new, mis-attributing edges. A quiet mis-attribution — bounded by flagging renames whose source edge-count drops sharply as “possible split — verify.” Named, not hidden.
Diverged (force-push) pair
git -M across unrelated history mis-pairs files. When the envelope is diverged, the delta still computes but carries a “no common ancestor — advisory” caveat.
Two full builds ≈ 2× cost
Inspector query, not the edit loop; HEAD side is cache-hit; soft-warn over ~60s. Not a v1 blocker.
Fence completeness
Bounded, enumerated blind classes (string-registry / .astro / dynamic-import / parse-failed / unresolved-outbound), each with its own gate leg + deliberate-break teeth.
06
How we’ll know it works
Run from raw by the lead, on Neuron (read-only) + fixtures for the teeth.
True rewire detected.Real added import → added; I recompute before/after edge sets from git and they match.
Rename is quiet, incl. over-limit (teeth).Renamed file, contents unchanged → zero deltas; break the rewrite → phantom churn → RED. Plus >100 renamed files → still zero with -l0, else loud degraded — never silent.
Blind-class honesty, BOTH axes (teeth).Consumer→registry → caveat. The danger case: source file made unparseable in B → all its vanished edges carry the outbound caveat, not a confident “removed.” Drop the graphUnknown check → reports clean “removed” → RED.
Orphaned-on-removal(if C4=include) — removing the last consumer flags the producer.
Currency reproducibility.Both pins + honest relation to HEAD; diff(A,HEAD) re-pins; moved HEAD shows drift; diverged pair carries the advisory.
Read-only on Neuron: only git ls-tree/blob reads + git diff -M — never the working tree of ~/vetharness-sites.
07
Reference strip & provenance
By-type lookup. Frontier evidence is on disk (non-author grep-verifiable): docs/ai/structural-diff-frontier-scan.md. The graded-review leg (reviewer ≠ author) runs after your red-pen of v2, before build.
v1 claimed the outbound blind-class check was free reuse of query #1. Raw proof: it’s inbound-only, the snapshot seals its fence inputs, and a parse-failed file’s edges vanish → would report as confident “removed.” Now: new classifyOutboundBlindness + accessors + G-DIFF-4 teeth.
HIGHrenameLimit
git -M silently skips above 100 files. Now -l0 + loud degraded flag + over-limit fixture.
HIGHC4 reframe
Pulled the cheap orphaned-on-removal consequence into v1; output gains grouping + salience (was a flat tuple dump).
HIGHC5 working-tree
Silently-omitted dirty-tree case is now an explicit (deferred) decision.
MEDReframings
Rename-quiet demoted to noise-floor; blind-class honesty elevated to the differentiator. Citation honesty (rename-quiet is Plato’s own, not bazel-diff’s). Frontier brief landed on disk. Merge/diverged, file-split, perf soft-guard, reserved-slot trigger.
CONFIRMEDC1 stronger
All five raw-fact claims confirmed. mergeRuntimeEdges doesn’t exist — edge source is genuinely constant.