From 2a1f60b97366345ca5c76667619a062b45efc0f0 Mon Sep 17 00:00:00 2001 From: ankan roy Date: Thu, 6 Aug 2026 12:26:00 +0530 Subject: [PATCH 1/2] Add source diagnostic checks --- bun.lock | 3 +- cli/build/drc-diagnostic-filter.ts | 5 +- cli/build/register.ts | 1 + cli/check/register.ts | 26 ++++++++++ cli/check/source/register.ts | 48 +++++++++++++++++++ cli/main.ts | 2 + lib/shared/circuit-json-diagnostics.ts | 26 ++++++++-- package.json | 1 + ...rcuit-json-warning-type-precedence.test.ts | 16 +++++++ tests/cli/check/check-all-diagnostics.test.ts | 40 ++++++++++++++++ tests/cli/check/check-source.test.ts | 42 ++++++++++++++++ 11 files changed, 204 insertions(+), 6 deletions(-) create mode 100644 cli/check/source/register.ts create mode 100644 tests/analyze-circuit-json-warning-type-precedence.test.ts create mode 100644 tests/cli/check/check-all-diagnostics.test.ts create mode 100644 tests/cli/check/check-source.test.ts diff --git a/bun.lock b/bun.lock index 0657d0ffc..4460d97cb 100644 --- a/bun.lock +++ b/bun.lock @@ -11,6 +11,7 @@ "@tscircuit/circuit-json-placement-analysis": "^0.0.6", "@tscircuit/circuit-json-routing-analysis": "^0.0.6", "@tscircuit/circuit-json-schematic-placement-analysis": "github:tscircuit/circuit-json-schematic-placement-analysis#bb0b41d80c9714695b498e182c2a558f1e0ceb2d", + "@tscircuit/circuit-json-util": "^0.0.104", "@tscircuit/eval": "^0.0.1016", "@tscircuit/fake-snippets": "^0.0.182", "@tscircuit/file-server": "^0.0.32", @@ -329,7 +330,7 @@ "@tscircuit/circuit-json-schematic-placement-analysis": ["@tscircuit/circuit-json-schematic-placement-analysis@github:tscircuit/circuit-json-schematic-placement-analysis#bb0b41d", { "dependencies": { "@tscircuit/circuit-json-util": "^0.0.94" }, "peerDependencies": { "circuit-json": "*", "typescript": "^5" } }, "tscircuit-circuit-json-schematic-placement-analysis-bb0b41d", "sha512-L8P1Qs4rs9tBWosUJEFvNKSKwBKHHIArmJh+aDCGz5m9g5dP+STyadkTOb9BJRXuWXm1ndBD05VMfHMwb9F9JQ=="], - "@tscircuit/circuit-json-util": ["@tscircuit/circuit-json-util@0.0.97", "", { "dependencies": { "parsel-js": "^1.1.2" }, "peerDependencies": { "circuit-json": "*", "transformation-matrix": "*", "zod": "3" } }, "sha512-qg0R/X4mCwb43f53+W3FkxUNRvgGz1WNjpQfKi+QXnoqISw6EZ1Y7VcSynO8S2d8hPGV9Mk5DUOjTPjlEUrrGA=="], + "@tscircuit/circuit-json-util": ["@tscircuit/circuit-json-util@0.0.104", "", { "dependencies": { "parsel-js": "^1.1.2" }, "peerDependencies": { "circuit-json": "*", "transformation-matrix": "*", "zod": "3" } }, "sha512-mJM4s29CHZLGpQvt49PaDk6l7QWv2xZUXYNRby1sqrEoMvEQAs4kqa5cVPRRwtfMkb7K9VKX03HAFBFaHZPmhA=="], "@tscircuit/copper-pour-solver": ["@tscircuit/copper-pour-solver@0.0.39", "", { "dependencies": { "@tscircuit/manifold-2d": "^0.0.6" }, "peerDependencies": { "typescript": "^5" } }, "sha512-Z8+3UrK919QbwJkGyHsb8DGuHqE66iCylpbF/v132PuvrRbHo+phmOfP9nEQjI63ldh976EsT26qw535H15M1g=="], diff --git a/cli/build/drc-diagnostic-filter.ts b/cli/build/drc-diagnostic-filter.ts index 782661a62..98e91d44d 100644 --- a/cli/build/drc-diagnostic-filter.ts +++ b/cli/build/drc-diagnostic-filter.ts @@ -18,6 +18,7 @@ const EMPTY_IGNORE_COUNTS = (): DrcIgnoreCounts => ({ pin_specification: 0, placement: 0, routing: 0, + source: 0, unknown: 0, }) @@ -25,7 +26,8 @@ const normalizeCategory = (category: string): DrcCategory => category === "netlist" || category === "pin_specification" || category === "placement" || - category === "routing" + category === "routing" || + category === "source" ? category : "unknown" @@ -112,6 +114,7 @@ export const formatIgnoredDrcCounts = (counts: DrcIgnoreCounts): string => ["pin_specification", counts.pin_specification], ["placement", counts.placement], ["routing", counts.routing], + ["source", counts.source], ["unknown", counts.unknown], ] as const ) diff --git a/cli/build/register.ts b/cli/build/register.ts index d23487a17..fada6357a 100644 --- a/cli/build/register.ts +++ b/cli/build/register.ts @@ -405,6 +405,7 @@ export const registerBuild = (program: Command) => { pin_specification: 0, placement: 0, routing: 0, + source: 0, unknown: 0, } const staticFileReferences: StaticBuildFileReference[] = [] diff --git a/cli/check/register.ts b/cli/check/register.ts index 0a0716686..97957713e 100644 --- a/cli/check/register.ts +++ b/cli/check/register.ts @@ -1,7 +1,33 @@ +import type { AnyCircuitElement } from "circuit-json" import type { Command } from "commander" +import { + analyzeCircuitJson, + formatCircuitJsonDiagnostics, +} from "lib/shared/circuit-json-diagnostics" +import { getCircuitJsonForCheck, resolveCheckInputFilePath } from "./shared" + +export const check = async (file?: string) => { + const resolvedInputFilePath = await resolveCheckInputFilePath(file) + const circuitJson = (await getCircuitJsonForCheck({ + filePath: resolvedInputFilePath, + platformConfig: {}, + allowPrebuiltCircuitJson: true, + })) as AnyCircuitElement[] + + return formatCircuitJsonDiagnostics(analyzeCircuitJson(circuitJson)) +} export const registerCheck = (program: Command) => { program .command("check") .description("Partially build and validate circuit artifacts") + .argument("[file]", "Path to the entry file") + .action(async (file?: string) => { + try { + console.log(await check(file)) + } catch (error) { + console.error(error instanceof Error ? error.message : String(error)) + process.exit(1) + } + }) } diff --git a/cli/check/source/register.ts b/cli/check/source/register.ts new file mode 100644 index 000000000..67ad43bb3 --- /dev/null +++ b/cli/check/source/register.ts @@ -0,0 +1,48 @@ +import { categorizeErrorOrWarning } from "@tscircuit/circuit-json-util" +import type { PlatformConfig } from "@tscircuit/props" +import type { AnyCircuitElement } from "circuit-json" +import type { Command } from "commander" +import { + type CircuitJsonIssue, + analyzeCircuitJson, + formatCircuitJsonDiagnostics, +} from "lib/shared/circuit-json-diagnostics" +import { getCircuitJsonForCheck, resolveCheckInputFilePath } from "../shared" + +export const isSourceDiagnostic = (issue: CircuitJsonIssue) => + categorizeErrorOrWarning(issue) === "source" + +export const checkSource = async (file?: string) => { + const resolvedInputFilePath = await resolveCheckInputFilePath(file) + const circuitJson = (await getCircuitJsonForCheck({ + filePath: resolvedInputFilePath, + platformConfig: { + pcbDisabled: true, + routingDisabled: true, + placementDrcChecksDisabled: true, + } satisfies PlatformConfig, + allowPrebuiltCircuitJson: true, + })) as AnyCircuitElement[] + const diagnostics = analyzeCircuitJson(circuitJson) + + return formatCircuitJsonDiagnostics({ + errors: diagnostics.errors.filter(isSourceDiagnostic), + warnings: diagnostics.warnings.filter(isSourceDiagnostic), + }) +} + +export const registerCheckSource = (program: Command) => { + program.commands + .find((command) => command.name() === "check")! + .command("source") + .description("Partially build and validate source diagnostics") + .argument("[file]", "Path to the entry file") + .action(async (file?: string) => { + try { + console.log(await checkSource(file)) + } catch (error) { + console.error(error instanceof Error ? error.message : String(error)) + process.exit(1) + } + }) +} diff --git a/cli/main.ts b/cli/main.ts index 4b70050ad..d2985c7b5 100644 --- a/cli/main.ts +++ b/cli/main.ts @@ -20,6 +20,7 @@ import { registerCheck } from "./check/register" import { registerCheckRoutingDifficulty } from "./check/routing-difficulty/register" import { registerCheckSchematicPlacement } from "./check/schematic-placement/register" import { registerCheckShorts } from "./check/shorts/register" +import { registerCheckSource } from "./check/source/register" import { registerCheckTraceLength } from "./check/trace-length/register" import { registerClone } from "./clone/register" import { registerConfigPrint } from "./config/print/register" @@ -89,6 +90,7 @@ registerCheckPlacement(program) registerCheckRoutingDifficulty(program) registerCheckSchematicPlacement(program) registerCheckShorts(program) +registerCheckSource(program) registerCheckTraceLength(program) registerRegistry(program) diff --git a/lib/shared/circuit-json-diagnostics.ts b/lib/shared/circuit-json-diagnostics.ts index f41e211b6..6e4a407fe 100644 --- a/lib/shared/circuit-json-diagnostics.ts +++ b/lib/shared/circuit-json-diagnostics.ts @@ -21,15 +21,33 @@ export function analyzeCircuitJson(circuitJson: any[]): { const isTypedError = typeof t === "string" && t.endsWith("_error") const isTypedWarning = typeof t === "string" && t.endsWith("_warning") - if (hasErrorType || isTypedError) { - errors.push(item as CircuitJsonIssue) + if (hasWarningType || isTypedWarning) { + warnings.push(item as CircuitJsonIssue) continue } - if (hasWarningType || isTypedWarning) { - warnings.push(item as CircuitJsonIssue) + if (hasErrorType || isTypedError) { + errors.push(item as CircuitJsonIssue) } } return { errors, warnings } } + +export function formatCircuitJsonDiagnostics({ + errors, + warnings, +}: { + errors: CircuitJsonIssue[] + warnings: CircuitJsonIssue[] +}): string { + const lines = [`Errors: ${errors.length}`, `Warnings: ${warnings.length}`] + + for (const issue of [...errors, ...warnings]) { + const issueType = + issue.warning_type ?? issue.error_type ?? issue.type ?? "unknown_issue" + lines.push(`- ${issueType}: ${issue.message ?? ""}`) + } + + return lines.join("\n") +} diff --git a/package.json b/package.json index 2aea94544..09c96332a 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@tscircuit/circuit-json-placement-analysis": "^0.0.6", "@tscircuit/circuit-json-routing-analysis": "^0.0.6", "@tscircuit/circuit-json-schematic-placement-analysis": "github:tscircuit/circuit-json-schematic-placement-analysis#bb0b41d80c9714695b498e182c2a558f1e0ceb2d", + "@tscircuit/circuit-json-util": "^0.0.104", "@tscircuit/eval": "^0.0.1016", "@tscircuit/fake-snippets": "^0.0.182", "@tscircuit/file-server": "^0.0.32", diff --git a/tests/analyze-circuit-json-warning-type-precedence.test.ts b/tests/analyze-circuit-json-warning-type-precedence.test.ts new file mode 100644 index 000000000..6c7a2be90 --- /dev/null +++ b/tests/analyze-circuit-json-warning-type-precedence.test.ts @@ -0,0 +1,16 @@ +import { expect, test } from "bun:test" +import { analyzeCircuitJson } from "lib/shared/circuit-json-diagnostics" + +test("analyzeCircuitJson prefers a warning type over error_type metadata", () => { + const { errors, warnings } = analyzeCircuitJson([ + { + type: "source_property_ignored_warning", + error_type: "source_property_ignored_warning", + property_name: "positiveConnection", + message: "ambiguous differential-pair trace", + }, + ]) + + expect(errors).toHaveLength(0) + expect(warnings).toHaveLength(1) +}) diff --git a/tests/cli/check/check-all-diagnostics.test.ts b/tests/cli/check/check-all-diagnostics.test.ts new file mode 100644 index 000000000..b2a12691a --- /dev/null +++ b/tests/cli/check/check-all-diagnostics.test.ts @@ -0,0 +1,40 @@ +import { expect, test } from "bun:test" +import { writeFile } from "node:fs/promises" +import path from "node:path" +import { getCliTestFixture } from "../../fixtures/get-cli-test-fixture" + +test("check prints diagnostics from every category including unknown", async () => { + const { tmpDir, runCommand } = await getCliTestFixture() + const circuitJsonPath = path.join(tmpDir, "all-diagnostics.circuit.json") + const diagnosticTypes = [ + "source_property_ignored_warning", + "source_pin_must_be_connected_error", + "pcb_component_outside_board_error", + "pcb_trace_error", + "source_no_power_pin_defined_warning", + "future_diagnostic_warning", + ] + + await writeFile( + circuitJsonPath, + JSON.stringify( + diagnosticTypes.map((type) => ({ + type, + message: `Diagnostic for ${type}`, + })), + ), + ) + + const { stdout, stderr, exitCode } = await runCommand( + `tsci check ${circuitJsonPath}`, + ) + + expect(exitCode).toBe(0) + expect(stderr).toBe("") + expect(stdout).toContain("Errors: 3") + expect(stdout).toContain("Warnings: 3") + + for (const type of diagnosticTypes) { + expect(stdout).toContain(type) + } +}) diff --git a/tests/cli/check/check-source.test.ts b/tests/cli/check/check-source.test.ts new file mode 100644 index 000000000..6a5336152 --- /dev/null +++ b/tests/cli/check/check-source.test.ts @@ -0,0 +1,42 @@ +import { expect, test } from "bun:test" +import { writeFile } from "node:fs/promises" +import path from "node:path" +import { getCliTestFixture } from "../../fixtures/get-cli-test-fixture" + +test("check source prints only source diagnostics", async () => { + const { tmpDir, runCommand } = await getCliTestFixture() + const circuitJsonPath = path.join(tmpDir, "source-warning.circuit.json") + + await writeFile( + circuitJsonPath, + JSON.stringify([ + { + type: "source_property_ignored_warning", + error_type: "source_property_ignored_warning", + message: "Source property was ignored", + }, + { + type: "source_pin_must_be_connected_error", + error_type: "source_pin_must_be_connected_error", + message: "Pin must be connected", + }, + { + type: "pcb_trace_error", + error_type: "pcb_trace_error", + message: "Trace failed", + }, + ]), + ) + + const { stdout, stderr, exitCode } = await runCommand( + `tsci check source ${circuitJsonPath}`, + ) + + expect(exitCode).toBe(0) + expect(stderr).toBe("") + expect(stdout).toContain("Errors: 0") + expect(stdout).toContain("Warnings: 1") + expect(stdout).toContain("source_property_ignored_warning") + expect(stdout).not.toContain("source_pin_must_be_connected_error") + expect(stdout).not.toContain("pcb_trace_error") +}) From e0e64977251699270dc6c3ce796345ab725dbbd2 Mon Sep 17 00:00:00 2001 From: ankan roy Date: Thu, 6 Aug 2026 14:50:28 +0530 Subject: [PATCH 2/2] Update circuit-json-util --- bun.lock | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bun.lock b/bun.lock index 4460d97cb..28c683273 100644 --- a/bun.lock +++ b/bun.lock @@ -11,7 +11,7 @@ "@tscircuit/circuit-json-placement-analysis": "^0.0.6", "@tscircuit/circuit-json-routing-analysis": "^0.0.6", "@tscircuit/circuit-json-schematic-placement-analysis": "github:tscircuit/circuit-json-schematic-placement-analysis#bb0b41d80c9714695b498e182c2a558f1e0ceb2d", - "@tscircuit/circuit-json-util": "^0.0.104", + "@tscircuit/circuit-json-util": "^0.0.105", "@tscircuit/eval": "^0.0.1016", "@tscircuit/fake-snippets": "^0.0.182", "@tscircuit/file-server": "^0.0.32", @@ -330,7 +330,7 @@ "@tscircuit/circuit-json-schematic-placement-analysis": ["@tscircuit/circuit-json-schematic-placement-analysis@github:tscircuit/circuit-json-schematic-placement-analysis#bb0b41d", { "dependencies": { "@tscircuit/circuit-json-util": "^0.0.94" }, "peerDependencies": { "circuit-json": "*", "typescript": "^5" } }, "tscircuit-circuit-json-schematic-placement-analysis-bb0b41d", "sha512-L8P1Qs4rs9tBWosUJEFvNKSKwBKHHIArmJh+aDCGz5m9g5dP+STyadkTOb9BJRXuWXm1ndBD05VMfHMwb9F9JQ=="], - "@tscircuit/circuit-json-util": ["@tscircuit/circuit-json-util@0.0.104", "", { "dependencies": { "parsel-js": "^1.1.2" }, "peerDependencies": { "circuit-json": "*", "transformation-matrix": "*", "zod": "3" } }, "sha512-mJM4s29CHZLGpQvt49PaDk6l7QWv2xZUXYNRby1sqrEoMvEQAs4kqa5cVPRRwtfMkb7K9VKX03HAFBFaHZPmhA=="], + "@tscircuit/circuit-json-util": ["@tscircuit/circuit-json-util@0.0.105", "", { "dependencies": { "parsel-js": "^1.1.2" }, "peerDependencies": { "circuit-json": "*", "transformation-matrix": "*", "zod": "3" } }, "sha512-zyAP7AkfARgw8Bsme6D9AmffA03swTYDKg0ypDASMMR3aGghR5MET+IswugfOuAa019gnBOV6Q1an4kaIOKdCw=="], "@tscircuit/copper-pour-solver": ["@tscircuit/copper-pour-solver@0.0.39", "", { "dependencies": { "@tscircuit/manifold-2d": "^0.0.6" }, "peerDependencies": { "typescript": "^5" } }, "sha512-Z8+3UrK919QbwJkGyHsb8DGuHqE66iCylpbF/v132PuvrRbHo+phmOfP9nEQjI63ldh976EsT26qw535H15M1g=="], diff --git a/package.json b/package.json index 09c96332a..6d40a8353 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "@tscircuit/circuit-json-placement-analysis": "^0.0.6", "@tscircuit/circuit-json-routing-analysis": "^0.0.6", "@tscircuit/circuit-json-schematic-placement-analysis": "github:tscircuit/circuit-json-schematic-placement-analysis#bb0b41d80c9714695b498e182c2a558f1e0ceb2d", - "@tscircuit/circuit-json-util": "^0.0.104", + "@tscircuit/circuit-json-util": "^0.0.105", "@tscircuit/eval": "^0.0.1016", "@tscircuit/fake-snippets": "^0.0.182", "@tscircuit/file-server": "^0.0.32",