Skip to content

Support immutable routes during trace simplification - #1780

Merged
seveibar merged 3 commits into
mainfrom
agent/immutable-trace-simplifier
Jul 28, 2026
Merged

Support immutable routes during trace simplification#1780
seveibar merged 3 commits into
mainfrom
agent/immutable-trace-simplifier

Conversation

@seveibar

@seveibar seveibar commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add an optional otherHdRoutes input to TraceSimplificationSolver
  • propagate immutable peer routes through via removal, same-net via merging, and path simplification
  • include immutable routes in collision indexes without adding them to mutation queues or solver output
  • resolve synthetic peer-route connectivity through rootConnectionName
  • render immutable peers as subdued, dashed layer-colored traces and cover the result with an SVG snapshot

Why

A simplifier sometimes needs to avoid copper that is already routed but must not modify it. Previously the only way for a route to participate in simplification collision checks was to include it in the editable route list. Pipeline9 exposed this gap for preloaded traces, but this change is generic and has no Pipeline9 dependency.

This is the first independently mergeable prerequisite extracted from #1755.

Validation

  • bun test tests/features/trace-simplification-immutable-routes.test.ts tests/bugs/simplification-trace-overlap-repro.test.ts tests/solvers/same-net-via-merger-direct-overlap-star.test.ts tests/pipeline7-trace-simplification-net-colors.test.ts
  • bunx tsc --noEmit
  • bun run build
  • git diff --check
  • SVG snapshot: trace-simplification-immutable-routes-immutable-routed-peer.snap.svg

The focused regression separately verifies path shortening, two-via layer-collapse removal, same-net via merging, immutability, output exclusion, and completed visualization.

@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
capacity-node-autorouter Ready Ready Preview, Comment Jul 28, 2026 6:47pm

Request Review

@tscircuitbot

Copy link
Copy Markdown
Contributor

Benchmark This PR

Run benchmarks by commenting on this PR:

/benchmark [benchmark.sh args...]
/benchmark-long [benchmark.sh args...]
/benchmark-all

Comment /benchmark to run the default dataset, or append any arguments accepted by ./benchmark.sh.
Comment /benchmark-long for an 8-vCPU run that defaults to 8 workers and has an eight-hour timeout.
Comment exactly /benchmark-all to start separate workflow runs and result comments for the default dataset plus srj18, srj19, srj20, srj21, and srj23.

Everything after /benchmark or /benchmark-long is safely forwarded to ./benchmark.sh, except --profile-solvers, which enables profile comparison tables.
Examples: /benchmark --dataset 18 --sample-timeout 2000s, /benchmark --pipeline 7 --scenario-limit 20, /benchmark all 20 --concurrency 8, and /benchmark-long --dataset 18.

Use /update-snapshots (or /us) to run BUN_UPDATE_SNAPSHOTS=1 bun test --timeout 120_000 on the PR branch and auto-commit snapshot updates.
Use /usf to read recent failed test files, update and verify their exact CI test shards, and auto-commit only their snapshots. It uses the configured fast benchmark runner by default; use /usf --ubuntu-latest for GitHub-hosted x64 CI parity.

Any PR whose title contains [BENCHMARK TEST] will automatically run one default-dataset benchmark on PR updates; it does not post a PR result comment.

@seveibar
seveibar marked this pull request as ready for review July 28, 2026 18:56
@seveibar
seveibar requested a review from 0hmX July 28, 2026 18:56
Comment on lines +164 to +285
test("trace simplification avoids immutable routed traces without emitting or mutating them", () => {
const immutableSnapshot = structuredClone(immutableRoute)

const routesWithoutFixedCopper = solve()
expect(routesWithoutFixedCopper).toHaveLength(1)
expect(
getMinimumRouteDistance(routesWithoutFixedCopper[0]!, immutableRoute),
).toBe(0)

const routesWithFixedCopper = solve([immutableRoute])
expect(routesWithFixedCopper).toHaveLength(1)
expect(routesWithFixedCopper[0]!.connectionName).toBe("editable")
expect(
getMinimumRouteDistance(routesWithFixedCopper[0]!, immutableRoute),
).toBeGreaterThanOrEqual(0.25)
expect(immutableRoute).toEqual(immutableSnapshot)
})

test("trace simplification visualizes immutable routed peers", () => {
const solver = createTraceSimplifier([immutableRoute])
solver.solve()

expect(solver.failed).toBe(false)
const simplifiedRoute = solver.simplifiedHdRoutes[0]!
expect(editableRoute.route).toHaveLength(9)
expect(simplifiedRoute.route).toHaveLength(5)

const immutableGraphics = getRouteGraphics({
route: immutableRoute,
strokeColor: "#3f3f46",
strokeWidth: 0.16,
strokeDash: [0.06, 0.06],
pointFill: "#d4d4d8",
})
const inputGraphics = mergeGraphics(
getRouteGraphics({
route: editableRoute,
strokeColor: "#d97706",
strokeWidth: 0.12,
pointFill: "#ffedd5",
}),
immutableGraphics,
)
const outputGraphics = mergeGraphics(
getRouteGraphics({
route: editableRoute,
strokeColor: "rgba(217, 119, 6, 0.28)",
strokeWidth: 0.06,
strokeDash: [0.05, 0.05],
}),
{
lines: [
{
points: [
{ x: -2, y: 0 },
{ x: 2, y: 0 },
],
strokeColor: "#dc2626",
strokeWidth: 0.045,
strokeDash: [0.08, 0.08],
},
{
points: [
{ x: -0.11, y: -0.11 },
{ x: 0.11, y: 0.11 },
],
strokeColor: "#dc2626",
strokeWidth: 0.055,
},
{
points: [
{ x: -0.11, y: 0.11 },
{ x: 0.11, y: -0.11 },
],
strokeColor: "#dc2626",
strokeWidth: 0.055,
},
],
},
getRouteGraphics({
route: simplifiedRoute,
strokeColor: "#16803c",
strokeWidth: 0.14,
pointFill: "#dcfce7",
}),
immutableGraphics,
)

const renderPanel = (graphics: GraphicsObject) =>
getSvgFromGraphicsObject(graphics, {
backgroundColor: "white",
svgWidth: 560,
svgHeight: 420,
hideInlineLabels: true,
})

expect(
stackSvgsHorizontally(
[
addPanelHeader({
svg: renderPanel(inputGraphics),
title: "BEFORE • UNSIMPLIFIED EDITABLE ROUTE",
details: [
"9 orange points are eligible for simplification.",
"Gray dashed copper is pre-routed and read-only.",
],
}),
addPanelHeader({
svg: renderPanel(outputGraphics),
title: "AFTER • SIMPLIFIED AROUND FIXED COPPER",
details: [
"5 green points remain (4 removed); faint orange = input.",
"Red direct shortcut was rejected; gray copper is unchanged.",
],
}),
],
{ gap: 12, normalizeSize: false },
),
).toMatchSvgSnapshot(import.meta.path, {
svgName: "immutable-routed-peer",
})
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file contains 4 test(...) calls (at lines 164, 182, 287, and 329), but the rule states that a *.test.ts file may have AT MOST one test(...). After the first test, the remaining tests should be split into separate, numbered files. For example: trace-simplification-immutable-routes1.test.ts, trace-simplification-immutable-routes2.test.ts, trace-simplification-immutable-routes3.test.ts, and trace-simplification-immutable-routes4.test.ts.

Suggested change
test("trace simplification avoids immutable routed traces without emitting or mutating them", () => {
const immutableSnapshot = structuredClone(immutableRoute)
const routesWithoutFixedCopper = solve()
expect(routesWithoutFixedCopper).toHaveLength(1)
expect(
getMinimumRouteDistance(routesWithoutFixedCopper[0]!, immutableRoute),
).toBe(0)
const routesWithFixedCopper = solve([immutableRoute])
expect(routesWithFixedCopper).toHaveLength(1)
expect(routesWithFixedCopper[0]!.connectionName).toBe("editable")
expect(
getMinimumRouteDistance(routesWithFixedCopper[0]!, immutableRoute),
).toBeGreaterThanOrEqual(0.25)
expect(immutableRoute).toEqual(immutableSnapshot)
})
test("trace simplification visualizes immutable routed peers", () => {
const solver = createTraceSimplifier([immutableRoute])
solver.solve()
expect(solver.failed).toBe(false)
const simplifiedRoute = solver.simplifiedHdRoutes[0]!
expect(editableRoute.route).toHaveLength(9)
expect(simplifiedRoute.route).toHaveLength(5)
const immutableGraphics = getRouteGraphics({
route: immutableRoute,
strokeColor: "#3f3f46",
strokeWidth: 0.16,
strokeDash: [0.06, 0.06],
pointFill: "#d4d4d8",
})
const inputGraphics = mergeGraphics(
getRouteGraphics({
route: editableRoute,
strokeColor: "#d97706",
strokeWidth: 0.12,
pointFill: "#ffedd5",
}),
immutableGraphics,
)
const outputGraphics = mergeGraphics(
getRouteGraphics({
route: editableRoute,
strokeColor: "rgba(217, 119, 6, 0.28)",
strokeWidth: 0.06,
strokeDash: [0.05, 0.05],
}),
{
lines: [
{
points: [
{ x: -2, y: 0 },
{ x: 2, y: 0 },
],
strokeColor: "#dc2626",
strokeWidth: 0.045,
strokeDash: [0.08, 0.08],
},
{
points: [
{ x: -0.11, y: -0.11 },
{ x: 0.11, y: 0.11 },
],
strokeColor: "#dc2626",
strokeWidth: 0.055,
},
{
points: [
{ x: -0.11, y: 0.11 },
{ x: 0.11, y: -0.11 },
],
strokeColor: "#dc2626",
strokeWidth: 0.055,
},
],
},
getRouteGraphics({
route: simplifiedRoute,
strokeColor: "#16803c",
strokeWidth: 0.14,
pointFill: "#dcfce7",
}),
immutableGraphics,
)
const renderPanel = (graphics: GraphicsObject) =>
getSvgFromGraphicsObject(graphics, {
backgroundColor: "white",
svgWidth: 560,
svgHeight: 420,
hideInlineLabels: true,
})
expect(
stackSvgsHorizontally(
[
addPanelHeader({
svg: renderPanel(inputGraphics),
title: "BEFORE • UNSIMPLIFIED EDITABLE ROUTE",
details: [
"9 orange points are eligible for simplification.",
"Gray dashed copper is pre-routed and read-only.",
],
}),
addPanelHeader({
svg: renderPanel(outputGraphics),
title: "AFTER • SIMPLIFIED AROUND FIXED COPPER",
details: [
"5 green points remain (4 removed); faint orange = input.",
"Red direct shortcut was rejected; gray copper is unchanged.",
],
}),
],
{ gap: 12, normalizeSize: false },
),
).toMatchSvgSnapshot(import.meta.path, {
svgName: "immutable-routed-peer",
})
})
test("trace simplification avoids immutable routed traces without emitting or mutating them", () => {
const immutableSnapshot = structuredClone(immutableRoute)
const routesWithoutFixedCopper = solve()
expect(routesWithoutFixedCopper).toHaveLength(1)
expect(
getMinimumRouteDistance(routesWithoutFixedCopper[0]!, immutableRoute),
).toBe(0)
const routesWithFixedCopper = solve([immutableRoute])
expect(routesWithFixedCopper).toHaveLength(1)
expect(routesWithFixedCopper[0]!.connectionName).toBe("editable")
expect(
getMinimumRouteDistance(routesWithFixedCopper[0]!, immutableRoute),
).toBeGreaterThanOrEqual(0.25)
expect(immutableRoute).toEqual(immutableSnapshot)
})

Spotted by Graphite (based on custom rule: Custom rule)

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

@seveibar
seveibar merged commit 3ba50fa into main Jul 28, 2026
14 checks passed
@tscircuitbot

Copy link
Copy Markdown
Contributor

Thank you for your contribution! 🎉

PR Rating: ⭐⭐⭐
Impact: Major

Track your contributions and see the leaderboard at: tscircuit Contribution Tracker


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants