@tscircuit/schematic-trace-solver 0.0.87 → 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 (113) 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/lib/testing/PipelineStageDebugRunner.ts +344 -0
  6. package/package.json +3 -2
  7. package/scripts/debug-pipeline-stages.ts +146 -0
  8. package/site/bug-reports/bug-report-20260706T220324Z.page.tsx +4 -0
  9. package/site/bug-reports/bug-report-20260707T020342Z.page.tsx +4 -0
  10. package/site/bug-reports/bug-report-20260707T092615Z.page.tsx +4 -0
  11. package/site/bug-reports/bug-report-20260707T134549Z.page.tsx +4 -0
  12. package/site/bug-reports/bug-report-20260707T134722Z.page.tsx +4 -0
  13. package/site/bug-reports/bug-report-20260707T140410Z.page.tsx +4 -0
  14. package/site/bug-reports/bug-report-20260707T141025Z.page.tsx +4 -0
  15. package/site/bug-reports/bug-report-20260707T141421Z.page.tsx +4 -0
  16. package/tests/bug-reports/bug-report-20260706T220324Z/__snapshots__/bug-report-20260706T220324Z.snap.svg +357 -3448
  17. package/tests/bug-reports/bug-report-20260707T020342Z/__snapshots__/bug-report-20260707T020342Z.snap.svg +64 -0
  18. package/tests/bug-reports/bug-report-20260707T020342Z/bug-report-20260707T020342Z.json +233 -0
  19. package/tests/bug-reports/bug-report-20260707T020342Z/bug-report-20260707T020342Z.test.ts +12 -0
  20. package/tests/bug-reports/bug-report-20260707T092615Z/__snapshots__/bug-report-20260707T092615Z.snap.svg +2098 -0
  21. package/tests/bug-reports/bug-report-20260707T092615Z/bug-report-20260707T092615Z.json +1392 -0
  22. package/tests/bug-reports/bug-report-20260707T092615Z/bug-report-20260707T092615Z.test.ts +12 -0
  23. package/tests/bug-reports/bug-report-20260707T134549Z/__snapshots__/bug-report-20260707T134549Z.snap.svg +857 -0
  24. package/tests/bug-reports/bug-report-20260707T134549Z/bug-report-20260707T134549Z.json +652 -0
  25. package/tests/bug-reports/bug-report-20260707T134549Z/bug-report-20260707T134549Z.test.ts +12 -0
  26. package/tests/bug-reports/bug-report-20260707T134722Z/__snapshots__/bug-report-20260707T134722Z.snap.svg +275 -0
  27. package/tests/bug-reports/bug-report-20260707T134722Z/bug-report-20260707T134722Z.json +265 -0
  28. package/tests/bug-reports/bug-report-20260707T134722Z/bug-report-20260707T134722Z.test.ts +12 -0
  29. package/tests/bug-reports/bug-report-20260707T140410Z/__snapshots__/bug-report-20260707T140410Z.snap.svg +950 -0
  30. package/tests/bug-reports/bug-report-20260707T140410Z/bug-report-20260707T140410Z.json +610 -0
  31. package/tests/bug-reports/bug-report-20260707T140410Z/bug-report-20260707T140410Z.test.ts +12 -0
  32. package/tests/bug-reports/bug-report-20260707T141025Z/__snapshots__/bug-report-20260707T141025Z.snap.svg +218 -0
  33. package/tests/bug-reports/bug-report-20260707T141025Z/bug-report-20260707T141025Z.json +185 -0
  34. package/tests/bug-reports/bug-report-20260707T141025Z/bug-report-20260707T141025Z.test.ts +12 -0
  35. package/tests/bug-reports/bug-report-20260707T141421Z/__snapshots__/bug-report-20260707T141421Z.snap.svg +613 -0
  36. package/tests/bug-reports/bug-report-20260707T141421Z/bug-report-20260707T141421Z.json +499 -0
  37. package/tests/bug-reports/bug-report-20260707T141421Z/bug-report-20260707T141421Z.test.ts +12 -0
  38. package/tests/examples/__snapshots__/example01.snap.svg +59 -162
  39. package/tests/examples/__snapshots__/example02.snap.svg +64 -273
  40. package/tests/examples/__snapshots__/example03.snap.svg +85 -290
  41. package/tests/examples/__snapshots__/example04.snap.svg +50 -94
  42. package/tests/examples/__snapshots__/example05.snap.svg +73 -191
  43. package/tests/examples/__snapshots__/example06.snap.svg +49 -95
  44. package/tests/examples/__snapshots__/example07.snap.svg +65 -218
  45. package/tests/examples/__snapshots__/example08.snap.svg +61 -143
  46. package/tests/examples/__snapshots__/example09.snap.svg +123 -493
  47. package/tests/examples/__snapshots__/example10.snap.svg +57 -136
  48. package/tests/examples/__snapshots__/example11.snap.svg +64 -173
  49. package/tests/examples/__snapshots__/example12.snap.svg +58 -159
  50. package/tests/examples/__snapshots__/example13.snap.svg +78 -348
  51. package/tests/examples/__snapshots__/example14.snap.svg +77 -293
  52. package/tests/examples/__snapshots__/example15.snap.svg +140 -984
  53. package/tests/examples/__snapshots__/example16.snap.svg +62 -162
  54. package/tests/examples/__snapshots__/example17.snap.svg +67 -198
  55. package/tests/examples/__snapshots__/example18.snap.svg +69 -249
  56. package/tests/examples/__snapshots__/example19.snap.svg +63 -202
  57. package/tests/examples/__snapshots__/example20.snap.svg +60 -148
  58. package/tests/examples/__snapshots__/example21.snap.svg +73 -298
  59. package/tests/examples/__snapshots__/example22.snap.svg +62 -156
  60. package/tests/examples/__snapshots__/example23.snap.svg +66 -172
  61. package/tests/examples/__snapshots__/example24.snap.svg +64 -164
  62. package/tests/examples/__snapshots__/example25.snap.svg +76 -299
  63. package/tests/examples/__snapshots__/example26.snap.svg +64 -164
  64. package/tests/examples/__snapshots__/example27.snap.svg +83 -300
  65. package/tests/examples/__snapshots__/example28.snap.svg +54 -115
  66. package/tests/examples/__snapshots__/example29.snap.svg +257 -1299
  67. package/tests/examples/__snapshots__/example30.snap.svg +91 -293
  68. package/tests/examples/__snapshots__/example31.snap.svg +47 -88
  69. package/tests/examples/__snapshots__/example32.snap.svg +107 -405
  70. package/tests/examples/__snapshots__/example33.snap.svg +71 -180
  71. package/tests/examples/__snapshots__/example34.snap.svg +55 -116
  72. package/tests/examples/__snapshots__/example35.snap.svg +69 -176
  73. package/tests/examples/__snapshots__/example36.snap.svg +65 -159
  74. package/tests/examples/__snapshots__/example37.snap.svg +75 -223
  75. package/tests/examples/__snapshots__/example38.snap.svg +46 -76
  76. package/tests/examples/__snapshots__/example39.snap.svg +51 -106
  77. package/tests/examples/__snapshots__/example40.snap.svg +83 -270
  78. package/tests/examples/__snapshots__/example41.snap.svg +54 -118
  79. package/tests/examples/__snapshots__/example42.snap.svg +67 -194
  80. package/tests/examples/__snapshots__/example43.snap.svg +62 -168
  81. package/tests/examples/__snapshots__/example44.snap.svg +86 -369
  82. package/tests/examples/__snapshots__/example45.snap.svg +60 -169
  83. package/tests/examples/__snapshots__/example46.snap.svg +80 -342
  84. package/tests/examples/__snapshots__/example47.snap.svg +82 -323
  85. package/tests/examples/__snapshots__/example48.snap.svg +64 -167
  86. package/tests/examples/__snapshots__/example49.snap.svg +120 -565
  87. package/tests/examples/__snapshots__/example50.snap.svg +71 -189
  88. package/tests/fixtures/getLastStepGraphicsObject.ts +38 -0
  89. package/tests/fixtures/getLastStepSvg.ts +9 -0
  90. package/tests/repros/__snapshots__/bugreport-001-gnd-overlap.snap.svg +67 -218
  91. package/tests/repros/__snapshots__/manufacturePartNumber-text-box.snap.svg +47 -87
  92. package/tests/repros/__snapshots__/netlabel-connector-through-rail-label.snap.svg +73 -272
  93. package/tests/repros/__snapshots__/repro-atmega328p-fault-pullup.snap.svg +284 -0
  94. package/tests/repros/__snapshots__/repro-atmega328p-missing-gnd-netlabel.snap.svg +470 -0
  95. package/tests/repros/__snapshots__/repro-bq24074-battery-charger.snap.svg +94 -395
  96. package/tests/repros/__snapshots__/repro-cc2340r5.snap.svg +88 -467
  97. package/tests/repros/__snapshots__/repro-ina237-current-monitor.snap.svg +109 -569
  98. package/tests/repros/__snapshots__/repro130-bq27441-fuel-gauge-trace-through-c1.snap.svg +95 -459
  99. package/tests/repros/__snapshots__/repro51-overlap-junction-crossing.snap.svg +64 -176
  100. package/tests/repros/__snapshots__/trace-overlap-box-resistor.snap.svg +51 -112
  101. package/tests/repros/assets/repro-atmega328p-fault-pullup.input.json +239 -0
  102. package/tests/repros/assets/repro-atmega328p-missing-gnd-netlabel.input.json +432 -0
  103. package/tests/repros/repro-atmega328p-fault-pullup.test.ts +11 -0
  104. package/tests/repros/repro-atmega328p-missing-gnd-netlabel.test.ts +16 -0
  105. package/tests/solvers/SchematicTraceSingleLineSolver2/__snapshots__/SchematicTraceSingleLineSolver2_01-example17-d1_1-u1_1.snap.svg +59 -159
  106. package/tests/solvers/TraceCleanupSolver/__snapshots__/TraceCleanupSolver.snap.svg +54 -118
  107. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/__snapshots__/TraceLabelOverlapAvoidanceSolver.snap.svg +58 -245
  108. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/renderComparisonView/__snapshots__/renderComparisonView01.snap.svg +73 -222
  109. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/renderComparisonView/__snapshots__/renderComparisonView02.snap.svg +65 -204
  110. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/renderComparisonView/__snapshots__/renderComparisonView03.snap.svg +65 -183
  111. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/__snapshots__/MergedNetLabelObstacles.snap.svg +54 -139
  112. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/__snapshots__/OverlapAvoidanceStepSolver.snap.svg +61 -215
  113. package/tests/solvers/TraceLabelOverlapAvoidanceSolver/sub-solver/__snapshots__/SingleOverlapSolver.snap.svg +54 -124
@@ -0,0 +1,115 @@
1
+ ---
2
+ name: schematic-trace-visualization
3
+ description: Work with schematic trace solver visualization in this repo. Use when changing solver `visualize()` or `preview()` output, adding debug graphics, creating SVG snapshots, inspecting pipeline stages, or generating graphics-debug artifacts for AI/headless review.
4
+ ---
5
+
6
+ # Schematic Trace Visualization
7
+
8
+ Use `GraphicsObject` from `graphics-debug` for solver debug views, SVG snapshots, and headless artifacts.
9
+
10
+ ## Graphics Object Types
11
+
12
+ Import visualization types from `graphics-debug`:
13
+
14
+ ```ts
15
+ import type {
16
+ GraphicsObject,
17
+ Point,
18
+ Line,
19
+ Rect,
20
+ Circle,
21
+ Text,
22
+ } from "graphics-debug"
23
+ ```
24
+
25
+ Do not redefine these shapes in repo docs, tests, or utilities.
26
+
27
+ ## Image Functions
28
+
29
+ This repo uses `graphics-debug` for SVG and PNG rendering:
30
+
31
+ ```ts
32
+ import {
33
+ getPngBufferFromGraphicsObject,
34
+ getSvgFromGraphicsObject,
35
+ } from "graphics-debug"
36
+ ```
37
+
38
+ Call them with a white background for stable artifacts:
39
+
40
+ ```ts
41
+ const svg = getSvgFromGraphicsObject(graphics, { backgroundColor: "white" })
42
+ const png = await getPngBufferFromGraphicsObject(graphics, {
43
+ backgroundColor: "white",
44
+ pngWidth: 1536,
45
+ pngHeight: 1536,
46
+ })
47
+ ```
48
+
49
+ ## Existing Repo Helpers
50
+
51
+ - `tests/fixtures/matcher.ts`: `expect(solver).toMatchSolverSnapshot(import.meta.path)` converts `solver.visualize()` to SVG and snapshots it.
52
+ - `tests/fixtures/getLastStepGraphicsObject.ts`: filters a `GraphicsObject` to its highest numbered `step`.
53
+ - `tests/fixtures/getLastStepSvg.ts`: converts only the last step to SVG.
54
+ - `lib/testing/PipelineStageDebugRunner.ts`: runs `SchematicTracePipelineSolver` and writes per-stage PNG/SVG/graphics JSON artifacts.
55
+ - `scripts/debug-pipeline-stages.ts`: CLI wrapper around the stage debug runner.
56
+
57
+ ## Minimal SVG Tests
58
+
59
+ Prefer focused solver SVG snapshots over broad dumps:
60
+
61
+ ```ts
62
+ const solver = new SchematicTracePipelineSolver(inputProblem as any)
63
+ solver.solve()
64
+ expect(solver).toMatchSolverSnapshot(import.meta.path)
65
+ ```
66
+
67
+ For multi-step graphics where only the final stage matters:
68
+
69
+ ```ts
70
+ expect(getLastStepSvg(solver.visualize())).toMatchSvgSnapshot(import.meta.path)
71
+ ```
72
+
73
+ Update snapshots with:
74
+
75
+ ```bash
76
+ BUN_UPDATE_SNAPSHOTS=1 bun test
77
+ ```
78
+
79
+ ## Headless Stage Debugging
80
+
81
+ To inspect a pipeline run without Cosmos:
82
+
83
+ ```bash
84
+ bun run debug:pipeline tests/assets/example01.json
85
+ ```
86
+
87
+ Useful options:
88
+
89
+ ```bash
90
+ bun run debug:pipeline tests/assets/example17.json --out tmp/schematic-trace-debug/example17
91
+ bun run debug:pipeline tests/assets/example17.json --stop-after schematicTraceLinesSolver
92
+ bun run debug:pipeline tests/assets/example17.json --no-json
93
+ bun run debug:pipeline tests/assets/example17.json --svg --step-pngs
94
+ ```
95
+
96
+ The runner writes:
97
+
98
+ - `logs.txt`
99
+ - `stageNN-stageName.png`
100
+ - `stageNN-stageName.graphics.json` unless `--no-json` is passed
101
+
102
+ Pass `--svg` when SVG companion artifacts are useful.
103
+ Pass `--step-pngs` to split graphics with `step` values into per-step PNGs.
104
+
105
+ Stage numbers are important because this pipeline can reuse the same solver name more than once, for example `netLabelPlacementSolver`.
106
+
107
+ ## Choosing The View
108
+
109
+ Use solver-native `visualize()` for internal state, candidates, failed geometry, staged progress, net labels, and trace cleanup state.
110
+
111
+ Use `SchematicTracePipelineSolver.visualize()` for final combined output after `solve()`.
112
+
113
+ Use `visualizeInputProblem(inputProblem)` when the question is about the raw input geometry, schematic components, pins, direct connections, or net connection labels before routing.
114
+
115
+ Keep SVG backgrounds white. Treat colors, labels, line widths, opacity, and `step` values as snapshot-sensitive.
@@ -48,15 +48,19 @@ async function main() {
48
48
  const reportId = `bug-report-${timestamp}`
49
49
  const branchName = `json-bug-report/${reportId}`
50
50
  const reportDir = path.join("tests", "bug-reports", reportId)
51
+ const siteReportDir = path.join("site", "bug-reports")
51
52
  const reportDirAbs = path.join(repoRoot, reportDir)
52
53
  const jsonPath = path.join(reportDir, `${reportId}.json`)
53
54
  const testPath = path.join(reportDir, `${reportId}.test.ts`)
55
+ const pagePath = path.join(siteReportDir, `${reportId}.page.tsx`)
54
56
 
55
57
  writeOutput("report_id", reportId)
56
58
  writeOutput("branch_name", branchName)
57
59
  writeOutput("report_dir", reportDir)
60
+ writeOutput("site_report_dir", siteReportDir)
58
61
  writeOutput("json_path", jsonPath)
59
62
  writeOutput("test_path", testPath)
63
+ writeOutput("page_path", pagePath)
60
64
 
61
65
  if (fs.existsSync(reportDirAbs)) {
62
66
  writeOutput("created", "false")
@@ -70,15 +74,17 @@ async function main() {
70
74
  assertInputProblem(jsonAttachment.value)
71
75
 
72
76
  fs.mkdirSync(reportDirAbs, { recursive: true })
77
+ fs.mkdirSync(path.join(repoRoot, siteReportDir), { recursive: true })
73
78
  fs.writeFileSync(
74
79
  path.join(repoRoot, jsonPath),
75
80
  `${JSON.stringify(jsonAttachment.value, null, 2)}\n`,
76
81
  )
77
82
  fs.writeFileSync(path.join(repoRoot, testPath), getTestFile(reportId))
83
+ fs.writeFileSync(path.join(repoRoot, pagePath), getPageFile(reportId))
78
84
 
79
85
  writeOutput("created", "true")
80
86
  writeOutput("attachment_url", jsonAttachment.url)
81
- console.log(`Generated ${jsonPath} and ${testPath}`)
87
+ console.log(`Generated ${jsonPath}, ${testPath}, and ${pagePath}`)
82
88
  }
83
89
 
84
90
  function isJsonBugReportIssue(issue: IssueEvent["issue"], body: string) {
@@ -458,6 +464,14 @@ test("${reportId}", () => {
458
464
  `
459
465
  }
460
466
 
467
+ function getPageFile(reportId: string) {
468
+ return `import { PipelineDebugger } from "site/components/PipelineDebugger"
469
+ import inputProblem from "../../tests/bug-reports/${reportId}/${reportId}.json"
470
+
471
+ export default () => <PipelineDebugger inputProblem={inputProblem as any} />
472
+ `
473
+ }
474
+
461
475
  function writeOutput(key: string, value: string) {
462
476
  if (!githubOutputPath) return
463
477
  fs.appendFileSync(githubOutputPath, `${key}=${value}\n`)
@@ -64,7 +64,7 @@ jobs:
64
64
 
65
65
  - name: Format generated test
66
66
  if: steps.import.outputs.created == 'true'
67
- run: ./node_modules/.bin/biome format --write "${{ steps.import.outputs.test_path }}"
67
+ run: ./node_modules/.bin/biome format --write "${{ steps.import.outputs.test_path }}" "${{ steps.import.outputs.page_path }}"
68
68
 
69
69
  - name: Open pull request
70
70
  id: pr
@@ -82,6 +82,7 @@ jobs:
82
82
  git config user.email "tscircuit-bot@users.noreply.github.com"
83
83
  git checkout -B "$BRANCH_NAME"
84
84
  git add "${{ steps.import.outputs.report_dir }}"
85
+ git add "${{ steps.import.outputs.page_path }}"
85
86
 
86
87
  if git diff --cached --quiet; then
87
88
  echo "No changes to commit."
@@ -96,7 +97,7 @@ jobs:
96
97
  {
97
98
  echo "Generated from #${ISSUE_NUMBER}."
98
99
  echo
99
- echo "Adds a snapshot-only regression test for the attached JSON solver input."
100
+ echo "Adds a snapshot-only regression test and debugger page for the attached JSON solver input."
100
101
  echo
101
102
  echo "Workflow run: ${RUN_URL}"
102
103
  } > "$pr_body_file"
package/AGENTS.md ADDED
@@ -0,0 +1,22 @@
1
+ # Agent Notes
2
+
3
+ - Use `bunx tsc --noEmit` to type check.
4
+ - Use `bun test path/to/test.ts` to run a test.
5
+ - Use `bun add <package>` to add a package.
6
+
7
+ ## Solver Visualization
8
+
9
+ For schematic trace solver visualization work, use the repo-local skill:
10
+
11
+ - `.agents/skills/schematic-trace-visualization/SKILL.md`
12
+
13
+ This repo supports headless stage artifact generation without starting Cosmos:
14
+
15
+ ```bash
16
+ bun run debug:pipeline tests/assets/example01.json
17
+ ```
18
+
19
+ The command writes per-stage PNG and graphics JSON artifacts under `tmp/schematic-trace-debug/...`.
20
+ Pass `--svg` when SVG companion files are needed.
21
+
22
+ Interactive debugging still exists through `site/**/*.page.tsx`, but agents should prefer the headless debug runner when they need to inspect solver state programmatically.
@@ -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.87",
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",