The release workflow is the only hosted job that runs the Core, Engine,
IO and Server suites on Windows, and it had no hang guard: a silent test
would hold the runner until the 30-minute cap with no clue which test
hung. Apply the guard the windows-desktop CI job already uses:
--blame-hang-timeout 5m with a mini dump, and upload all of TestResults
rather than only the TRX files.
The suites have roughly doubled since the last release run (7.5 min),
so raise the job cap to 45 minutes. Validated with actionlint.
The Details area of the Database-mode Open dialog now has a plate
preview beside the Plates/Drawings tabs, with a draggable divider. It
draws one plate of the highlighted nest; the arrow buttons below it step
through the plates ("Plate 2 of 5"), and choosing a row on the Plates tab
shows that plate, so the table and the preview stay on the same plate.
The preview is read-only (no selection or drop) and refits when resized.
NestDetails keeps the downloaded nest's plates (PlateLayouts) beside its
rows, so the preview draws the copy already read for the tables with no
second download.
Tests: NestDetails keeps every plate in nest order (fails with the
assignment removed). The Windows form test steps forward with the
button, checks the row follows, and selects a row back.
File > Open in Database mode now shows a resizable browser instead of
the small list dialog. The upper Nests grid keeps the server-side
search, sort and paging (row numbers count through the filtered list;
the title shows the server and the range). Below it, Details tabs list
the highlighted nest's plates and drawings, read from its archive after
the highlight rests for 250 ms; moving back to a nest already shown
reuses its result, and a missing archive is reported in the details
line.
Enter opens the highlighted nest (in the Find box it runs the search at
once instead) and Esc closes. Double-click opens a nest, and a context
menu offers Open, Delete (after confirmation) and Refresh. It only
browses saved nests; new nests are still created from the main window.
Windows tests drive the real form: page load, details for two nests,
Enter opening the highlighted one, the missing-archive message and Esc.
The second delta review found four more false-equal classes in the
general reflective fingerprint, all reachable only through custom
settings subclasses: cycle markers that dropped the target ancestor,
display-formatted DateTime/DateTimeOffset, ignored dictionary and set
comparers, and arrays flattened without their dimensions. Safe arrays of
OpenNest elements were also newly refused. Every repair of the generic
traversal opened another such case.
Settings capture now supports exactly the types regeneration already
accepts (OwnedCuttingParameters): CuttingParameters, SequenceParameters,
AssignmentParameters and the built-in lead-in, lead-out and tab types.
Each member is written explicitly, doubles by bit pattern with invariant
numerals and text length-prefixed. Every object's runtime type is checked
before any member is read, so no other type's code runs. A plate-scoped
request whose part or plate settings contain any other type, subclasses
included, is UnsupportedGeometry at capture instead of a Ready plan that
can never apply. A nested settings object replaced by such a type after
capture makes Apply Stale. Detached part-list requests are unaffected.
Coverage tests fail when a supported type gains a property or field the
fingerprint does not write, or when a new built-in lead or tab type is
added without fingerprint support.
Delta review found three Important defects and a Minor one in the
settings fingerprint that closed the first freshness gap:
- Accepted settings state was silently omitted: dictionary entries render
as KeyValuePair structs whose Key/Value are properties, property-backed
custom structs contribute no public fields, and graphs past the depth
limit wrote a constant marker, so all three edits fingerprinted equal
and a changed plate still applied.
- Reading public properties executed arbitrary getters, so a capture
documented as read-only could mutate live settings (Bump => ++Kerf).
- An enumerable settings member was enumerated at Apply, where its
enumerator could throw out of the public commit call.
- Fingerprint text used ambient-culture interpolation, so an invariant
capture compared unequal under a digit-substituting culture.
Traversal is now a closed boundary. OpenNest types render their public
readable properties and fields. Foreign types render only declared
instance fields, which include auto-property backing fields, because a
field read executes no code. Only arrays and List/Dictionary/HashSet/
KeyValuePair are enumerated, with insertion-ordered containers sorted;
other enumerables, delegates and unrepresentable shapes refuse to an
Invalid marker that never compares equal, so refused state is Stale
rather than silently equal. Doubles fingerprint by bit pattern rendered
with invariant formatting, and depth or budget overflow refuses instead
of truncating. A reference already on the path renders as a cycle
marker; built-in lead and tab objects reference settings back.
Capture stores refusals as-is, so a plate with uncaptureable settings
stays plannable and every commit against it reports Stale without
re-reading live state, and a fingerprint that fails on re-read is
likewise Stale, never an exception.
Document the review hardening: classification, settings content and
malformed programs count as changes, a no-op proposal stays current,
parts repeated across plates are refused, the installer is internal, and
nested-part candidates use material bounds only.
A CuttingPlanRequest constructor overload taking a Plate made the existing
detached call new CuttingPlanRequest(null) ambiguous (CS0121). Plate scope
is now requested with CuttingPlanRequest.ForPlate, and the result summary
describes dependencies and Apply as they now behave.
The cutting planner now accepts cutoffs on plate-scoped requests and
plans whole-part prerequisites captured from owned values:
- A cutoff precedes every part its nominal span crosses, using the same
rule and drawing-reference matching as automatic sequencing; a cutoff
without a definition precedes every part. Cutoffs stay fixed programs,
need no lead-in and never become rapid obstacles; rapids into and out
of them are still checked.
- A part proven, on native clean material, to lie inside a cutout of
another part precedes that host. Touching or crossing boundaries are
ambiguous and refuse; a part in a concave pocket has no dependency.
- Both searches only expand ready parts, a preserved order that breaks a
prerequisite is a constraint conflict, and final replay rechecks the
captured prerequisites instead of trusting the search.
Plate-scoped cutting plan requests now record the plate's exact state at
capture, and CuttingPlanService.Apply installs Ready, replayed proposals
for a whole scope at once:
- Any change after capture (order, pose bits, program reference or
in-place content, drawing program, lock/lead-in flags, settings,
quantity, size, quadrant or cutoff definitions) returns Stale with
nothing changed.
- Order changes without PartAdded/PartRemoved, so drawing quantities and
sentinel plates are untouched; ObservableList.Reorder exposes the same
operation and Plate.PartsReordered is raised once per changed plate.
- Regenerated parts receive owned copies of the replayed program and of
the settings captured with the request; fixed programs stay in place.
- An install failure restores every plate exactly; an observer failure
after publication is reported as a refresh error, not a rollback.
A tab trims the perimeter short of its entry, but the lead-out was still
generated from the nominal entry point. An arc lead-out therefore started
off its own radius (ExecutionMotionReader rejected it as inconsistent), and
a line lead-out ran diagonally back toward the entry.
Every lead-out style on a tabbed perimeter now leaves from the trimmed
cut's actual end, on that entity's normal, so arcs are tangent and the tab
gap stays uncut. Untabbed contours and the corner run-out rules are
unchanged. Malformed legacy output is still refused, never refit.
Red before the fix: the three tabbed arc cases threw "Arc has zero or
inconsistent radius" and the line case ended at y=5 instead of 4.8. Keeping
the entry's normal at the actual end fails the curved-perimeter case.
CutoutLatticeFill fills one closed cutout of a frame part with copies of one part, the
first piece of the cutouts-first pass. It runs Default Fill over the cutout's bounds
plus one part step on every side, shifts the lattice over a (2n+1)^2 grid of offsets
up to half a step each way, and at each offset keeps the copies whose reference point
lies in the part's inner-fit region of the inscribed cutout (part circumscribed and
grown by the spacing). The offset keeping the most copies wins; every returned pose
is then certified with NestLayoutCheck.Clears against the frame and the other copies.
20" ring, 10" round cutout, 1" squares, 0.25" spacing: 37 copies, against 25 for a
block sized to the inscribed rectangle and 32 for the unshifted lattice.
Not wired into any engine or pipeline: parts inside cutouts wait on containment-aware
cutting order. Fill can still return different, equally scored lattices between calls
for some parts, so identical results are not yet claimed.
Brings in the independently reviewed exact-contact repair (c7b4ba4,
25755a8, bd5db59): line-only part outlines keep exact analytic bounds,
so an 8x3 part fits rotated on 8.5x3.5 stock at 0.25 spacing, while
curved outlines keep their conservative padding and no validator,
spacing or fit tolerance changes. The held hole-profile and pocket
commits built on bd5db59 stay unmerged.
SQLite LIKE stops matching at an embedded NUL, so search=Alpha%00x
behaved like 'Alpha' followed by a wildcard and returned rows that do
not contain the whole search text. The shared query validation now
rejects NUL, so the server answers 400 and the client throws before
sending.
The Database-mode Open dialog no longer downloads every record. It
browses through NestBrowseSession, which sends one bounded query per
change: the filter box (300 ms debounce) searches on the server and
returns to the first page, column headers sort every match on the
server (a second click reverses), and Previous/Next page by 100 with a
range and total in the status line. A response superseded by a newer
request is cancelled and discarded, refresh steps back when the current
page was emptied, and failures clear the rows and show the error
without a modal box per keystroke.
The query accepts sort=<column>&order=asc|desc over a fixed allowlist
(saved, name, customer, status, material, dates, thickness, plate and
part counts, made by, comments, file size). Text columns sort with
NOCASE, ties break by id in the same direction so pages partition the
matches, and unknown or numeric sort values and other orders return 400.
The client sends the sort as its camelCase name.
GET /api/nests/query filters name, customer, material, made by, comments
and status (stored and display names) with an escaped LIKE parameter,
orders newest saved first with an id tie-break, and returns one page of
at most 500 records plus the total match count, read in one database
hold. Unknown, repeated or out-of-range parameters return 400.
RemoteNestRepository.QueryAsync validates the same bounds before
sending, rejects oversized pages, and reports a 404 from an older server
as a server that needs updating. GET /api/nests stays the full
enumeration used by backup, restore checks and the container smoke.
- Run as the .NET image's non-root app user (UID 1654) with root-owned
application files and an app-owned /app/data that fresh named volumes inherit.
- Add a curl HEALTHCHECK on /healthz (30s/5s/10s/3, 2s start interval), keep the
explicit 8090 URL and clear the base image's 8080 port default.
- Parameterize VERSION/SOURCE_REVISION and base images; add OCI labels with
development defaults.
- Add an image-only Compose example (required image and bind address, named
volume, cap_drop ALL, no-new-privileges) and an env template.
- Document deployment, the upload limit, scoped ownership preparation, and
checked backup/restore/upgrade functions that refuse existing destinations and
verify the metadata list and every archive hash before switching volumes.
- Extend the container smoke: image user/healthcheck/label contract, PID 1 UID,
capabilities and writable paths, Docker-reported health, near-limit and
oversized uploads, stopped-service backup restored into a second volume, and
startup failure on read-only and root-owned data mounts.
GitHub (ajisaacs/OpenNest) now owns branches, tags and pull requests.
Gitea keeps a read-only pull mirror that syncs hourly and refuses pushes,
so PRs merged on GitHub are no longer closed by the old Gitea push mirror.
Desktop Auto Nest, Console --autonest, MCP autonest_plate and the API
NestRunner all run through NestPipeline, so nothing calls the old
orchestration any more.
Delete MultiPlateNester (with MultiPlateNestOptions, MultiPlateResult,
PlateResult, PartClass and PartSortOrder), PlateOptimizer and
PlateOptimizerResult, plus their tests. The explicit-strategy contract
those tests checked now lives at PlateFillService.ResolveStrategy, which
keeps its null-means-Default, canonical-name and unknown-name tests.
CreateFiller loses its internal visibility, which only the deleted
orchestrators used.
This breaks source and binary compatibility for external callers of the
removed types; whole-job callers use NestPipeline.Run or INestingEngine.
Skip FillGrid's Step 2 overlap check only when Step 1 checked this exact
row clean and the perpendicular tiling appended zero parts, so gridResult
still holds the same Part objects in the same order and poses. The local
rowIsVerified flag is cleared by every Step 1 skip or bbox fallback, and
any nonzero append (including partial-copy parts) keeps the eager check.
Debug work assertions pin the reduced overlap work (horizontal stripe
8->4 exact calls, 4->2 preparations, 16->8 triangulations; vertical
stripe 36->18, 4->2, 38->19) and keep single-seed, Horizontal full-grid,
partial-only and PerpOnly work at base; the invalid overlapping-seed
control pins both fallback stages plus the retained exact-call total.
Three mutants (forced eager check, dropped flag, complete-rows-only)
are caught by their designated controls. Serial whole-job layout is
byte-identical to the frozen 62b5a8d0 oracle before and after.
Adds docs/nesting-engines.md: which engine suits which jobs, the old plug-in
names the registry maps, retired engines, and the rule that an engine change
lands only when it beats that engine's current benchmark result with every
layout valid. README, release and automatic-nesting docs drop the bundled
plug-in wording; AGENTS.md now places built-in engines in
OpenNest.Engine/NestingEngines and keeps only external plug-ins out of the
solution.
Capture the report target and reject whole-job nesting, open progress
windows, interactive fill and busy plate actions across every view
sharing the nest; revalidate after the save dialog, capture the snapshot
synchronously on the UI thread and render it through OpenNest.Reporting.
Windows adapter tests cover enablement, guards, cancel, success and a
write failure against an existing destination (compile-only on Linux;
Windows runtime acceptance still owed).
SQLite-backed (Microsoft.Data.Sqlite, WAL) minimal API storing NestRecord
metadata plus the .nest archive as a BLOB. Endpoints: GET/POST /api/nests,
GET /api/nests/{id}[/file], PUT /api/nests/{id}/file, PUT
/api/nests/{id}/metadata, DELETE /api/nests/{id}, GET /healthz. Multipart
upload contract matches RemoteNestRepository (metadata JSON part + file
part). Added to OpenNest.sln, builds standalone on Linux. Dockerfile
publishes to a runtime image listening on :8090 with a /app/data volume
for the SQLite file. docs/nest-storage.md documents the wire contract,
endpoints and deployment.
Full curl round trip verified manually against a running instance:
upload (server-assigned id + computed fileSize), list, get, byte-exact
file download, metadata-only update (archive unchanged), file update
(new archive persisted), 404s for unknown ids, delete, post-delete 404.
Add NestStatus (Quote/ToBeCut/HasBeenCut) plus MadeBy on Nest, written as
additive camelCase nest.json fields with PascalCase enum strings matching
the units convention. Legacy files and unknown status values fall back to
Quote. The nest info dialog gains a Status dropdown and Made By box.
Slice 2 of the nest report plan: pagination, overflow and dense-label
coverage on top of the Slice 1 one-plate library.
- NestPdfWriter: general multi-plate pagination. Summary/plate tables
continue across pages with repeated (HeadingFormat) header rows; notes
flow as an ordinary paragraph instead of a bounded cell; page headers
are wrapped and sized into the top margin; drawing areas are located
per page via DocumentRenderer.GetRenderInfoFromPage.
- ReportText: lossless pre-wrapping (MigraDoc clips an over-tall row and
lets an unbroken token overflow a narrow cell without warning), capped
table-cell line counts, plate-range compression ("1-2, 4"), and
A/B/.../AA map-grid row names.
- NestReportDiagram: label placement centers each part ID on its
PolyLabel pole (matching PlateView's LayoutPart), computed on a
placement-independent quantized copy so identical parts share one
label position. Parts whose ID cannot fit legibly at overview scale
get a lettered/numbered map grid and a zoomed, framed detail page per
crowded cell; the writer fails with plate/part/ID when even that
cannot place a label, or a plate would need more than 24 detail views.
- Tests: NestPdfLayoutTests (multi-plate totals/ranges/same-named
references, table continuation with no lost rows, long name/notes
wrapping without column overflow, mm units in all four quadrants,
dense-label detail views with hole avoidance, save/reload of the
tabbed/lead-in fixture with stale tab flags, invalid later-plate data,
late write failures against an existing destination, source
unchanged on success/failure), ReportPdf test helper (page/word/
content-stream extraction).
- docs/nest-reports.md: replace the Slice 1 one-plate limits section
with the general pagination/dense-label contract and the process-wide
PDFsharp/MigraDoc render lock.
Verification: Reporting filter 70/70; full OpenNest.Tests 2524
passed/21 skipped/0 failed; Engine 351/351; IO 77/77;
EnableWindowsTargeting=true full-solution build 0 errors; scoped
dotnet format --verify-no-changes exit 0. Preview PDFs rendered and
visually inspected (evidence: /home/aj/extracted/2026-09-29/opennest-report-slice2/).
Windows runtime, the desktop adapter and packaged-app font deployment
remain unverified (Slice 3).
Add OpenNest.Reporting (net8.0, PDFsharp-MigraDoc 6.2.4) with a detached
report snapshot and a PDF writer for an empty/demand-only job or a single
plate layout: Letter portrait summary with vector part thumbnails and a
landscape plate page with a vector sheet diagram, both with Page X of Y.
- Snapshot uses reference identity for document-local R### IDs, recounts
nested quantities from placements x copies with checked wide integers,
excludes cutoffs from accounting, and retains no live domain objects.
- Geometry keeps Cut/Display material paths, drops rapid/scribe/lead paths,
preserves native arcs, holes and intentional tab gaps, and rejects
malformed programs with plate/part identification.
- Open contours and cutoffs are stroked per contour so PDFsharp cannot join
a tab gap to the next contour.
- Bundled DejaVu Sans via a custom resolver (including MigraDoc's error
font); unsupported text fails with the field and code point.
- Layouts beyond this slice (multiple plates, page overflow, labels that
do not fit) fail with NotSupportedException before the destination is
replaced; output is rendered to a temporary sibling and moved atomically.