Skip to content

fix: make getString render ids stable per circuit (#2848)#2849

Open
DPS0340 wants to merge 1 commit into
tscircuit:mainfrom
DPS0340:fix/circuit-stable-render-id
Open

fix: make getString render ids stable per circuit (#2848)#2849
DPS0340 wants to merge 1 commit into
tscircuit:mainfrom
DPS0340:fix/circuit-stable-render-id

Conversation

@DPS0340

@DPS0340 DPS0340 commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Closes #2848.

Rendering the same board three times in one process produced three different warnings:

before                                    after
<trace#14(from:.R1 > .pin1 to:net.VCC) />  <trace#14(...) />
<trace#36(...) />                          <trace#14(...) />
<trace#58(...) />                          <trace#14(...) />

getString() embeds _renderId, which is a module-level counter that never resets — so the number reflects how many components the process has constructed, not anything about the circuit.

Why the fix is in the display path, not the counter

_renderId has to stay process-unique: cssSelectPrimitiveComponentAdapter compares it for identity (a._renderId === b._renderId). Resetting the counter per circuit would break that. So getString() now offsets against the lowest id in the circuit, computed once and memoised on the root:

_getCircuitRelativeRenderId(): string {
  // walk the tree once, cache the minimum on root._renderIdOrigin
  const relative = Number(this._renderId) - root._renderIdOrigin
  if (!Number.isFinite(relative) || relative < 0) return this._renderId
  return `${relative}`
}

It falls back to the raw id whenever the offset can't be computed (no root yet, non-numeric id, negative result), so no call site can end up worse off than today.

Verification

The test bites. Reverting PrimitiveComponent.ts:

expect(new Set(messages).size).toBe(1)
Expected: 1
Received: 3

A second test guards the obvious way to "fix" this wrongly: ids must stay distinct within a circuit. Two resistors on one board still produce two different strings, so collapsing everything to a constant can't pass.

I also checked that circuits of different shapes still get different numbers (a board with an extra capacitor reports trace#26 vs trace#14) — the id remains meaningful, it just stops depending on process history.

Suite: 1261 pass / 0 fail, tsc --noEmit 0 errors, biome format clean. No snapshots changed.

How this was found

Diffing two renders of an identical board element by element. Of 105 elements, source_unnamed_trace_warning was the only one that differed — which is what pointed at getString() rather than at any single component.

Worth noting the blast radius: getString() also feeds the "routed outside the board boundaries", "chipRef without connections", "manual placement and prop coordinates", "schSheetName does not match", "is ambiguous: references multiple non-overlapping pads", "Missing ports" and "Failed to route net islands" messages. All of them inherit the same instability today.

@vercel

vercel Bot commented Jul 25, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
tscircuit-core-benchmarks Ready Ready Preview, Comment Jul 25, 2026 5:27pm

Request Review

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.

warning/error messages embed a process-global counter, so identical circuits produce different text

1 participant