Repository navigation
Remove the old chain-based track model - #16
Merged
Merged
Conversation
The ecc@ecc plugin ships 408 skills, 71 agents and 97 commands for every stack, and a plugin install is all-or-nothing: plugin.json declares skills: ["./skills/"], so there is no per-skill enable. The --modules machinery under manifests/ belongs to the standalone `ecc setup` installer, and the lean@1 context profile says of itself that it does not activate a harness. Selection therefore has to happen at use time, which is what this section records. Lists the nine skills and agents that fit a .NET 10 / Uno / Clean Architecture repo, the two that need a caveat, and the gaps: - csharp-testing is xUnit-based but prescribes FluentAssertions, NSubstitute and Testcontainers, none of which this repo uses; tests here are plain xUnit with hand-written doubles under Test/Trackify.Tests/Fakes/. - mcp-server-patterns is Node/TypeScript only with no C# content, so it is not a reference for the ModelContextProtocol work in Infrastructure/Mcp/. - ECC has no C#/.NET build resolver, so /ecc:build-fix has nothing to delegate to for a failed dotnet build. - ECC has no Uno/XAML/MAUI/WinUI skill at all, leaving the whole app head uncovered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Streckenplaner's geometry was invented: 90-unit straights, radius-70 curves, 90-degree turns, 30-degree branch peel-off. None of those numbers exist in the LEGO system, so a plan built with them cannot be plugged together out of real track. Replace it with the real dimensions, in studs (1 stud = 8 mm): straight 16 studs (53401) curve R40, 22.5 degrees, 16 to the full circle (53400) point 32 studs main route, 22.5 degree diverging route (53407/53404) crossing 16x16 studs, 90 degrees (9V 32087) The important consequence is that positions cannot be snapped to a grid. A curve ends at 40*sin(22.5 degrees) = 15.3073... studs, so after a few parts nothing sits on a round number; rounding would produce a plan that looks tidy on screen and falls apart in plastic. The only safe invariant is that every part is plugged into a port of another part, so a plan is a graph of parts and port connections and world coordinates are derived by walking it -- never stored, never entered. Headings, by contrast, are exact: always a multiple of 22.5 degrees, so a heading is an integer step 0..15 and compares exactly. Positions compare against a tolerance. Catalogue port poses are derived from the drawn spans rather than typed in, so geometry and connection points cannot drift apart. The catalogue holds only parts LEGO actually makes; the community radii R56..R104 are each one more entry, not a code change. The walker reports connections the geometry contradicts instead of letting the last write win -- that is a loop the user drew which does not really close, and saying "one curve short" is the most useful thing the planner can tell them. Verified by 77 tests, including: 16 R40 curves return to the start pose, a stadium oval of 8 curves + straight + 8 curves + straight closes, and a 15-curve loop is diagnosed as one 22.5 degree step and one chord short. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two halves of "draw it instead of configuring it", both pure geometry. TrackSnapper follows a stroke: from an open end it repeatedly picks the straight or R40 curve whose far end lands nearest where the stroke will be a part-length further on, with a small penalty for disagreeing with the stroke's own direction so it does not zig-zag between a left and a right curve where a straight tracks better. Greedy on purpose -- it runs on every pointer move, and a preview that follows the finger now beats one that is a millimetre closer later. The gesture may only lay straights and curves; points and crossings are placed deliberately, never guessed. TrackLoopCloser is a search, not a fit, because closing a loop has to land on the target pose exactly and greedy following never will. Iterative deepening gives the fewest parts that work, pruned by two admissible bounds: no part advances further than a straight, and none turns by more than one 22.5 degree step. The property that makes the whole approach honest is tested directly: a free-form stroke -- straight, quarter circle either way, or a wiggle -- always yields a plan the walker finds buildable, because nothing free-form is ever stored, only whole catalogue parts plugged port to port. 92 tests green, including: a quarter-circle stroke becomes exactly four R40 curves, a circle one curve short is closed with exactly one curve, two ends already nose to nose get a connection and no new track, and a gap beyond the budget is reported as no answer rather than a bad fit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"I generate it in the UI and import it into the CLI, and the rest no longer interests me" -- so export/import is the feature, not a trailer. The format (trackplan.v1.json) stores the GRAPH, not coordinates: parts by catalogue key plus connections written as "p1:end" -> "p2:start". A stored coordinate could describe a layout that cannot be plugged together; a connection cannot. Document-local ids keep the file readable by a person, which is the point of having one. Drive behaviour sits in its own block per part, separate from topology, because they are different kinds of fact -- and because the camera import that comes later can only ever supply topology. The same reasoning puts an optional measured "pose" on each part: a reconstruction from a photograph hands over poses, and ports landing on each other within tolerance can be paired into connections from there. Nothing reads it yet; it is the hook that keeps that path open without changing the format. Points now carry what they need to be driven: which hub and port throws them, where the sensor watching the approach is plugged in, and entry rules saying "a train arriving here, bound for there, needs the point set this way". Destination-specific rules beat catch-all ones regardless of list order. Acting on this is later work; the data exists now. Persistence moves to EF Core migrations (the arc42 section 11 debt), with dotnet-ef pinned as a local tool so CI and every machine generate the same thing. A plan is read and written whole, in one transaction -- half a graph would render as track that falls apart. Generated migrations are marked as generated code in .editorconfig, since dotnet ef emits block-scoped namespaces that IDE0161 rejects as an error here. The DTO boundary grows to match: the architecture test now also forbids the CLI from touching Trackify.Domain.Tracks, which caught a real slip -- `track show` was reading TrackMetrics directly to print metres. The summary converts it instead. trackify track import <file> [--replace] [--dry-run] | export | show Verified by 109 tests and a real CLI run: importing a straight, two R40 curves and a wired left point reports 4 parts, 3 connections, 3 open ends, and `track show` lists the parts needed, 79 studs of rail and confirms every joint lines up. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
You pull from a glowing dot and real LEGO track follows your finger. That is the whole mechanic, and building needs no properties at all: speed, direction and sensor are edited afterwards by tapping a piece. Nothing free-form is ever stored. A gesture becomes a stroke in studs, TrackSnapper turns the stroke into whole catalogue parts, and the drawing comes back out of walking the graph -- so everything on screen is buildable out of real track by construction. Drag near another open end and TrackLoopCloser searches for the parts that genuinely reach it rather than bending anything to fit. The editor is no longer a chain of segments with a properties table beside it. Strands are gone: a plan is a graph, so a part has ports, not a position in a list, which is why reorder and duplicate went with them. Points and crossings are placed deliberately at the aimed-at open end, because the snapper may only ever lay straights and curves -- a gesture must not guess a turnout. Undo/redo covers building; property edits stay off the stack, since one tap on "Zurück" undoing a slider nobody thought of as a step is worse than no undo. Within the house rules: a new DrawGestureBehavior turns pointer press/ drag/release into commands with a point, so drawing adds no code-behind; it sits on the Canvas itself, so points arrive in canvas coordinates and the zoom (still a bound Viewbox size) never has to be undone by hand. TrackPlanFitter computes the view from the committed parts only, so the plan does not rescale under the finger while a preview grows past its edge. Found while verifying, and fixed here: - Uno does not implement Shape.StrokeLineJoin, so it is gone from the XAML. - A database created by the old EnsureCreated() has no __EFMigrationsHistory, so Migrate() died with "table already exists" -- which says nothing about what to do, against a file holding the user's saved trains. TrackifySchema now detects it and says to move the file aside; deleting someone's data is not ours to decide. - That message arrived wrapped in "Could not resolve type ..." because commands are built by DI, so the CLI's handler now reports the innermost cause. - SecondViewModel is started with a fire-and-forget load, where a store failure would vanish as an unobserved task exception and leave an editor that silently has no plan. Load and save now surface it on the canvas. Verified: all four buildable heads green, 109 tests green, the desktop head launches (exit 0), and `track import` still round-trips against a fresh store. The legacy-database path was exercised directly and reports the intended message. arc42 §05 updated, ADR-17 (graph, not coordinates) and ADR-18 (migrations) recorded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The graph editor (ADR-17) replaced the strand chain, but the shared core still carried the model it replaced. Nothing read it any more, so it goes: - Domain: `Trains/TrackSegment` and the `SegmentType` enum. `CurveDirection`, `SwitchRoute` and `TrackDirection` stay — the graph model uses all three. - Application: `TrackSegmentDto`/`TrackSegmentMapping`, `ITrackSegmentRepository`, `ITrackPlanService`/`TrackPlanService`, `TemplateType`, and the two bits that existed only to feed the old "Teil anhängen" toolbar (`TrackPartOption` and `LegoinoCatalog.TrackParts`, neither of which had a caller left). - Infrastructure: `SqliteTrackSegmentRepository`, the `TrackSegments` DbSet and its key mapping. The table is dropped by its own `DropTrackSegments` migration rather than by editing `InitialCreate`, so a database that already ran the initial migration drops the table instead of failing to match it. Docs pulled in step: the Domain/Application/Infrastructure tables in arc42 §5 now name the graph model, §8's class diagram shows `TrackPart`/`TrackLink`, R6 in §1 and debt D-6 in §11 point at the surviving files, and ADR-17 records that the chain model is fully replaced. CLAUDE.md and README.md said `TrackSegment` was a Domain entity; CLAUDE.md also still claimed the schema is created with `EnsureCreated()` and no migrations, which this commit's migration contradicts. Verified: 106 tests green (109 minus the three deleted `TrackPlanServiceTests`), all four buildable heads green, and `trackify track show` against a fresh store creates only Trains/TrackParts/TrackLinks with both migrations applied. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
All 106 existing tests stop at Domain and Application. SecondViewModel and TrackPlanFitter live in the app head, so nothing exercised the half of the editor the user actually touches. Why a second test project: Source/Trackify declares only head TFMs, with no plain net10.0, so the existing net10.0 Trackify.Tests cannot reference it. This one uses Uno.Sdk and targets net10.0-windows10.0.19041.0 rather than net10.0-desktop, because the desktop head pulls in WPF transitively and NETSDK1136 then demands a Windows-targeted consumer. The consequence is recorded in ADR-13: Windows only, deliberately not in ci.yml, where the plain net10.0 project stays the gate. The tests drive the ViewModel the way the canvas does -- press, drag, release -- and check what the plan became: - a drag from an open end lays track the walker finds buildable - a drag that curves answers with R40 curves, not only straights - a press away from every open end lays nothing - mid-drag the preview is drawn but does NOT count as laid, which is the distinction a ghost preview lives or dies by - undo takes a run back and redo restores it - the oval template is a closed loop of 18 real parts - a point lands with a SwitchConfig, and deleting a piece takes its connections with it - a store that cannot be read surfaces on the canvas instead of vanishing as an unobserved task exception On the fitter, the test that matters most is that a growing preview never moves the committed plan: the view is computed from committed parts alone, or the whole layout would shift and rescale under the user's finger. Also pinned: ToStuds is the exact inverse of the fit, a small plan is not blown up past the default scale, an oversized one is shrunk to the canvas, and a point draws as two runs of rail. Renamed from Trackify.App.Tests: that namespace collided with the app's own App type (CS0435). 131 tests green: 106 + 25. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Finishes the app head's test coverage. The editor screen was done; this adds the train screen and the per-part presentation logic. MainViewModel (both partials), 27 tests: adding a train selects it and clears the search so it is actually visible; duplicating puts the copy right after its original; deleting falls back to the first train left; duplicate and delete stay disabled with nothing selected. The search filters by name and by hub type, case-insensitively and trimmed. The active count follows a train being switched off. Choosing a colour marks exactly one swatch. Hub control: a platform without Bluetooth says so instead of scanning; a scan that finds nothing offers no hub; a found hub is held for confirmation and becomes a train, or binds to the train that already knows its MAC rather than adding a second. Connecting addresses the hub by its discovered device id and falls back to the typed-in BLE address -- the HubKey rule, which is easy to get backwards. Disconnecting clears the flag and re-enables Connect. Only the two offline branches of ApplyConnection are exercised. With a server URL set, the sync genuinely dials out, and a test must not depend on that. TrackPiece, 15 tests: the speed-colour thresholds, a German label for every catalogue part, the sensor marker's kind, and that the speed label and sensor marker land on opposite sides of the rail -- the label offset against the outward normal, the marker along it. That last one caught my own wrong assumption about the sign before it caught anything else. ShellViewModel has no tests on purpose: it is an empty class that exists only so Navigation's root RouteMap has something to bind. 175 tests green: 106 in the net10.0 project, 69 here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Towards 100% line / >=90% branch coverage. Two parts: saying what the
target covers, and starting on the biggest gap.
The scope is declared in both test projects rather than left implicit,
because a percentage over code that cannot run is a number without a
meaning. Excluded, deliberately:
Infrastructure/Ble BlueZ over D-Bus; needs a Pi with a real radio
Application/Services Plugin.BLE and WinRT; compile only on their heads
app head Platforms/**, App.xaml.cs, XAML partials, and the
two layout code-behinds, which need a visual tree
Writing tests for those would mean mocking the hardware away and then
asserting the mocks. The Threshold switch is left off until the scope is
actually covered; turning it on now would fail every build.
Measured in scope, the picture changes a lot: Infrastructure 68% -> 89%
line and 13% -> 45% branch, Application 67% -> 72%. Total 62% -> 72.7%
line, 62.7% branch.
Then the CLI, which was the real hole at 0.5%. The track commands are now
covered through their exit codes, which is the CLI's actual contract --
what a script or a systemd unit reacts to. `show` returns 1 for a plan
whose joints do not line up; `import` returns 1 for a missing file without
touching the store, passes --replace through, and a --dry-run validates
without writing; `export` writes the file and passes the plan name on.
Finding the way in took a TDD loop of its own: ExecuteAsync is protected
on Spectre's AsyncCommand, and ICommand exposes ExecuteAsync rather than
Execute, so tests go in the way Spectre's own executor does. That harness
is the reusable part -- the nine remaining commands can follow it.
CLI 0.5% -> 14.2% line. 117 + 69 = 186 tests green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Focused on Domain, Application and Infrastructure. Three gaps the coverage report pointed at, largest first. SqliteTrackPlanRepository was at 0% -- the whole class, including code added on this branch. Now tested against a real SQLite file, because the things worth proving only exist there: that parts and connections survive together, that a point's SwitchConfig round-trips through its nested JSON column (motor, sensor, trigger colour and entry rules), that Replace throws the previous plan away, that Append continues past the existing positions instead of reusing them, and that enum fields come back as the values they went in as. LegoinoCatalog and its seven option records were at 0%. The useful assertion there is not that the data exists but that every enum value a picker can hold is in it -- a missing entry makes the lookups throw in front of the user. Each of SpeedFunctionType, TrackDirection, SensorType and SensorActionType is now checked against its list. TrainMapping was at 50%: only one direction was exercised. A round trip now pins every field, which is the only thing that catches a field forgotten in the mapping. Two of my own assumptions were wrong and the tests caught them, not the code: SpeedFunctionType.Custom carries no formula on purpose (the user types their own), and a fresh Train is active by default. Both now assert what is true rather than what I assumed. The repository's append offset was right too -- my "four distinct positions" assertion was wrong, because the fixture left both parts at order 0. In scope: Application 71.8% -> 86.7% line, Infrastructure 89.0% -> 92.8% line and 45% -> 75% branch. Total 72.7% -> 77.0% line, 64.7% branch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TrackPlanTopology sat at 57%: the four existing track tests all use it as a builder via Add/Connect, so Disconnect, Remove, TryGetPart and IsPortUsed were never exercised — and those are exactly the edits the editor makes when a user deletes or unplugs something. Now covered, including the two properties that matter beyond the happy path: that every edit returns a new topology and leaves the original alone, which is what lets the editor's undo stack be a plain stack of these values; and that removing a part takes every connection touching it with it, rather than leaving a link pointing at nothing. Written in AAA form with the phases marked, which is the style the rest of the suite is being moved to. Domain 92.5% -> 94.3% line, 74.8% -> 76.0% branch. Total 77.0% -> 77.5%. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
It was the largest single gap left at 28.6% line / 38.9% branch: the service that every drive command in both front-ends goes through was barely exercised. Now covered: that connecting prefers the discovered device id over the typed-in BLE address and falls back the other way when there is none -- the HubKey rule, which is easy to get backwards and which both front-ends depend on; that a train with no hub at all is refused loudly on connect (a silent no-op there would look like a connection that worked) but is a quiet no-op on disconnect, speed and LED, where there is nothing to undo; that the speed is clamped to the +/-100 a motor accepts; and that the LED is sent as the three bytes of the catalogue's hex. The debounce gets two tests, because its value is entirely in what it does NOT send: a single change arrives after the pause, and three changes in quick succession -- a dragged slider -- leave only the last one reaching the hub. Written as a sister file rather than extending TrainControlServiceTests, so the pending AAA refactor and this do not collide in one file. AAA form with the phases marked. Application 86.7% -> 94.0% line, 74.6% -> 81.5% branch. 167 tests green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
My own slip: a `git add -A` while measuring coverage checked in coverage.cobertura.xml for both test projects. Coverlet rewrites them on every -p:CollectCoverage run, so they would have produced a noisy diff forever and tempted someone to resolve conflicts in generated XML. Untracked (the files stay on disk) and added to .gitignore. The existing /coverage/ entry did not cover them, because Coverlet writes its report next to the test project rather than into a folder of its own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
master's 62a5dce ("Keep the planner and editor panes from clipping their content") landed on the editor files this branch rewrote, so the merge needed deciding rather than resolving mechanically. Its substance is kept, not discarded: - The pane widths (260/340) and splitter minimums auto-merged into SecondPage and its code-behind, and are verified present. - SegmentInspector's header conflicted. Taken from master: the Grid with a `*` column instead of a horizontal StackPanel, which is the actual fix — a StackPanel measures its children with infinite width, so the id overflowed the pane instead of trimming. Taken from this branch: the binding, since SelectedSegment became SelectedPiece when the chain model gave way to the graph. - StrandList was a modify/delete: deleted here in e3df738 because there are no strands any more. It stays deleted, but its fix did not die with it — the NoWrap + CharacterEllipsis it gained is now on PieceList's name row, which has the same no-horizontal-scroll pane around it. Verified after merging: desktop head builds clean, 167 + 69 tests green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Operator-facing, page by page: what the planner's canvas, toolbar, parts list and inspector each do, and what every setting means. Written from the XAML's own labels so it matches what is on screen rather than describing an invented layout. The five screenshot slots under docs/images/ are left empty on purpose. The Skia surface comes back blank here and the WASM canvas times out the browser tool, so the captures have to come from a real device; the file names and what each should show are listed at the end of the document. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
docs/editor-view.md is now the English version and docs/editor-view.de.md the German one, cross-linked at the top. Both quote the app's German labels verbatim, because that is what is on screen; the English one glosses each label so it reads without German. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



No description provided.