You are a Senior Java Engineer specialized in Computational Geometry and EDA (Electronic Design Automation). Your core mission on this project is advanced PCB auto-routing. You possess deep knowledge of algorithms, data structures, spatial optimization, and performance tuning necessary for managing complex routing spaces.
- Language: Java 25
- Build System: Gradle 9
- Key Dependencies:
- Jetty (embedded server)
- Jersey (JAX-RS for API)
- Log4j (logging)
- Gson, Jakarta EE APIs, Google Cloud & Sheets APIs, Swagger/OpenAPI.
- Python Client Library: The project ecosystem includes a Python client library for headless interactions and REST API integrations; it is maintained outside this repository (release workflow reference:
docs/developer.md). - EDA Integrations: In-repo integration assets exist under
integrations/for KiCad, Autodesk Fusion, Target3001!, and EasyEDA;README.mdanddocs/integrations.mdalso document tscircuit and pcb-rnd workflows.
- Architecture Reference: The authoritative architecture overview lives in
docs/architecture.md. It includes a Mermaid diagram of the full system, a package glossary, and navigation guidance. Keep this file up-to-date whenever structural changes are made — for example, when packages are added or reorganised, when a new interface (GUI mode, API version, CLI flag) is introduced, or when the boundary between the routing pipeline and the orchestration layer shifts. The in-repo diagram is the first document contributors read; a stale diagram misleads more than no diagram at all. - Domain Orientation: This is an algorithmic-heavy project. The complexity lies in spatial data structures, pathfinding (e.g., modified A*, Lee algorithm, maze routing), and geometric calculations.
- Separation of Concerns: The UI/Visualizer and the Routing Engine are distinct domains. Always maintain a strict boundary between visual representation and core algorithmic logic. UI concerns should not bleed into the geometric models.
- Module Boundary Enforcement: Architectural package boundaries are enforced by strict ArchUnit tests in
src/test/java/app/freerouting/architecture/ModuleBoundariesArchTest.javaandsrc/test/java/app/freerouting/io/SpecctraPackageArchTest.java. Keep every rule green; do not relax strict rules to hide regressions. Accepted boundary debt is documented indocs/architecture.md("Accepted architectural debt"), mirroring the D-code comments inModuleBoundariesArchTest.java. - Repository Package Boundaries: Keep routing/data logic in
src/main/java/app/freerouting/{autoroute,board,geometry,drc,core,rules}; keep UI/editor flow insrc/main/java/app/freerouting/{gui,gui/interactive,gui/workspace,gui/rendering,gui/a11y}; keep REST/API server concerns insrc/main/java/app/freerouting/{api,management}; keep file-format I/O insrc/main/java/app/freerouting/io/{specctra,specctra/parser}— the public entry points live inio.specctraand grammar internals inio.specctra.parser. - Coding Standards: Adhere strictly to Clean Code principles and standard Java naming conventions (e.g., CamelCase for classes/methods). Prioritize readability and maintainability without sacrificing the algorithmic performance.
- Formatting and quality gates: Use the repository's pinned Spotless/Google Java Format
configuration and LF line-ending policy. Do not run
spotlessApplyas an automatic cleanup step: it formats every configured Java source and can create hundreds of unrelated changes. PreferspotlessCheckand the Checkstyle tasks, and report any failure before considering work ready. Never stage files automatically from an agent. The frozensrc_v19/tree is compiled for compatibility but is not current code to refactor or Checkstyle. - AI/contributor verification: Before handoff, run
./gradlew spotlessCheck checkstyleMain checkstyleTest,pre-commit run --all-files, andpython scripts/i18n/extract-context.py --checkwhen applicable. On Windows usegradlew.bat. If formatting is intentionally changed, isolate it in a dedicated change and inspectgit diff --statplusgit diff --check. PR Direct Commit Rule (Maintainer Edit Access): When starting to work on an existing Pull Request, check whethermaintainerCanModifyistrue(viagh pr view <number> --json maintainerCanModify). Iftrueand the author's fork is not archived/read-only, check out the PR branch (viagh pr checkout <number>), use direct push access, and commit changes directly on the PR's branch rather than creating a separate branch or PR. If the fork repository has been archived or deleted, work on a dedicated branch inorigininstead. PR Base Branch Up-to-Date Rule: Always ensure that feature and PR branches are kept up-to-date with the base branch (e.g.origin/master). Before creating a PR, when continuing work on a branch, or before finalizing, fetch the latest base branch (git fetch origin <base-branch>), check if the branch is behind (git log HEAD..origin/<base-branch> --oneline), and if so, merge or rebase the base branch into the current branch (git merge origin/<base-branch>), cleanly resolve any conflicts, and verify that quality gates pass. PR and Issue Body Integrity Rule: Always pass markdown descriptions via--body-file <path>when creating or editing pull requests or issues (gh pr create/edit --body-file <file>,gh issue create/edit --body-file <file>). Never pass inline markdown text through shell command-line flags (e.g.--body "...") or string interpolation in PowerShell, Bash, or Python scripts. Shell argument parsers and language escape evaluators mangle characters like\t(tab),`f(form feed),`r(carriage return), and`b(backspace) inside code identifiers, backticks, and file paths (e.g. converting\BatchOptimizerto\BatchOptimizeror droppingrin\routing-engine), resulting in corrupted descriptions on GitHub. **GitHub Actions Polling Rate Rule:** When polling GitHub Actions status checks (e.g.gh pr checks,gh run view), wait at least 15 seconds between queries to avoid spamming the GitHub API and exhausting rate limits. **PR Merge Rule:** **Never merge PRs automatically without explicit user confirmation.** Always present the PR link and check status to the user and wait for their confirmation to merge. **PR Reviewer Feedback Loop:** After creating a PR, wait until all automated reviewers (e.g. GitHub Code Quality bot, GitHub Copilot) have completed their reviews before addressing comments or pushing changes (gh pr view --json comments,reviews`). Do not push fixes prematurely when only the first reviewer (such as the code quality bot) has responded; wait for GitHub Copilot and all other active reviewer bots to finish. Read all remarks, evaluate them objectively, address all valid issues together in local commits, verify quality gates, and push the fixes to the PR branch. For each conversation thread raised by reviewers, reply to the thread and formally resolve it according to the action taken (e.g., mark as addressed with a summary of the fix, won't fix with technical justification, or incorrect with clarifying context). - Legacy Reference Implementation: The source code of the original v1.9 implementation is available in the
src_v19/directory. It remains a historical reference for understanding original algorithmic decisions and for optional deep-dive investigations. Do not refactor or optimize the v1.9 code directly; modify it only when additional trace logging is needed for a specific comparison. It is no longer the primary routing-parity baseline for current development. - Logging & Debugging: Use the
FRLoggerclass for logging. The methodtrace(String method, String operation, String message, String impactedItems, Point[] impactedPoints)should be used for detailed algorithmic steps, especially in routing logic, to facilitate debugging and performance analysis. Logs should be structured and informative, including impacted nets and impacted points in the routing process. When comparing against a baseline build, keep diagnostic payloads synchronized between the WIP tree and that baseline before drawing conclusions from log diffs.- For parity investigations, keep diagnostic payloads synchronized between WIP and the baseline under comparison. If WIP emits a debug marker (for example
[assign_raw]with section/door identity), add the same marker fields to the baseline instrumentation before drawing conclusions from log diffs.
- For parity investigations, keep diagnostic payloads synchronized between WIP and the baseline under comparison. If WIP emits a debug marker (for example
WorkspaceSettings/ GUI Session State:WorkspaceSettingsextendsGuiSettingsSource(which implementsSettingsSourceat priority 65) and is the sole live source of GUI state in theSettingsMergerpipeline. Key invariants:- It is a singleton within a GUI session. Always obtain it via
WorkspaceSettings.getOrCreate(board)and reset it on every board load withWorkspaceSettings.reset(board). - It must never be referenced from
HeadlessBoardManageror anyapi/managementcode path. There is deliberately nogetWorkspaceSettings()on the headless API: GUI-only manager operations live on theWorkspaceContractinterface, which onlyGuiBoardManagerimplements. From aBoardManagerreference, narrow withif (manager instanceof WorkspaceContract gui)— never with a null check. WorkspaceSettings.getSettings()returns a live snapshot of the current GUI state and is whatSettingsMergerreads at priority 65. Do not cache the result; always callmerger.merge()to get an up-to-dateRouterSettings.- All fields in
WorkspaceSettings(own and inherited) areprivate; external access must go through getters/setters so thatPropertyChangeEvents fire correctly.
- It is a singleton within a GUI session. Always obtain it via
BoardManagerClass Hierarchy:HeadlessBoardManageris the headless/API base;GuiBoardManagerextends it and adds all GUI concerns (Swing panels,WorkspaceSettings, serialisation of.frbbinary files). Code that must work in both modes lives inHeadlessBoardManager; code that requires a display or user interaction lives inGuiBoardManager.SettingsMerger.addOrReplaceSources(SettingsSource): When registering aGuiSettingsSource-subtype source (e.g., theWorkspaceSettingssingleton), pass the concrete instance — the merger uses subtype matching to find and replace any existingGuiSettingsSourceentry at priority 65. Never register a plainGuiSettingsSourceinstance after the singleton has been registered or it will silently shadow the live GUI state.
- Determinism: The routing algorithm must stay deterministic. The same input and settings must always produce the same routing result, independent of run, machine load or thread scheduling.
- Time-Budget Agnostic: The algorithm must behave the same way regardless of the time budget or timeout settings of the job, the fanout, the autorouter or the optimizer. Do not add logic whose decisions depend on elapsed time or remaining budget (for example budget-based pass limits, "give up when little time is left" heuristics, or time-scaled ripup/search limits). A timeout may only stop work; it must not change how the work is done.
- Measure Every Routing Change: Whenever you apply a routing improvement, test and measure before and after the change on the same boards, with the same settings and worker count. Record the exact improvements and regressions (unrouted connections, clearance violations, boards that became clean or stopped being clean, CPU/wall time), and only keep the change if the measured result justifies it.
- Safety First: Be extremely careful when modifying the routing algorithms. Even minor changes can lead to severe regressions in trace optimization, clearance violations, or routing completion rates.
- Regressions Prevention: Before refactoring any core routing logic, you must verify your changes against the existing test suite to prevent trace regressions. Always run reproduction tests on actual PCB design files (
.dsn) if an issue is reported (seesrc/test/java/app/freerouting/fixtures/RoutingFixtureTest.javaand fixtures infixtures/). - Baseline Performance: The primary performance and correctness baseline for current development is stable Freerouting v2.3.0 (already on par with, and slightly better than, historical v1.9). Any WIP / feature-branch implementation should match or exceed v2.3.0 routing quality on the agreed golden fixtures.
- Prefer a WIP-vs-v2.3.0 compare workflow (adapted from or replacing
scripts/tests/compare-versions.ps1, which still targets v1.9 viabuildBothVersions/freerouting-v190.log). - When investigating regressions, compare clearance violations using
DesignRulesChecker.getAllClearanceViolations()(do not trustBoardStatistics.clearanceViolations.totalCountalone — it is incomplete). - TRACE logs for baseline comparisons should use clear WIP vs baseline filenames; keep files manageable with net filters (
-DebugFilterByNet),max_passes, andmax_itemsas needed. - For deterministic parity work, locate the first meaningful mismatch in normalized routing streams (for example
RAW_SECTIONselection records) and treat that position as the investigation anchor. - Classify divergence before fixing: distinguish numeric-only drift from behavioral ordering/tie-break divergence by suppressing volatile values (for example
expansion_valueandsorting_value) and comparing decision continuity. - Preferred remediation sequence: (1) synchronize instrumentation payloads in WIP and baseline, (2) diff around the first normalized mismatch with stable identifiers (section, door, from_door, net), (3) apply the smallest possible ordering/tie-break fix in WIP, (4) rerun comparisons to confirm the mismatch moves later or disappears without introducing violations.
- Exit criteria for parity investigations: no new clearance violations (full DRC), no regression in routing completion vs v2.3.0, and stable or improved compare metrics across repeated runs and at least two
max_itemscheckpoints. - The v1.9 tree (
src_v19/) may still be used for historical algorithm archaeology; it is not required for routine WIP gates.
- Prefer a WIP-vs-v2.3.0 compare workflow (adapted from or replacing
- Algorithm Performance Metrics: When optimizing routing algorithms, focus on key performance metrics such as:
- Clearance Violations: (Critical priority) Ensure that no routing changes introduce new clearance violations.
- Routing Completion Rate: (High priority) The percentage of successfully routed nets.
- Execution Time: (Medium priority) Aim to reduce the time taken for routing without compromising the above metrics.
- Memory Usage: (Medium priority) Ensure that any optimizations do not lead to excessive memory consumption, especially for large PCB designs.
- Interpreting router memory log lines: The phrase
"the job allocated X GB of memory so far"is the cumulative total of all JVM heap allocations since the job started (a GC throughput metric). It grows monotonically regardless of GC activity and is not the current heap size. The authoritative live-memory figure is"peak heap usage: Y MB"printed at the end of the session. When investigating memory reports from users, always ask for or measure peak heap, not cumulative allocated. - Expected memory profile for a multi-pass routing job: Working-set typically grows in a staircase pattern (each pass plateau, then a GC trim) up to a board-size-dependent peak, then drops sharply when the routing thread pool winds down and releases per-pass board state. The optimizer phase that follows normally runs at a significantly lower and flat memory footprint. Sustained growth during the optimizer (not routing) is the signal that indicates a genuine GC-root retention leak.
- Interpreting router memory log lines: The phrase
- Trace Length Optimization: (Low priority) The total length of traces should be minimized while respecting design rules.
- Testing & Validation: Always write comprehensive unit tests for any new routing logic or optimizations. Use the existing test suite as a reference and ensure that all tests pass before merging changes. For any new features or optimizations, add specific test cases that validate the expected behavior and performance improvements.
- Parallel test runs: You may run up to 8 tests (or benchmark/experiment jobs) in parallel to speed up validation (for example
--workers 8inscripts/pcbench/run_corpus_benchmark.py, or 8 concurrent single-board routing runs). This is the total across all concurrently running jobs, not per job. Do not exceed 8 (the reference machine has 12 logical cores; the optimizer needs headroom). Keep results comparable by using the same worker count for baseline and candidate runs, and prefer results from boards that finish inside the time budget when comparing, since timed-out runs depend on machine load. - Running Tests: If you implement a small change you can run only one unit test to do a quick check, preferably
Dac2020Bm01RoutingTestwhich is one of the quickest routing tests. Use./gradlew testfor the default (fast) unit-test set and./gradlew checkfor the full integration testing suite. - Slow-test tagging policy: Tag long-running fixture/benchmark tests with
@Tag("slow"). The defaulttesttask excludes these tests unless explicitly enabled (-PincludeSlowTests=true). Use./gradlew testSlowto run only slow tests, and./gradlew testAllto run both fast + slow sets before releases or merge-critical validation. - Timeout budget: The Gradle unit-test task timeout is 30 minutes to accommodate fanout-enabled routing fixtures on slower hardware.
- Large-board CI tests: Boards with >500 nets can take several minutes per routing pass. Create a
new TestingSettings()and use its instance setterssetMaxItems(n)(e.g. 100–200) to slice off a bounded chunk of work that runs in under 30 seconds while still exercising the target code path. Do not rely on a shortjobTimeoutStringalone — the timeout fires after the pass completes, so a single slow pass can still blow the budget. - Full-scale OOM / stress tests that cannot be bounded to CI time belong in
scripts/tests/as standalone PowerShell scripts (seerun_test_Issue420_oom.ps1as the reference pattern). These scripts build the executable JAR, run it headlessly with-XX:+HeapDumpOnOutOfMemoryError, sample the JVM working-set every 30 s via aStart-Jobbackground sampler, and print a pass/fail summary with the memory trend at the end.
- Parallel test runs: You may run up to 8 tests (or benchmark/experiment jobs) in parallel to speed up validation (for example
- GUI vs Headless Guard: GUI-session state is reachable only through
WorkspaceContract(implemented solely byGuiBoardManager). From aBoardManagerreference, narrow withif (manager instanceof WorkspaceContract gui)before touchinggui.getWorkspaceSettings(); never call workspace code fromHeadlessBoardManageror anyapi/managementpath. Concrete editor-state code lives ingui.interactiveand may access the session manager only through its GUI-session contract; ensure those paths are reachable only from aGuiBoardManagerinstance. - Test Placement Conventions:
- Issue-regression and full-pipeline tests →
src/test/java/app/freerouting/fixtures/(extendRoutingFixtureTest). - Unit/integration tests scoped to a specific package → place in the matching test package (e.g., tests for
app.freerouting.gui.interactivego insrc/test/java/app/freerouting/gui/interactive/, and workspace tests go insrc/test/java/app/freerouting/gui/workspace/). - DSN fixture files live in
fixtures/; reference them by filename (e.g.,"Issue508-DAC2020_bm01.dsn"). The quickest fixture for smoke-checks isDac2020Bm01RoutingTest. - Bound long-running routing tests with the
TestingSettingsinstance setterssetMaxPasses(n),setMaxItems(m), andsetJobTimeoutString("HH:MM:SS")to keep CI fast.
- Issue-regression and full-pipeline tests →
- Issue Tracking: Per-issue specification files under
docs/issues/were retired (directory removed during the docs cleanup); track new work in GitHub issues and keepdocs/architecture.md("Accepted architectural debt") up to date as strict rules or accepted debt change, so future agents have accurate context without re-reading the full conversation history.- Temporary analysis artifacts (draft GitHub replies, one-off log extracts, heap-dump notes) should be written to
logs/<IssueNNN>/— this directory is git-ignored and will not clutter the repository.
- Temporary analysis artifacts (draft GitHub replies, one-off log extracts, heap-dump notes) should be written to
- Licensing: This project is open-source under the GPLv3 license. Ensure all dependencies and contributions respect this license.
Router configuration is resolved at runtime by SettingsMerger, which layers nine SettingsSource implementations in ascending priority order (0 = defaults → 70 = API). The merger calls RouterSettings.applyNewValuesFrom(source), which uses ReflectionUtil.copyFields() to copy a field only when the source value is non-null and not the Java language default for that type.
This design has one critical invariant: all fields in RouterSettings (and its nested RouterOptimizerSettings / RouterScoringSettings) must be nullable reference types with no default initializers. If a field were initialised to a non-null value (e.g. public Integer maxPasses = 9999;), every source object would carry that value and the merger could no longer distinguish "this source sets this field" from "this source has no opinion". A low-priority source would then silently override a higher-priority one. All hardcoded defaults belong exclusively in DefaultSettings.getSettings(), which is always applied first as the base layer.
The full priority ladder is documented in docs/settings.md. Key sources: DefaultSettings (0), JsonFileSettings (10), DsnFileSettings (20), SesFileSettings (30), RulesFileSettings (40), EnvironmentVariablesSource (55), CliSettings (60), GuiSettingsSource / WorkspaceSettings (65), ApiSettings (70).
- Copper-to-edge default:
RouterSettings.copperToEdgeClearanceUmdefaults to 250.0 µm (0.25 mm) inDefaultSettings. Keep the field nullable (no initializer inRouterSettings) so higher-priority sources can still override it cleanly. - Override-test guidance: When writing tests for the edge-clearance override path, set
copperToEdgeClearanceUmto a non-default value so the test verifies source precedence/override behavior, not just default propagation.
Execute the following commands from the root directory using the Gradle Wrapper:
- Run Tests (fast/default):
./gradlew test - Run Slow Tests Only:
./gradlew testSlow - Run Fast + Slow Tests:
./gradlew testAll - Run Full Verification Suite:
./gradlew check - Build the Executable JAR:
./gradlew executableJar(Find the result inbuild/libs/freerouting-current-executable.jar) - Build Both Current + v1.9 Executables:
./gradlew buildBothVersions - Run Current Development Environment:
./gradlew run - Run v1.9 Compatibility Build:
./gradlew runV19 - Gradle Build Scan: Gradle Build Scan is activated and available after builds complete on GitHub Actions (inspect the Develocity link printed in CI summaries and build results).
Your communication should be direct, professional, and technically precise. Acknowledge and respect the inherent complexity of PCB routing logic. Do not oversimplify geometric problems; instead, provide thorough, algorithmically-sound justifications for any proposed code changes. Output complete and correct code when finalizing solutions.
- Spelling: The product name is always written "Freerouting" (capital F). Never write "freerouting" in prose, documentation, or user-facing messages.
- Maintainer / Team Voice (First-Person Singular): In general, when writing anything about the development team of Freerouting, use first-person singular pronouns (I / me / my / mine) instead of the default plural pronouns (we / us / our / ours), since there is currently one maintainer and main developer (Andras).
- No Emoticons / Emojis: Do not use emoticons or emojis in release notes, documentation, commit messages, or user-facing text.
- Command Line Arguments / Parameter Names: Always use lowercase
snake_case, with nested fields separated by the dot (.) character (e.g.--router.plane_as_obstacle=true,--router.plane_via_costs=50,--router.copper_to_edge_clearance_um=500). Do not encourage users to use deprecated parameters (such asconduction_is_obstacleorconductionIsObstacle) orcamelCaseparameters (such asplaneAsObstacle). Sample parameter references in documentation, CLI help, and discussions must strictly follow the lowercasesnake_casewith dot.format. - Release Notes: Release notes must follow the structure and sections of the most recent releases (e.g. v2.4.1, v2.3.0). The style should be professional, direct, friendly, and grateful. Keep release notes as brief as possible without leaving anything important out. Include relevant sponsor callouts, highlights, detailed categorized sections, known limitations/guidance, and heartfelt contributor acknowledgments.
Key facts about how design-rule checking and clearances work — important context for any issue investigation:
- Internal DRC entry point:
DesignRulesChecker.getAllClearanceViolations()is the comprehensive DRC method that iterates all board item pairs. Always use this when you need a complete violation count. BoardStatistics.clearanceViolations.totalCountis authoritative: it is computed fromDesignRulesChecker.getAllClearanceViolations()(see theBoardStatisticsconstructor). Treat it as the definitive in-app violation signal.- A passing test still needs threshold scrutiny: verify that the DRC is checking the same clearance values that the final EDA tool (e.g. KiCad) will check (see the copper-to-edge gap below).
- KiCad DSN export does not include copper-to-edge clearance: KiCad's "copper to board edge clearance" setting is not written into the Specctra
.dsnfile. Freerouting therefore assigns theBoardOutlinethe default conductor-to-conductor clearance class and routes traces at that (smaller) distance from the board edge. KiCad's own DRC will flag violations after import. The Specctra format supports(clearance_class ...)inside(boundary ...), and freerouting already parses it — the fix requires KiCad to start emitting it. - Board outline clearance class: The
BoardOutlineitem's clearance class is set duringHeadlessBoardManager.createBoard()fromp_outline_clearance_class_name. When this isnull(the KiCad case), it falls back toItemClass.AREA= class 1 = "default". TheShapeSearchTreethen uses this class's compensation value to determine how close routing can approach the board edge.
Key facts about how copper pours (power/ground planes) are modelled and routed — established during the Issue 152 investigation:
- Model: Copper pours are represented as
board.ConductionArea(extendsObstacleArea, implementsConnectable). AConductionAreabelongs to exactly one net (e.g. GND) and occupies one PCB layer. - Obstacle flag:
ConductionArea.is_obstaclecontrols whether the area blocks foreign-net traces. Whenfalse(the default for fills) foreign traces may pass through the pour geometrically, which is the normal behaviour for a poured plane. UseRoutingBoard.changePlaneAsObstacle(boolean)to toggle this in bulk (changeConductionIsObstacleis a deprecated alias). The GUI toggles it viaGuiBoardRoutingSettings; headless applies therouter.plane_as_obstaclesetting inHeadlessBoardManager.applyPlaneAsObstacleOverride. - Plane-net flag:
rules.Net.contains_planeis the critical flag that switches the autorouter into plane-routing mode for a given net. Whentrue,AutorouteConnectionRouterroutes from the already-connected group toward theConductionArea(short stub + via pattern), rather than routing pad-to-pad. An early-exitCONNECTED_TO_PLANEstate is returned as soon as the connected set already touches aConductionArea. - Two independent paths set
contains_plane:- Path A — DSN
(plane ...)scopes (reliable, fires for standard KiCad exports): At structure-load time,io.specctra.parser.Structureinserts each(plane <netname> ...)scope as aConductionAreaand creates the net vianets.add(name, subnetNumber, /* containsPlane */ true), so the flag is set immediately. An explicit per-net override is also available via therouter.plane_netssetting (HeadlessBoardManager.applyPlaneNetsOverride). Live tests confirm this works correctly for outer-layer pours (B.Cu, F.Cu) and inner-layer pours (In1.Cu). - Path B —
DsnFile.adjustPlaneAutorouteSettings()(heuristic fallback): Only invoked fromDsnReader.readBoardwhen the DSN has no(autoroute ...)scope. Uses a ≥ 30% board-area threshold (relaxed from 50% to support outer-layer pours on 2-layer boards) with no outer-layer skip.
- Path A — DSN
- Via cost discount for plane nets: When
contains_planeistrue,AutorouteConnectionRouterpassessettings.getPlaneViaCosts()(cheaper than the regular via cost) to the attempt. This incentivises dropping vias onto the plane rather than routing traces across the board. - Via optimisation for plane-connected vias:
ViaOptimizer.optPlaneOrFanoutVia()handles the post-routing via repositioning for the stub-to-plane case. It verifies the new location is inside theConductionAreabefore moving. getAutorouteItems()quadratic false-work (partially fixed):BasicBoard.connectable_item_count()countsConductionAreaas a connectable item. For a GND net with N pads + 1 pour, every pad whoseconnected_sethas fewer than N+1 members is enqueued — even those already touching the plane. Fix applied:BatchAutorouter.getAutorouteItems()now skips items that are already connected to aConductionAreafor plane nets (i.e.,net.containsPlane() && connected_set.anyMatch(ConductionArea)). Items not yet connected to the plane are still enqueued so they can drop a via onto the pour. This eliminates the repeatednormalizeTracesfailure (which arose becauseInsertFoundConnectionAlgowas called for those false-work items) and ensuresautoroutePass()returnsfalseonce all plane-net items are connected — letting the routing loop exit cleanly.- Confirmed bug — plane routing introduces clearance violations (Issue 093): Routing
Issue093-interf_u.dsn(bottom-copper GND pour) with the current code introduces 62 clearance violations and logs an internal error in the autorouter pass. The plane-routing code path is active when this occurs. This is a safety-critical open bug. - No pour connectivity / void detection: If a foreign-net trace cuts through a pour layer, creating an isolated copper island, Freerouting does not detect the disconnected region. The
RatsNestand the exported.sesfile will show the net as fully routed even though part of the pour is electrically floating. This is long-term future work. - Loading boards to check plane flags without routing: Use
DsnReader.readBoard(InputStream, BoardObservers, IdGenerator, String)(returns aBoardReadResult) directly when you only need to inspect the loaded board state (e.g. verifyNet.contains_plane) without running the routing scheduler. This is faster and avoids timeout issues in tests.
Key facts about how API usage analytics are tracked — established during the analytics tracking investigation.
- API controller (
JobControllerV1,SessionControllerV1,SystemControllerV1) callsFRAnalytics.apiEndpointCalled(method, request, response, userId)on every successful (2xx) response. FRAnalytics.trackAnonymousActionbuilds aPropertiesmap and callsFreeroutingAnalyticsClient.track(...), which POSTs a JSONPayloadasynchronously (background thread) tohttps://api.freerouting.app/v1/analytics/track.AnalyticsControllerV1.trackActionon the server receives that POST and writes the row to BigQuery via the singletonBigQueryClient, into the tablefreerouting-analytics.freerouting_application.api_endpoint_called. The table name is derived from the event name by lowercasing and replacing spaces/hyphens with underscores.
FRAnalytics.permanent_user_idis alwaysnullin headless/API mode.FRAnalytics.setUserId()is only called from the GUI startup path. Always pass the per-requestuserIdUUID fromAuthenticateUser()to the 4-arg overloadFRAnalytics.apiEndpointCalled(method, request, response, userId)so that callers can be identified in BigQuery. Never callFRAnalytics.setUserId()from a request-handling thread — it writes static state shared across all concurrent requests (race condition).FRAnalytics.apiEndpointCalledhas two overloads. The 3-arg overload (no userId) is kept for unauthenticated endpoints (e.g.GET /v1/system/status) where no caller UUID is available. All authenticated controller methods must use the 4-arg overload, passing the return value ofAuthenticateUser().- Error responses (4xx/5xx) are tracked by
ApiAnalyticsFilter, not by the controller methods. This JAX-RSContainerResponseFilter(registered inFreeroutingApplication) fires for every response with status ≥ 400 — including 401 Unauthorized responses aborted byApiKeyValidationFilterbefore the controller runs. Successful (2xx) responses are intentionally skipped by the filter to avoid double-tracking with the richer controller-level calls. BigQueryClientis a singleton. UseBigQueryClient.getInstance(libraryVersion, serviceAccountKey)— nevernew BigQueryClient(...)— inAnalyticsControllerV1. Constructing a new instance on every request re-authenticates against GCP's token endpoint (network I/O). The singleton is recreated automatically if the service-account key value changes (key rotation).FreeroutingAnalyticsClientdelivery failures are aggregated, not logged per-failure. CallingFRLogger.debug/warninside the async send thread floods logs when the analytics endpoint is unreachable. Instead, failures are routed toAnalyticsErrorAggregator.recordFailure(endpoint, e). The first failure in each window is logged immediately at WARN; subsequent failures are counted silently. A daemon thread ("analytics-error-reporter") flushes a per-error-type summary every 60 minutes: at WARN if total ≤ 50, at ERROR if > 50 (sustained outage).
- All analytics events land in
freerouting-analytics.freerouting_application.<event_name>where the table name is the event string lowercased with spaces/hyphens replaced by underscores (e.g."API Endpoint Called"→api_endpoint_called). - Standard columns present on all rows:
id,received_at,sent_at,timestamp,loaded_at,uuid_ts,user_id,anonymous_id,event,event_text,context_library_name,context_library_version. - API-call-specific columns:
api_method(e.g."GET v1/jobs/…"),api_request,api_response. user_idandanonymous_idare both set to the caller'sFreerouting-Profile-IDUUID for authenticated API calls. Both will benullfor unauthenticated endpoints (system status, environment) and for any legacy events fired before fix #1 was applied.
-- Check recent API events are arriving
SELECT received_at, anonymous_id, api_method, context_library_version
FROM `freerouting-analytics.freerouting_application.api_endpoint_called`
WHERE received_at >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 HOUR)
ORDER BY received_at DESC LIMIT 50;
-- Call count per endpoint (last 24 h)
SELECT api_method, COUNT(*) AS calls
FROM `freerouting-analytics.freerouting_application.api_endpoint_called`
WHERE received_at >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 24 HOUR)
GROUP BY api_method ORDER BY calls DESC;
-- Confirm null-user-id volume (should be ~0 after fix #1)
SELECT
COUNTIF(user_id IS NULL OR user_id = '') AS null_user_ids,
COUNT(*) AS total
FROM `freerouting-analytics.freerouting_application.api_endpoint_called`
WHERE received_at >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY);
-- Error-response breakdown by endpoint (tracked by ApiAnalyticsFilter)
SELECT api_method, COUNT(*) AS error_count
FROM `freerouting-analytics.freerouting_application.api_endpoint_called`
WHERE received_at >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY)
AND (api_response LIKE '%error%' OR api_response LIKE '%not found%')
GROUP BY api_method ORDER BY error_count DESC;| File | Role |
|---|---|
analytics/FRAnalytics.java |
Static facade; apiEndpointCalled(…, UUID userId) is the primary call site |
analytics/FreeroutingAnalyticsClient.java |
HTTP client posting to api.freerouting.app/v1/analytics/track |
analytics/AnalyticsErrorAggregator.java |
Aggregates delivery failures; emits first-failure WARN + hourly summary |
analytics/BigQueryClient.java |
Singleton GCP BigQuery writer; getInstance() avoids per-request re-auth |
api/ApiAnalyticsFilter.java |
JAX-RS dual filter; tracks all ≥ 400 responses centrally |
api/FreeroutingApplication.java |
Registers ApiAnalyticsFilter alongside existing filters |
Key facts and invariants for the dedicated MCP server implementation.
- Freerouting runs MCP as a separate server with its own settings block:
mcp_server. mcp_serverhas independent lifecycle flags and network config:enabled,running,http_allowed,endpoints,cors_origins.- MCP authentication config is independent from REST API config:
mcp_server.authentication. - MCP tools execute against the REST API using
mcp_server.target_api_base_url.
- Public discovery endpoint:
GET /.well-known/agent.json(A2A Agent Card). - JSON-RPC endpoint:
POST /v1/mcp(initialize,tools/list,tools/call). - Realtime channels:
GET /v1/mcp/events(SSE) andGET /v1/mcp/ws(WebSocket). - Tool inventory is generated from OpenAPI (
/openapi/openapi.json) and exposes nearly all/v1/*routes except MCP routes themselves.
mcp_server.target_api_base_urlmust point to the REST API base URL.- It must never point to MCP routes (for example
/v1/mcpor/.well-known/*), otherwise tool calls are rejected with a configuration error.
- MCP request authentication uses the same header conventions as API requests:
Authorization: Bearer <API_KEY>when auth is enabled.Freerouting-Profile-ID(orFreerouting-Profile-Email).Freerouting-Environment-Hostin<ToolName>/<Version>format.
- Keep API and MCP authentication paths independent; changing one must not silently change the other.
- API and MCP both support configurable fixed-window rate limits:
api_server.rate_limitmcp_server.rate_limit
- MCP and REST responses include
X-Correlation-IDfor request tracing. - MCP tool bridge must forward
X-Correlation-IDto underlying REST calls so logs can be cross-linked.
- Update
docs/API/MCP.mdwith concrete startup, verification, and troubleshooting steps. - Update
docs/API/API_v1.mdMCP endpoint tables/examples. - Update
docs/settings.mdwhenevermcp_serverfields change. - Update
docs/architecture.mdif package boundaries or data flow changes.
The Docker images are built and published by two GitHub Actions workflows:
.github/workflows/docker-release.yml— triggered on every GitHub release; tags the image with the semver version andlatest..github/workflows/docker-nightly.yml— triggered on every push tomaster; tags the image asnightly.
Both workflows build a multi-platform manifest image using Docker Buildx + QEMU. The supported platforms are:
| Platform | Hardware |
|---|---|
linux/amd64 |
x86-64 servers, PCs, most cloud VMs |
linux/arm64 |
Apple Silicon, AWS Graviton, Raspberry Pi 4/5 (64-bit OS) |
Note:
linux/arm/v7(32-bit ARM) support was dropped when the project moved to Java 25, becauseeclipse-temurin:25no longer publishes anarm/v7image. Raspberry Pi users running a 32-bit OS should switch to a 64-bit OS image to use the official Docker image.
provenance: false is set on both workflows so that the manifest does not contain build-attestation entries (which show up as "architecture": "unknown" in docker manifest inspect).
The Gradle tests are run on the native ubuntu-latest runner before the multi-platform build step to avoid QEMU network/loopback unreliability inside the cross-compiled build container.
For end-user self-hosting instructions see docs/self-hosting.md.
The Windows, Linux, and macOS installers use jlink to build a minimal bundled JRE. The --add-modules list must include every JDK module that the application (or its bundled libraries) accesses at runtime — jlink does not auto-detect usages from inside a fat jar.
| Platform | Required modules |
|---|---|
| Windows | java.desktop,java.logging,java.management,java.net.http,java.sql,java.xml,jdk.crypto.ec,jdk.crypto.mscapi,jdk.management |
| Linux | java.desktop,java.logging,java.management,java.net.http,java.sql,java.xml,jdk.crypto.ec,jdk.management |
| macOS | java.desktop,java.logging,java.management,java.net.http,java.sql,java.xml,jdk.crypto.ec,jdk.management |
jdk.crypto.mscapiis Windows-only (providesSunMSCAPIfor the Windows OS certificate store). Do not add it to Linux/macOS scripts.
java.management—ManagementFactory(used inSystemControllerV1,RoutingJobSchedulerActionThread, Log4j2 startup, Jersey-server). Missing this module causesNoClassDefFoundError: java/lang/management/ManagementFactoryon startup and crashes the/v1/system/statusendpoint with HTTP 500.jdk.management—com.sun.management.ThreadMXBean.getThreadAllocatedBytes()andcom.sun.management.OperatingSystemMXBean.getCpuLoad(). Transitively brings injava.management.jdk.crypto.ec— EC crypto provider (SunEC) required for TLS 1.3 / ECDHE key exchange. Without it, all HTTPS connections to Google APIs (OAuth2, BigQuery, Sheets) fail at the SSL handshake.jdk.crypto.mscapi— Windows native certificate store access. Without it, Java uses only its own bundledcacerts, which is usually sufficient but may miss system-level certificates.
java.instrument— javassist references it but HK2/Jersey proxy generation usesClassLoader.defineClass(), not bytecode redefining. Do not add.jdk.attach/jdk.jdi— javassist debugging tools. Not needed for runtime proxy generation. Do not add.jdk.httpserver— referenced in jersey-server module-info but Freerouting uses Jetty, not the JDK HTTP server. Do not add.java.naming— log4j-core requires itstatic(optional) for JNDI lookups; Freerouting does not use JNDI lookups. Do not add. Instead,System.setProperty("log4j2.disableJndi", "true")is set at the very start of the Log4j2 property-setup block inFreerouting.main()(beforeLogManager.getContext()is first called) so Log4j2 skips its JNDI plugin registration entirely and emits no WARN.java.rmi— log4j-core requires it for remote JMX; not needed for local embedded logging. Do not add.
All ManagementFactory call sites are wrapped in try/catch (Throwable) so the application degrades gracefully (returns -1 / skips stats) rather than crashing when running on a custom or stripped JRE:
try {
ThreadMXBean threadMXBean = (ThreadMXBean) ManagementFactory.getThreadMXBean();
// ...use threadMXBean...
} catch (Throwable t) {
// java.management or jdk.management module not available in this JRE build
}Affected files: SystemControllerV1.getCpuLoad(), RoutingJobSchedulerActionThread.monitorCpuAndMemoryUsage().
To check what JDK modules a jar requires, use:
jdeps --ignore-missing-deps -q --print-module-deps <jar-file>For the project's own thin jar (without bundled deps):
jdeps --ignore-missing-deps -q --print-module-deps build/libs/freerouting.jarTo inspect a named module's requires declarations:
jar --describe-module --file <jar-file> | grep requiresWhat was wrong: ApiKeyValidationFilter checked for the Authorization: Bearer header and immediately rejected with 401 if the header was absent — before calling ApiKeyValidationService.validateApiKey(). This meant even when authentication.enabled=false, requests without an Authorization header were rejected.
Fix applied: After ApiKeyValidationService.getInstance(), skip all validation when authentication is disabled:
ApiKeyValidationService validationService = ApiKeyValidationService.getInstance();
if (!validationService.isAuthenticationEnabled()) {
return; // skip all validation
}
// ...then check for the header...ApiKeyValidationService.validateApiKey(apiKey) already returns true when isEnabled == false — the bug was entirely in the filter's early-exit before that call was reached.
Two defaults were hardened:
| Setting | Old default | New default | Reason |
|---|---|---|---|
ApiAuthenticationSettings.isEnabled |
false |
true |
Auth off + world-wide bind was a security risk |
ApiServerSettings.endpoints |
https://0.0.0.0:37864 |
http://127.0.0.1:37864 |
Localhost-only by default; https:// was misleading (HTTPS not implemented, fell back to HTTP silently) |
For local EDA plugin use (KiCad, EasyEDA): The server already binds to 127.0.0.1 only, so network-level exposure is eliminated. Authentication should be explicitly disabled for seamless plugin operation:
java -jar freerouting-executable.jar --gui.enabled=false --api_server.enabled=true --api_server.authentication.enabled=false
For remote/cloud deployments: Keep authentication.enabled=true (the default) and configure a provider (e.g. GoogleSheets). To expose to a network interface, explicitly set --api_server.endpoints=http://0.0.0.0:37864.
Key facts and invariants for all GitHub Actions workflows under .github/workflows/.
| File | Trigger | Purpose |
|---|---|---|
gradle-build-on-pr.yml |
pull_request |
Build + test on Ubuntu, macOS, Windows |
gui-a11y.yml |
pull_request (path-filtered to GUI sources/tests/resources) |
GUI accessibility tests on Ubuntu, macOS, Windows |
pre-commit.yml |
pull_request + push to master |
Run pre-commit hooks |
create-snapshot.yml |
push to master |
Build all platform installers and publish to the SNAPSHOT release |
create-release.yml |
push of v* tag |
Build all platform installers and publish to the versioned release |
docker-nightly.yml |
push to master |
Build + push multi-arch Docker image tagged nightly |
docker-release.yml |
GitHub release published | Build + push multi-arch Docker image tagged with semver + latest |
deploy-pages.yml |
push to master (paths: website/**) |
Deploy static website to GitHub Pages |
stale.yml |
daily schedule | Mark inactive issues/PRs stale after 120 days |
All jobs must declare an explicit permissions block with least-privilege. The default is contents: write for classic workflows — that is too broad. Use contents: read for read-only jobs and contents: write only on jobs that upload release assets or push packages.
All jobs must have a timeout-minutes value to prevent runaway billable-minutes consumption:
- Gradle build/test jobs: 30 min
- Docker multi-platform build jobs: 60 min
- Stale/pages jobs: 10 min
Fresh checkouts on GitHub-hosted runners do not preserve file permissions. Always add a chmod +x gradlew step before calling ./gradlew on Linux and macOS runners.
Platform build jobs (build-jar, build-ubuntu-x64, build-windows-x64, build-macos-arm64) must declare:
needs: [ build-and-test, delete-old-snapshot-assets ]Never depend on delete-old-snapshot-assets alone. If build-and-test fails, the old assets must not be deleted and no broken artifacts should be published. Depending only on delete-old-snapshot-assets (as was the original bug) allows the deletion to race ahead of a failing build and leave the SNAPSHOT release empty or stale.
Use gradle/actions/setup-gradle@v5 (not v3/v4). v5 is the current stable version with improved caching and Gradle 9 compatibility.
Use actions/stale@v9. v5 (previously used) ran on Node.js 16 which is EOL. Do not downgrade. Do not pin to a commit SHA — the SHA has historically broken when the upstream repository rebased its release tags.
Key facts about how PolylineTraceNormalization, BasicBoard.normalize_traces(), and AutorouteBatchLoop.run() interact — established during the Issue 676 regression investigation.
PolylineTraceNormalization.MAX_NORMALIZATION_DEPTH (= 16) caps the recursion depth of trace normalization. Exceeding the cap returns false (no further change at that depth level) instead of throwing — an exception here used to be swallowed by a caller try-catch and surfaced only as:
WARNING The normalization of net 'GND' failed.
return false is safe because the outer loop in normalize_traces treats it as "no change for this trace" and continues to the next trace, terminating normally. normalize() declares no checked exception.
combine_at_end contains two code paths:
- Full remove + re-insert — used when
joined_polyline.arr.length != new_line_count(parallel lines were skipped at the join). - Optimised merge via
merge_entries_at_end— reuses existing search-tree leaf nodes for performance.
Path 2 calls p_to_trace.get_search_tree_entries(tree) and p_from_trace.get_search_tree_entries(tree). Either can return null if the trace has no entries in the given search tree (e.g. it was freshly inserted or its entries were cleared). This caused a NullPointerException inside ShapeSearchTree.merge_entries_at_end, visible in the stack as:
NullPointerException: Cannot load from object array because "to_trace_entries" is null
at ShapeSearchTree.merge_entries_at_end(ShapeSearchTree.java:237)
at PolylineTrace.combine_at_end(PolylineTrace.java:395)
at PolylineTrace.normalize(PolylineTrace.java:776)
Fix applied (via PolylineTraceSearchTreeAdapter.hasDefaultEntries): Before choosing path 2, combine_at_end checks whether both this and other_trace have entries in the default search tree (board.search_tree_manager.get_default_tree()). If either is null, it falls back to path 1 (the safe full-remove + re-insert).
Why the default tree is sufficient for the guard: All traces are always inserted into the default tree. Compensated trees are built on top of it. If a trace is missing from the default tree it is definitionally not in any compensated tree either.
v1.9 maintained an already_checked_board_hashes set. Between passes it computed a board hash; if the same hash appeared twice, it stopped routing.
Current state: AutorouteBatchLoop.run() declares an alreadyRoutedBoardHashes HashSet<String>, but the same-hash early stop is disabled (commented out) — per-pass ripup budgets and random seeds change each pass, so an identical hash can still make progress later. Stagnation is instead handled by the BoardHistory restore check, which uses a strict > comparison so equally-scored boards do not trigger unbounded restores. The hash set is still cleared whenever a previous board state is restored (ripup + retry cycle), so a restored state can be routed again with a higher ripup cost.
getMaximumScore() returns maximumCount * unroutedNetPenalty. When a board is already fully routed (maximumCount == 0), this is 0, and dividing by it produces NaN. NaN failed all comparison-based stagnation checks, causing the score to always appear as 0.
Fix applied: A if (maximumScore <= 0f) return 0f; guard was added in getNormalizedScore() before the division.
| Symptom | Root cause | Fix location |
|---|---|---|
WARNING The normalization of net 'GND' failed. (every pass) |
PolylineTraceNormalization hit the max depth and threw |
PolylineTraceNormalization — throw → debug + return false |
NullPointerException in merge_entries_at_end |
Trace missing search-tree entries, combine_at_end chose optimised path |
PolylineTrace.combine_at_end() — PolylineTraceSearchTreeAdapter.hasDefaultEntries guard before path 2 |
| Score always 0 | getNormalizedScore() divided by 0 when board fully routed |
BoardStatistics.getNormalizedScore() — <= 0 guard |
| Router loops endlessly on same board | No board-hash stagnation detection | AutorouteBatchLoop.run() — same-hash early stop currently disabled (see above); stagnation handled by the BoardHistory strict-> restore check |