@tscircuit/schematic-trace-solver 0.0.86 → 0.0.88

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/.agents/skills/schematic-trace-visualization/SKILL.md +115 -0
  2. package/.github/scripts/import-json-bug-report.ts +15 -1
  3. package/.github/workflows/json-bug-report.yml +3 -2
  4. package/AGENTS.md +22 -0
  5. package/dist/index.js +86 -10
  6. package/lib/solvers/Example28Solver/Example28Solver.ts +46 -10
  7. package/lib/solvers/Example28Solver/countCrossingsWithOtherTraces.ts +19 -0
  8. package/lib/solvers/Example28Solver/scoreSolutionTracePath.ts +26 -0
  9. package/lib/testing/PipelineStageDebugRunner.ts +344 -0
  10. package/package.json +3 -2
  11. package/scripts/debug-pipeline-stages.ts +146 -0
  12. package/site/bug-reports/bug-report-20260706T220324Z.page.tsx +4 -0
  13. package/site/bug-reports/bug-report-20260707T020342Z.page.tsx +4 -0
  14. package/site/bug-reports/bug-report-20260707T092615Z.page.tsx +4 -0
  15. package/site/bug-reports/bug-report-20260707T134549Z.page.tsx +4 -0
  16. package/site/bug-reports/bug-report-20260707T134722Z.page.tsx +4 -0
  17. package/site/bug-reports/bug-report-20260707T140410Z.page.tsx +4 -0
  18. package/site/bug-reports/bug-report-20260707T141025Z.page.tsx +4 -0
  19. package/site/bug-reports/bug-report-20260707T141421Z.page.tsx +4 -0
  20. package/tests/bug-reports/bug-report-20260706T220324Z/__snapshots__/bug-report-20260706T220324Z.snap.svg +357 -3448
  21. package/tests/bug-reports/bug-report-20260707T020342Z/__snapshots__/bug-report-20260707T020342Z.snap.svg +64 -0
  22. package/tests/bug-reports/bug-report-20260707T020342Z/bug-report-20260707T020342Z.json +233 -0
  23. package/tests/bug-reports/bug-report-20260707T020342Z/bug-report-20260707T020342Z.test.ts +12 -0
  24. package/tests/bug-reports/bug-report-20260707T092615Z/__snapshots__/bug-report-20260707T092615Z.snap.svg +2098 -0
  25. package/tests/bug-reports/bug-report-20260707T092615Z/bug-report-20260707T092615Z.json +1392 -0
  26. package/tests/bug-reports/bug-report-20260707T092615Z/bug-report-20260707T092615Z.test.ts +12 -0
  27. package/tests/bug-reports/bug-report-20260707T134549Z/__snapshots__/bug-report-20260707T134549Z.snap.svg +857 -0
  28. package/tests/bug-reports/bug-report-20260707T134549Z/bug-report-20260707T134549Z.json +652 -0
  29. package/tests/bug-reports/bug-report-20260707T134549Z/bug-report-20260707T134549Z.test.ts +12 -0
  30. package/tests/bug-reports/bug-report-20260707T134722Z/__snapshots__/bug-report-20260707T134722Z.snap.svg +275 -0
  31. package/tests/bug-reports/bug-report-20260707T134722Z/bug-report-20260707T134722Z.json +265 -0
  32. package/tests/bug-reports/bug-report-20260707T134722Z/bug-report-20260707T134722Z.test.ts +12 -0
  33. package/tests/bug-reports/bug-report-20260707T140410Z/__snapshots__/bug-report-20260707T140410Z.snap.svg +950 -0
  34. package/tests/bug-reports/bug-report-20260707T140410Z/bug-report-20260707T140410Z.json +610 -0
  35. package/tests/bug-reports/bug-report-20260707T140410Z/bug-report-20260707T140410Z.test.ts +12 -0
  36. package/tests/bug-reports/bug-report-20260707T141025Z/__snapshots__/bug-report-20260707T141025Z.snap.svg +218 -0
  37. package/tests/bug-reports/bug-report-20260707T141025Z/bug-report-20260707T141025Z.json +185 -0
  38. package/tests/bug-reports/bug-report-20260707T141025Z/bug-report-20260707T141025Z.test.ts +12 -0
  39. package/tests/bug-reports/bug-report-20260707T141421Z/__snapshots__/bug-report-20260707T141421Z.snap.svg +613 -0
  40. package/tests/bug-reports/bug-report-20260707T141421Z/bug-report-20260707T141421Z.json +499 -0
  41. package/tests/bug-reports/bug-report-20260707T141421Z/bug-report-20260707T141421Z.test.ts +12 -0
  42. package/tests/examples/__snapshots__/example01.snap.svg +59 -162
  43. package/tests/examples/__snapshots__/example02.snap.svg +64 -273
  44. package/tests/examples/__snapshots__/example03.snap.svg +85 -290
  45. package/tests/examples/__snapshots__/example04.snap.svg +50 -94
  46. package/tests/examples/__snapshots__/example05.snap.svg +73 -191
  47. package/tests/examples/__snapshots__/example06.snap.svg +49 -95
  48. package/tests/examples/__snapshots__/example07.snap.svg +65 -218
  49. package/tests/examples/__snapshots__/example08.snap.svg +61 -143
  50. package/tests/examples/__snapshots__/example09.snap.svg +123 -493
  51. package/tests/examples/__snapshots__/example10.snap.svg +57 -136
  52. package/tests/examples/__snapshots__/example11.snap.svg +64 -173
  53. package/tests/examples/__snapshots__/example12.snap.svg +58 -159
  54. package/tests/examples/__snapshots__/example13.snap.svg +78 -348
  55. package/tests/examples/__snapshots__/example14.snap.svg +77 -293
  56. package/tests/examples/__snapshots__/example15.snap.svg +140 -984
  57. package/tests/examples/__snapshots__/example16.snap.svg +62 -162
  58. package/tests/examples/__snapshots__/example17.snap.svg +67 -198
  59. package/tests/examples/__snapshots__/example18.snap.svg +69 -249
  60. package/tests/examples/__snapshots__/example19.snap.svg +63 -202
  61. package/tests/examples/__snapshots__/example20.snap.svg +60 -148
  62. package/tests/examples/__snapshots__/example21.snap.svg +73 -298
  63. package/tests/examples/__snapshots__/example22.snap.svg +62 -156
  64. package/tests/examples/__snapshots__/example23.snap.svg +66 -172
  65. package/tests/examples/__snapshots__/example24.snap.svg +64 -164
  66. package/tests/examples/__snapshots__/example25.snap.svg +76 -299
  67. package/tests/examples/__snapshots__/example26.snap.svg +64 -164
  68. package/tests/examples/__snapshots__/example27.snap.svg +83 -300
  69. package/tests/examples/__snapshots__/example28.snap.svg +54 -115
  70. package/tests/examples/__snapshots__/example29.snap.svg +257 -1299
  71. package/tests/examples/__snapshots__/example30.snap.svg +91 -293
  72. package/tests/examples/__snapshots__/example31.snap.svg +47 -88
  73. package/tests/examples/__snapshots__/example32.snap.svg +107 -405
  74. package/tests/examples/__snapshots__/example33.snap.svg +71 -180
  75. package/tests/examples/__snapshots__/example34.snap.svg +55 -116
  76. package/tests/examples/__snapshots__/example35.snap.svg +69 -176
  77. package/tests/examples/__snapshots__/example36.snap.svg +65 -159
  78. package/tests/examples/__snapshots__/example37.snap.svg +75 -223
  79. package/tests/examples/__snapshots__/example38.snap.svg +46 -76
  80. package/tests/examples/__snapshots__/example39.snap.svg +51 -106
  81. package/tests/examples/__snapshots__/example40.snap.svg +83 -270
  82. package/tests/examples/__snapshots__/example41.snap.svg +54 -118
  83. package/tests/examples/__snapshots__/example42.snap.svg +67 -194
  84. package/tests/examples/__snapshots__/example43.snap.svg +62 -168
  85. package/tests/examples/__snapshots__/example44.snap.svg +86 -369
  86. package/tests/examples/__snapshots__/example45.snap.svg +60 -169
  87. package/tests/examples/__snapshots__/example46.snap.svg +80 -342
  88. package/tests/examples/__snapshots__/example47.snap.svg +82 -323
  89. package/tests/examples/__snapshots__/example48.snap.svg +64 -167
  90. package/tests/examples/__snapshots__/example49.snap.svg +120 -565
  91. package/tests/examples/__snapshots__/example50.snap.svg +71 -195
  92. package/tests/fixtures/getLastStepGraphicsObject.ts +38 -0
  93. package/tests/fixtures/getLastStepSvg.ts +9 -0
  94. package/tests/repros/__snapshots__/bugreport-001-gnd-overlap.snap.svg +67 -218
  95. package/tests/repros/__snapshots__/manufacturePartNumber-text-box.snap.svg +47 -87
  96. package/tests/repros/__snapshots__/netlabel-connector-through-rail-label.snap.svg +73 -272
  97. package/tests/repros/__snapshots__/repro-atmega328p-fault-pullup.snap.svg +284 -0
  98. package/tests/repros/__snapshots__/repro-atmega328p-missing-gnd-netlabel.snap.svg +470 -0
  99. package/tests/repros/__snapshots__/repro-bq24074-battery-charger.snap.svg +94 -395
  100. package/tests/repros/__snapshots__/repro-cc2340r5.snap.svg +88 -467
  101. package/tests/repros/__snapshots__/repro-ina237-current-monitor.snap.svg +109 -569
  102. package/tests/repros/__snapshots__/repro130-bq27441-fuel-gauge-trace-through-c1.snap.svg +95 -459
  103. package/tests/repros/__snapshots__/repro51-overlap-junction-crossing.snap.svg +64 -176
  104. package/tests/repros/__snapshots__/trace-overlap-box-resistor.snap.svg +51 -112
  105. package/tests/repros/assets/repro-atmega328p-fault-pullup.input.json +239 -0
  106. package/tests/repros/assets/repro-atmega328p-missing-gnd-netlabel.input.json +432 -0
  107. package/tests/repros/repro-atmega328p-fault-pullup.test.ts +11 -0
  108. package/tests/repros/repro-atmega328p-missing-gnd-netlabel.test.ts +16 -0
  109. package/tests/solvers/SchematicTraceSingleLineSolver2/__snapshots__/SchematicTraceSingleLineSolver2_01-example17-d1_1-u1_1.snap.svg +59 -159
  110. package/tests/solvers/TraceCleanupSolver/__snapshots__/TraceCleanupSolver.snap.svg +54 -118
  111. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/__snapshots__/TraceLabelOverlapAvoidanceSolver.snap.svg +58 -245
  112. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/renderComparisonView/__snapshots__/renderComparisonView01.snap.svg +73 -222
  113. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/renderComparisonView/__snapshots__/renderComparisonView02.snap.svg +65 -204
  114. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/renderComparisonView/__snapshots__/renderComparisonView03.snap.svg +65 -183
  115. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/__snapshots__/MergedNetLabelObstacles.snap.svg +54 -139
  116. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/__snapshots__/OverlapAvoidanceStepSolver.snap.svg +61 -215
  117. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/__snapshots__/SingleOverlapSolver.snap.svg +54 -124
@@ -0,0 +1,344 @@
1
+ import { mkdir, writeFile } from "node:fs/promises"
2
+ import path from "node:path"
3
+ import {
4
+ getPngBufferFromGraphicsObject,
5
+ getSvgFromGraphicsObject,
6
+ } from "graphics-debug"
7
+ import type { GraphicsObject } from "graphics-debug"
8
+ import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
9
+ import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver"
10
+
11
+ type PipelineStepLike = {
12
+ solverName: string
13
+ }
14
+
15
+ type VisualizingSolver = {
16
+ visualize: () => GraphicsObject
17
+ iterations?: number
18
+ error?: string | null
19
+ solved?: boolean
20
+ failed?: boolean
21
+ }
22
+
23
+ export type PipelineStageArtifact = {
24
+ stageName: string
25
+ stageNumber: number
26
+ pngPath?: string
27
+ svgPath?: string
28
+ graphicsJsonPath?: string
29
+ stepPngPaths: string[]
30
+ }
31
+
32
+ export type PipelineStageDebugRunnerResult = {
33
+ outputDir: string
34
+ logsPath: string
35
+ stageArtifacts: PipelineStageArtifact[]
36
+ solved: boolean
37
+ failed: boolean
38
+ error: string | null
39
+ stoppedAfterStage: string | null
40
+ }
41
+
42
+ export class PipelineStageDebugRunner {
43
+ readonly pipelineSolver: SchematicTracePipelineSolver
44
+ readonly outputDir: string
45
+ readonly logsPath: string
46
+ readonly pngWidth: number
47
+ readonly pngHeight: number
48
+ readonly writePng: boolean
49
+ readonly writeSvg: boolean
50
+ readonly writeGraphicsJson: boolean
51
+ readonly writeStepPngs: boolean
52
+ readonly stopAfterStage?: string
53
+ readonly context: Record<string, string | number | boolean | null | undefined>
54
+ readonly onLog?: (line: string) => void
55
+
56
+ private readonly stageArtifacts: PipelineStageArtifact[] = []
57
+ private readonly capturedStageNumbers = new Set<number>()
58
+
59
+ constructor(opts: {
60
+ pipelineSolver: SchematicTracePipelineSolver
61
+ outputDir: string
62
+ pngWidth?: number
63
+ pngHeight?: number
64
+ writePng?: boolean
65
+ writeSvg?: boolean
66
+ writeGraphicsJson?: boolean
67
+ writeStepPngs?: boolean
68
+ stopAfterStage?: string
69
+ context?: Record<string, string | number | boolean | null | undefined>
70
+ onLog?: (line: string) => void
71
+ }) {
72
+ this.pipelineSolver = opts.pipelineSolver
73
+ this.outputDir = opts.outputDir
74
+ this.logsPath = path.join(this.outputDir, "logs.txt")
75
+ this.pngWidth = opts.pngWidth ?? 1536
76
+ this.pngHeight = opts.pngHeight ?? 1536
77
+ this.writePng = opts.writePng ?? true
78
+ this.writeSvg = opts.writeSvg ?? false
79
+ this.writeGraphicsJson = opts.writeGraphicsJson ?? true
80
+ this.writeStepPngs = opts.writeStepPngs ?? false
81
+ this.stopAfterStage = opts.stopAfterStage
82
+ this.context = opts.context ?? {}
83
+ this.onLog = opts.onLog
84
+ }
85
+
86
+ async run(): Promise<PipelineStageDebugRunnerResult> {
87
+ await mkdir(this.outputDir, { recursive: true })
88
+ await writeFile(this.logsPath, "")
89
+
90
+ await this.log(`startedAt=${new Date().toISOString()}`)
91
+ await this.log(`outputDir=${this.toDisplayPath(this.outputDir)}`)
92
+ await this.log(
93
+ `artifacts png=${this.writePng} svg=${this.writeSvg} graphicsJson=${this.writeGraphicsJson} stepPngs=${this.writeStepPngs} pngSize=${this.pngWidth}x${this.pngHeight}`,
94
+ )
95
+ for (const [key, value] of Object.entries(this.context)) {
96
+ await this.log(`${key}=${value ?? ""}`)
97
+ }
98
+
99
+ let stoppedAfterStage: string | null = null
100
+ let thrownError: unknown = null
101
+
102
+ await this.log(
103
+ this.getStageEnterLogLine(this.pipelineSolver.getCurrentPhase(), 0),
104
+ )
105
+
106
+ while (!this.pipelineSolver.solved && !this.pipelineSolver.failed) {
107
+ const previousStageNumber = this.pipelineSolver.currentPipelineStepIndex
108
+ const previousStageName = this.pipelineSolver.getCurrentPhase()
109
+
110
+ try {
111
+ this.pipelineSolver.step()
112
+ } catch (error) {
113
+ thrownError = error
114
+ }
115
+
116
+ const currentStageNumber = this.pipelineSolver.currentPipelineStepIndex
117
+ const currentStageName = this.pipelineSolver.getCurrentPhase()
118
+
119
+ if (
120
+ currentStageNumber !== previousStageNumber &&
121
+ previousStageName !== "none"
122
+ ) {
123
+ await this.captureStage(previousStageNumber)
124
+ if (
125
+ previousStageName === this.stopAfterStage ||
126
+ String(previousStageNumber + 1) === this.stopAfterStage
127
+ ) {
128
+ stoppedAfterStage = previousStageName
129
+ break
130
+ }
131
+ }
132
+
133
+ if (
134
+ currentStageNumber !== previousStageNumber &&
135
+ currentStageName !== "none"
136
+ ) {
137
+ await this.log(
138
+ this.getStageEnterLogLine(currentStageName, currentStageNumber),
139
+ )
140
+ }
141
+
142
+ if (thrownError) break
143
+ }
144
+
145
+ if (this.pipelineSolver.failed) {
146
+ await this.captureActiveStage()
147
+ }
148
+
149
+ if (thrownError) {
150
+ await this.log(`thrownError=${this.formatError(thrownError)}`)
151
+ }
152
+
153
+ const status = stoppedAfterStage
154
+ ? "stopped"
155
+ : this.pipelineSolver.solved
156
+ ? "solved"
157
+ : this.pipelineSolver.failed
158
+ ? "failed"
159
+ : "incomplete"
160
+
161
+ await this.log(
162
+ `completed status=${status} iterations=${this.pipelineSolver.iterations} error=${this.pipelineSolver.error ?? ""}`,
163
+ )
164
+
165
+ return {
166
+ outputDir: this.outputDir,
167
+ logsPath: this.logsPath,
168
+ stageArtifacts: [...this.stageArtifacts],
169
+ solved: this.pipelineSolver.solved,
170
+ failed: this.pipelineSolver.failed,
171
+ error:
172
+ this.pipelineSolver.error ??
173
+ (thrownError ? this.formatError(thrownError) : null),
174
+ stoppedAfterStage,
175
+ }
176
+ }
177
+
178
+ private async captureActiveStage() {
179
+ const stageNumber = this.pipelineSolver.currentPipelineStepIndex
180
+ if (this.pipelineSolver.getCurrentPhase() === "none") return
181
+ if (this.capturedStageNumbers.has(stageNumber)) return
182
+
183
+ await this.captureStage(stageNumber, this.pipelineSolver.activeSubSolver)
184
+ }
185
+
186
+ private async captureStage(stageNumber: number, solver?: BaseSolver | null) {
187
+ if (this.capturedStageNumbers.has(stageNumber)) return
188
+
189
+ const stageDef = this.getStageDef(stageNumber)
190
+ if (!stageDef) return
191
+
192
+ const stageSolver = solver ?? this.getStageSolver(stageDef.solverName)
193
+ if (!stageSolver) {
194
+ throw new Error(
195
+ `Unable to resolve solver for stage ${stageNumber + 1} "${stageDef.solverName}"`,
196
+ )
197
+ }
198
+
199
+ const basePath = path.join(
200
+ this.outputDir,
201
+ `stage${String(stageNumber + 1).padStart(2, "0")}-${this.getSafeStageName(stageDef.solverName)}`,
202
+ )
203
+ const pngPath = this.writePng ? `${basePath}.png` : undefined
204
+ const svgPath = this.writeSvg ? `${basePath}.svg` : undefined
205
+ const graphicsJsonPath = this.writeGraphicsJson
206
+ ? `${basePath}.graphics.json`
207
+ : undefined
208
+ const graphics = stageSolver.visualize()
209
+ const svg = getSvgFromGraphicsObject(graphics, { backgroundColor: "white" })
210
+
211
+ if (pngPath) {
212
+ await writeFile(pngPath, await this.renderGraphicsToPng(graphics))
213
+ }
214
+
215
+ const stepPngPaths =
216
+ this.writeStepPngs && this.writePng
217
+ ? await this.writeStepPngsForGraphics(basePath, graphics)
218
+ : []
219
+
220
+ if (svgPath) {
221
+ await writeFile(svgPath, svg)
222
+ }
223
+
224
+ if (graphicsJsonPath) {
225
+ await writeFile(graphicsJsonPath, JSON.stringify(graphics, null, 2))
226
+ }
227
+
228
+ this.capturedStageNumbers.add(stageNumber)
229
+ const artifact = {
230
+ stageName: stageDef.solverName,
231
+ stageNumber: stageNumber + 1,
232
+ pngPath,
233
+ svgPath,
234
+ graphicsJsonPath,
235
+ stepPngPaths,
236
+ } satisfies PipelineStageArtifact
237
+ this.stageArtifacts.push(artifact)
238
+
239
+ await this.log(
240
+ `captured stage=${artifact.stageNumber} name=${artifact.stageName} iterations=${stageSolver.iterations ?? 0} png=${pngPath ? this.toDisplayPath(pngPath) : ""} svg=${svgPath ? this.toDisplayPath(svgPath) : ""} steps=${stepPngPaths.length}`,
241
+ )
242
+ }
243
+
244
+ private async writeStepPngsForGraphics(
245
+ basePath: string,
246
+ graphics: GraphicsObject,
247
+ ): Promise<string[]> {
248
+ const stepPngPaths: string[] = []
249
+
250
+ for (const step of this.getObjectSteps(graphics)) {
251
+ const stepPngPath = `${basePath}.step-${step}.png`
252
+ await writeFile(
253
+ stepPngPath,
254
+ await this.renderGraphicsToPng(
255
+ this.filterGraphicsByStep(graphics, step),
256
+ ),
257
+ )
258
+ stepPngPaths.push(stepPngPath)
259
+ }
260
+
261
+ return stepPngPaths
262
+ }
263
+
264
+ private renderGraphicsToPng(graphics: GraphicsObject): Promise<Uint8Array> {
265
+ return getPngBufferFromGraphicsObject(graphics, {
266
+ backgroundColor: "white",
267
+ pngWidth: this.pngWidth,
268
+ pngHeight: this.pngHeight,
269
+ })
270
+ }
271
+
272
+ private getObjectSteps(graphics: GraphicsObject): number[] {
273
+ const steps = new Set<number>()
274
+ for (const objects of this.getGraphicsObjectArrays(graphics)) {
275
+ for (const object of objects) {
276
+ if (typeof object.step === "number") steps.add(object.step)
277
+ }
278
+ }
279
+
280
+ return [...steps].sort((a, b) => a - b)
281
+ }
282
+
283
+ private filterGraphicsByStep(
284
+ graphics: GraphicsObject,
285
+ step: number,
286
+ ): GraphicsObject {
287
+ const filtered: Record<string, unknown[]> = {}
288
+
289
+ for (const key of Object.keys(graphics)) {
290
+ const value = (graphics as Record<string, unknown>)[key]
291
+ filtered[key] = Array.isArray(value)
292
+ ? value.filter((object) => {
293
+ return (
294
+ typeof object === "object" &&
295
+ object !== null &&
296
+ (object as { step?: number }).step === step
297
+ )
298
+ })
299
+ : []
300
+ }
301
+
302
+ return filtered as GraphicsObject
303
+ }
304
+
305
+ private getGraphicsObjectArrays(
306
+ graphics: GraphicsObject,
307
+ ): Array<Array<{ step?: number }>> {
308
+ return Object.values(graphics).filter(Array.isArray) as Array<
309
+ Array<{ step?: number }>
310
+ >
311
+ }
312
+
313
+ private getStageDef(stageNumber: number): PipelineStepLike | undefined {
314
+ return this.pipelineSolver.pipelineDef[stageNumber]
315
+ }
316
+
317
+ private getStageSolver(stageName: string): VisualizingSolver | undefined {
318
+ return (
319
+ this.pipelineSolver as unknown as Record<string, VisualizingSolver>
320
+ )[stageName]
321
+ }
322
+
323
+ private getSafeStageName(stageName: string): string {
324
+ return stageName.replace(/[^a-zA-Z0-9._-]/g, "_")
325
+ }
326
+
327
+ private getStageEnterLogLine(stageName: string, stageNumber: number) {
328
+ return `enter stage=${stageNumber + 1} name=${stageName}`
329
+ }
330
+
331
+ private async log(line: string) {
332
+ await writeFile(this.logsPath, `${line}\n`, { flag: "a" })
333
+ this.onLog?.(line)
334
+ }
335
+
336
+ private toDisplayPath(filePath: string): string {
337
+ return path.relative(process.cwd(), filePath) || "."
338
+ }
339
+
340
+ private formatError(error: unknown) {
341
+ if (error instanceof Error) return `${error.name}: ${error.message}`
342
+ return String(error)
343
+ }
344
+ }
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@tscircuit/schematic-trace-solver",
3
3
  "main": "dist/index.js",
4
- "version": "0.0.86",
4
+ "version": "0.0.88",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "start": "cosmos",
8
+ "debug:pipeline": "bun scripts/debug-pipeline-stages.ts",
8
9
  "build": "tsup-node lib/index.ts --format esm --dts",
9
10
  "format": "biome format --write .",
10
11
  "format:check": "biome format ."
@@ -18,7 +19,7 @@
18
19
  "calculate-elbow": "^0.0.12",
19
20
  "connectivity-map": "^1.0.0",
20
21
  "flatbush": "^4.5.0",
21
- "graphics-debug": "^0.0.62",
22
+ "graphics-debug": "^0.0.96",
22
23
  "react": "^19.1.1",
23
24
  "react-cosmos": "^7.0.0",
24
25
  "react-cosmos-plugin-vite": "^7.0.0",
@@ -0,0 +1,146 @@
1
+ import path from "node:path"
2
+ import { readFile } from "node:fs/promises"
3
+ import { SchematicTracePipelineSolver } from "lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver"
4
+ import { PipelineStageDebugRunner } from "lib/testing/PipelineStageDebugRunner"
5
+ import type { InputProblem } from "lib/types/InputProblem"
6
+
7
+ type Options = {
8
+ inputPath: string
9
+ outDir?: string
10
+ stopAfterStage?: string
11
+ pngSize: number
12
+ writePng: boolean
13
+ writeSvg: boolean
14
+ writeGraphicsJson: boolean
15
+ writeStepPngs: boolean
16
+ }
17
+
18
+ function printUsage() {
19
+ console.log(`Usage: bun run debug:pipeline <input.json> [options]
20
+
21
+ Options:
22
+ --out <dir> Output directory
23
+ --stop-after <stage|n> Stop after a stage name or 1-based stage number
24
+ --png-size <px> PNG width and height, default 1536
25
+ --no-png Do not write .png files
26
+ --svg Also write .svg files
27
+ --no-json Do not write .graphics.json files
28
+ --step-pngs Also write one PNG per graphics step when step values exist
29
+
30
+ Example:
31
+ bun run debug:pipeline tests/assets/example01.json
32
+ `)
33
+ }
34
+
35
+ function parseArgs(argv: string[]): Options {
36
+ const args = [...argv]
37
+ const inputPath = args.shift()
38
+ if (!inputPath || inputPath === "--help" || inputPath === "-h") {
39
+ printUsage()
40
+ process.exit(inputPath ? 0 : 1)
41
+ }
42
+
43
+ const options: Options = {
44
+ inputPath,
45
+ pngSize: 1536,
46
+ writePng: true,
47
+ writeSvg: false,
48
+ writeGraphicsJson: true,
49
+ writeStepPngs: false,
50
+ }
51
+
52
+ while (args.length > 0) {
53
+ const arg = args.shift()
54
+ if (arg === "--out") {
55
+ options.outDir = args.shift()
56
+ if (!options.outDir) throw new Error("--out requires a directory")
57
+ continue
58
+ }
59
+ if (arg === "--stop-after") {
60
+ options.stopAfterStage = args.shift()
61
+ if (!options.stopAfterStage) {
62
+ throw new Error("--stop-after requires a stage name or number")
63
+ }
64
+ continue
65
+ }
66
+ if (arg === "--png-size") {
67
+ const pngSize = Number(args.shift())
68
+ if (!Number.isFinite(pngSize) || pngSize <= 0) {
69
+ throw new Error("--png-size requires a positive number")
70
+ }
71
+ options.pngSize = pngSize
72
+ continue
73
+ }
74
+ if (arg === "--no-png") {
75
+ options.writePng = false
76
+ continue
77
+ }
78
+ if (arg === "--svg") {
79
+ options.writeSvg = true
80
+ continue
81
+ }
82
+ if (arg === "--no-json") {
83
+ options.writeGraphicsJson = false
84
+ continue
85
+ }
86
+ if (arg === "--step-pngs") {
87
+ options.writeStepPngs = true
88
+ continue
89
+ }
90
+ throw new Error(`Unknown argument: ${arg}`)
91
+ }
92
+
93
+ return options
94
+ }
95
+
96
+ function getDefaultOutputDir(inputPath: string) {
97
+ const basename = path.basename(inputPath).replace(/\.json$/i, "")
98
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
99
+ return path.resolve(
100
+ process.cwd(),
101
+ "tmp",
102
+ "schematic-trace-debug",
103
+ `${basename}-${timestamp}`,
104
+ )
105
+ }
106
+
107
+ const options = parseArgs(process.argv.slice(2))
108
+ const inputPath = path.resolve(process.cwd(), options.inputPath)
109
+ const inputProblem = JSON.parse(
110
+ await readFile(inputPath, "utf8"),
111
+ ) as InputProblem
112
+ const outputDir = options.outDir
113
+ ? path.resolve(process.cwd(), options.outDir)
114
+ : getDefaultOutputDir(inputPath)
115
+ const solver = new SchematicTracePipelineSolver(inputProblem)
116
+
117
+ const runner = new PipelineStageDebugRunner({
118
+ pipelineSolver: solver,
119
+ outputDir,
120
+ writeGraphicsJson: options.writeGraphicsJson,
121
+ writePng: options.writePng,
122
+ writeSvg: options.writeSvg,
123
+ pngWidth: options.pngSize,
124
+ pngHeight: options.pngSize,
125
+ writeStepPngs: options.writeStepPngs,
126
+ stopAfterStage: options.stopAfterStage,
127
+ context: {
128
+ inputPath: path.relative(process.cwd(), inputPath),
129
+ stopAfterStage: options.stopAfterStage ?? "",
130
+ writePng: options.writePng,
131
+ writeSvg: options.writeSvg,
132
+ writeGraphicsJson: options.writeGraphicsJson,
133
+ writeStepPngs: options.writeStepPngs,
134
+ pngSize: options.pngSize,
135
+ },
136
+ onLog: (line) => console.log(line),
137
+ })
138
+
139
+ const result = await runner.run()
140
+
141
+ console.log(`\nWrote ${result.stageArtifacts.length} stage artifact(s).`)
142
+ console.log(`Output: ${path.relative(process.cwd(), result.outputDir)}`)
143
+
144
+ if (result.error) {
145
+ process.exitCode = 1
146
+ }
@@ -0,0 +1,4 @@
1
+ import { PipelineDebugger } from "site/components/PipelineDebugger"
2
+ import inputProblem from "../../tests/bug-reports/bug-report-20260706T220324Z/bug-report-20260706T220324Z.json"
3
+
4
+ export default () => <PipelineDebugger inputProblem={inputProblem as any} />
@@ -0,0 +1,4 @@
1
+ import { PipelineDebugger } from "site/components/PipelineDebugger"
2
+ import inputProblem from "../../tests/bug-reports/bug-report-20260707T020342Z/bug-report-20260707T020342Z.json"
3
+
4
+ export default () => <PipelineDebugger inputProblem={inputProblem as any} />
@@ -0,0 +1,4 @@
1
+ import { PipelineDebugger } from "site/components/PipelineDebugger"
2
+ import inputProblem from "../../tests/bug-reports/bug-report-20260707T092615Z/bug-report-20260707T092615Z.json"
3
+
4
+ export default () => <PipelineDebugger inputProblem={inputProblem as any} />
@@ -0,0 +1,4 @@
1
+ import { PipelineDebugger } from "site/components/PipelineDebugger"
2
+ import inputProblem from "../../tests/bug-reports/bug-report-20260707T134549Z/bug-report-20260707T134549Z.json"
3
+
4
+ export default () => <PipelineDebugger inputProblem={inputProblem as any} />
@@ -0,0 +1,4 @@
1
+ import { PipelineDebugger } from "site/components/PipelineDebugger"
2
+ import inputProblem from "../../tests/bug-reports/bug-report-20260707T134722Z/bug-report-20260707T134722Z.json"
3
+
4
+ export default () => <PipelineDebugger inputProblem={inputProblem as any} />
@@ -0,0 +1,4 @@
1
+ import { PipelineDebugger } from "site/components/PipelineDebugger"
2
+ import inputProblem from "../../tests/bug-reports/bug-report-20260707T140410Z/bug-report-20260707T140410Z.json"
3
+
4
+ export default () => <PipelineDebugger inputProblem={inputProblem as any} />
@@ -0,0 +1,4 @@
1
+ import { PipelineDebugger } from "site/components/PipelineDebugger"
2
+ import inputProblem from "../../tests/bug-reports/bug-report-20260707T141025Z/bug-report-20260707T141025Z.json"
3
+
4
+ export default () => <PipelineDebugger inputProblem={inputProblem as any} />
@@ -0,0 +1,4 @@
1
+ import { PipelineDebugger } from "site/components/PipelineDebugger"
2
+ import inputProblem from "../../tests/bug-reports/bug-report-20260707T141421Z/bug-report-20260707T141421Z.json"
3
+
4
+ export default () => <PipelineDebugger inputProblem={inputProblem as any} />