Skip to content

v0.6.0: compaction-aware refill turns recovered plate into fewer sheets - #11

Open
Steel-tech wants to merge 23 commits into
mainfrom
claude/best-in-class-loop-t5ynby
Open

v0.6.0: compaction-aware refill turns recovered plate into fewer sheets#11
Steel-tech wants to merge 23 commits into
mainfrom
claude/best-in-class-loop-t5ynby

Conversation

@Steel-tech

Copy link
Copy Markdown
Contributor

Implements docs/plans/2026-08-24-001-feat-compaction-aware-refill-plan.md (included in this PR). v0.5.0's verified true-shape compaction (#10) recovered plate area, but it ran only after every part was already assigned to a plate — so the recovered space enlarged remnant candidates while the sheet count and stranded parts stayed exactly what bounding-box packing decided. This PR closes that loop: recovered plate now becomes fewer purchased sheets.

What changed

Refill inside the packing loop — when a part fails to insert on every compatible open plate, eligible plates (all irregular parts outlined) that received new placements since their last attempt are compacted behind the same independent verification gate as v0.5.0, and the insertion is retried against the rebuilt free rectangles — before a new sheet is opened, and before the part is declared stock_exhausted. Any successful insertion re-marks the plate dirty, and the v0.5.0 end-of-run pass still tightens final layouts.

One shared gate helper — the compact → verify → accept/restore/disqualify block is now _attempt_plate_compaction, used by both the mid-pack and end-of-run paths. compaction.recovered_in and compaction.passes accumulate across attempts on the same plate; accepted reflects whether the current layout comes from an accepted compaction. No schema changes.

Safety argument

  • Refilled parts land only in free rectangles rebuilt by subtracting every compacted bounding box plus spacing, so a refilled part keeps at least kerf+gap clearance to every bounding box — and therefore to every true profile — on the plate. Mixed compacted/refilled plates still pass the full true-shape pairwise verification.
  • A rejected compaction restores the plate, records the non-blocking COMPACTION_REJECTED finding, permanently disqualifies that plate for the run, and packing proceeds exactly as the bounding-box flow would have (proved by the mid-pack corruption test).
  • Never worse: with compaction enabled a run uses at most as many plates and strands at most as many parts as with it disabled — compacting before opening a sheet can only add placement options (asserted across fixtures).
  • Bounded work: a plate is re-attempted only when dirty (new placements since last attempt), and each attempt keeps v0.5.0's pass/step bounds.
  • Burn-DXF eligibility, review posture, metrics definitions, remnant non-certification: unchanged. Algorithm version bumped (maxrects-bssf-u3-tsc2), which reaches the configuration hash as before; normalized input hashes untouched.

Tests (5 new; 211 total green)

  • Sheet reduction: the L/Z interlock plus a 4×6 filler needs 2 sheets boxed, 1 sheet with refill, with the filler landing exactly one clearance off the compacted Z's right face (x = 11.5).
  • Stranded-part rescue: with qty 1 stock the boxed run is blocked (stock_exhausted); with refill it completes as review_required with nothing unplaced.
  • Never-worse guarantee across fixtures, determinism, and the mid-pack rejection fallback (corrupt compactor → plate disqualified → identical-to-boxed layout on 2 sheets, one warning, verification still verified).

npm run release:check fully green (tests, ruff, privacy, pack, provenance); full-render smoke green.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MquyTzVAPKpV1V8Vt3XxnJ


Generated by Claude Code

claude added 23 commits August 23, 2026 06:31
Add steel-cutlist, a deterministic 1D bar-nesting engine for long
products (beams, HSS, angles, pipe) that packs required member lengths
onto purchasable mill lengths with explicit kerf and end-trim
allowances.

- Strategy portfolio per designation+grade group: mixed-stock greedy
  plus each single-stock-length restriction, ranked by unplaced count,
  total stock length, known cost, then bar count.
- Independent post-placement verification (overcommitment, material
  mismatch, duplicate or missing instances) gates publication.
- Weights resolve from explicit plf or the bundled AISC database;
  unknown weights surface as warnings, never silent zeros.
- Publishes manifested runs: bar diagrams, purchase summary, drop
  candidates, rfq_linear.json handoff, and a cutting_list.csv emitted
  only for verified fully placed runs.
- Register cutlist_partial/cutlist_verified package statuses and add a
  versioned cutlist-result schema.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MquyTzVAPKpV1V8Vt3XxnJ
Wire the steel-cutlist engine into the review-gated estimate pipeline
and carry its results into the draft RFQ workbook.

- Member items (designation + length, no plate geometry) now optimize
  onto configurable mill lengths (--mill-lengths-ft, default 40,50,60)
  with cut-list kerf, end-trim, and drop-threshold settings.
- Cut-list validation errors, verifier findings, and unplaced members
  gate the pipeline exactly like nest blockers; a member longer than
  every mill length blocks the run as cutlist_partial.
- Ready runs publish cutlist-result.json, rfq-linear.json, a verified
  cutting_list.csv, and reference bar diagrams alongside the nest
  artifacts.
- The RFQ compiler accepts a validated linear handoff (--linear on the
  standalone CLI) and renders a LINEAR STOCK / CUT-LIST REFERENCE
  section, with staleness checks against the estimate identity.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MquyTzVAPKpV1V8Vt3XxnJ
- Add CHANGELOG.md with dated release history from the git record.
- README: cut-list section, skill table row, workflow diagram, prompts.
- steel-estimate and steel-rfq SKILL.md document the cut-list stage and
  the --linear handoff; steel-nest description defers long products to
  steel-cutlist to keep trigger boundaries unambiguous.
- Bump package and runtime versions to 0.3.0 and ship CHANGELOG.md in
  the npm package.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MquyTzVAPKpV1V8Vt3XxnJ
- A negative member quantity now records its finding and publishes a
  structured blocked run instead of crashing instance expansion.
- Members without a grade are excluded from pipeline cut-list
  optimization with an explicit warning, restoring the pre-cutlist
  review_required outcome instead of hard-blocking the estimate.
- Blocked runs report verification.status 'not_run' rather than
  claiming a verification that never executed; schema updated.
- rfq_linear rows carry their own stock group's utilization instead of
  the job-wide blend.
- --mill-lengths-ft rejects non-finite entries as a usage error and no
  longer double-wraps its own failure message.
- Display formatting is decimal-exact, so cutting_list.csv preserves
  sixteenth-inch lengths instead of rounding at six significant digits.
- Deduplicate is_sha256, missing_optional_modules, and
  normalize_designation into the shared pi_steel package; nest, takeoff,
  and cutlist now share one copy.
- Hoist the placed-instance set out of the per-unplaced-row loop.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MquyTzVAPKpV1V8Vt3XxnJ
Extend the estimate-package contract with stock_form 'linear' entries so
vendor lengths and on-hand sticks flow into cut-list optimization.

- Schema: additive linearStock variant (designation, grade, length_ft,
  quantity, optional unlimited for purchasable supply, and the same
  hash-bound reviewer_confirmation as plate stock).
- Validation: unlimited on-hand linear stock is a blocker; new
  eligible_on_hand_linear_stock and linear_purchasable_stock helpers
  mirror the plate gating.
- Engine: stock entries carry stock_kind; on-hand stock must be finite
  and carries no cost basis, reports as cost_basis 'on_hand', and never
  degrades cost-known status. The portfolio now ranks by purchased
  length first (on-hand consumption is free), then known cost,
  purchased bars, and total length, so confirmed sticks eliminate
  purchases whenever they genuinely can.
- Pipeline: declared purchasable linear lengths replace the default
  mill lengths for their designation+grade group; confirmed on-hand
  sticks join as finite inventory; plate nesting skips linear entries.
- Purchase summaries, rfq_linear rows, bar reports, and the text report
  label on_hand rows explicitly.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MquyTzVAPKpV1V8Vt3XxnJ
The full-render smoke caught two problems with the cut-list additions:

- cutting_list.csv was published with geometry_verified readiness even
  when the overall run was review_required (e.g. irregular plate
  geometry), breaking the invariant that non-ready runs carry no
  verified-authority artifacts. The verified cutting list now requires
  the whole run to be ready, mirroring burn-DXF suppression, with a
  base-tier regression assertion.
- The workbook PDF page-count assertions assumed one total page, but
  the linear reference section grows the sheet and pagination varies by
  LibreOffice version (24.2 renders one page where CI renders two).
  The smoke test now asserts the actual contract: uniform page width
  (one page wide), all words within their own page bounds,
  document-order section ordering across pages, page-1 logo bounds,
  and a sanity cap on total pages.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MquyTzVAPKpV1V8Vt3XxnJ
- Reject non-boolean 'unlimited' values with an invalid_unlimited_flag
  finding instead of coercing truthy strings into unlimited supply,
  which could understate purchase requirements.
- Include stock_kind in generated stock identities so anonymous on-hand
  and purchasable rows of the same designation, grade, and length no
  longer collide as duplicates.
- README: list channels and tube among supported cut-list member types,
  matching the changelog.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MquyTzVAPKpV1V8Vt3XxnJ
- Small designation+grade groups (up to 12 pieces) now run a
  deterministic branch-and-bound search over complete bar assignments,
  seeded and pruned by the portfolio rank with a fixed node budget; it
  only ever replaces the greedy plan with a strictly better one. On the
  classic best-fit-decreasing failure (5,5,4,4,3,3,3,3 onto capacity
  10) it finds the 3-bar optimum where the greedy needs 4.
- The ranking objective is now economically honest: when every stock
  entry in a group carries a known cost basis, lowest purchase cost
  decides before purchased length (buying cheaper beats buying
  shorter); unpriced groups keep the least-purchased-length objective.
  Without prices, the exact search finds a 230 ft plan on the reference
  case where the greedy bought 240 ft.
- output-contract.md documents cutlist artifacts and cutlist_partial;
  the estimate-package example gains member items and vendor linear
  stock; version 0.3.1.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MquyTzVAPKpV1V8Vt3XxnJ
The branch-and-bound now branches over leaving each piece unplaced
(stock_exhausted) in addition to every placement, so when finite stock
cannot hold everything it finds the partial plan stranding the fewest
members instead of falling back to the greedy's weaker cut. Pruning
compares (unplaced, primary objective) lexicographically — both grow
monotonically along a path — and the never-worse guarantee is
unchanged. Adds docstrings to the search internals and a regression
test where the greedy strands three pieces but the optimum strands two.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MquyTzVAPKpV1V8Vt3XxnJ
Two capacity-10 bars with pieces 8,6,4,4,4: the exact search strands
only the 8 where the greedy stranded two 4s.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MquyTzVAPKpV1V8Vt3XxnJ
Irregular plate parts may now carry geometry.outline — a simple-polygon
vertex list in the part's local frame whose bounding box matches the
declared size.

- Exact shoelace areas and weights replace hand-declared estimates,
  reported as the new outline_exact approximation status.
- Holes are verified against the true profile: a hole inside the
  bounding box but in a notch now blocks instead of passing.
- Layouts and reference DXFs draw the real outline (with the bounding
  box dotted for context); placement stays by bounding box and burn-DXF
  suppression for irregular parts is unchanged.
- Outline validation (>=3 finite vertices, non-crossing edges, positive
  area, bbox spanning 0..width x 0..height, declared-area consistency)
  runs in both the canonical validator and the direct nesting engine;
  polygon helpers live in the shared geometry module.
- Schemas: estimate-package geometry.outline, nest-result placement
  outline and outline_exact enum. Version 0.4.0.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MquyTzVAPKpV1V8Vt3XxnJ
Address review findings on the outline feature:

- polygon_is_simple now rejects repeated vertices and ANY contact
  between non-adjacent edges (endpoint touches and collinear overlaps
  included, not just proper crossings), so reused-boundary and pinched
  rings can no longer claim exact areas or reach reference DXFs.
- Fallback source identities include the outline when one is present,
  so two legacy parts sharing a name and bounding box but different
  profiles no longer collide as duplicates; identities for parts
  without outlines stay byte-stable.
- The nest-result placement outline schema now permits only an empty
  list or three-plus vertex pairs, matching what the engine emits.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MquyTzVAPKpV1V8Vt3XxnJ
Design for the first placement-quality milestone on outlined irregular
parts: deterministic left-then-down profile compaction after bounding-box
packing, gated by an independent polygon-clearance verifier that discards
any compacted plate it cannot prove. Burn eligibility and review gating
are unchanged; full NFP nesting stays out of scope.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MquyTzVAPKpV1V8Vt3XxnJ
U1 of the true-shape compaction plan: exact polygon-polygon minimum
distance (zero on any contact or overlap, bounding-box early exit per
edge pair), overlap detection, rectangle-as-polygon adapter, and
usable-area containment, with unit tests covering touching, nested,
diagonal, and notch-adjacent cases. No behavior change to placement or
verification yet.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MquyTzVAPKpV1V8Vt3XxnJ
Review caught that clearance along a slide is non-monotonic for concave
profiles, so binary search over the offset is unsound. The compaction
slide is a continuous motion: fixed-step scan from the current position
and stop one step before the first clearance violation. U2 gains a
required concave alternating-clearance fixture proving the scan cannot
tunnel through a blocking neighbor into a later clear interval.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MquyTzVAPKpV1V8Vt3XxnJ
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MquyTzVAPKpV1V8Vt3XxnJ
After MaxRects bounding-box packing, plates whose irregular parts all
carry validated outlines run a deterministic left-then-down compaction
pass: each placement slides in fixed 1/32-in scan-to-first-contact steps
until its true profile (not its box) reaches the kerf-plus-gap clearance,
so complementary profiles interlock and recover plate. Clearance along a
slide is not monotonic for concave profiles, so the scan stops one step
before the first blocking offset and never tunnels into a deeper clear
interval; bounding-box gaps only skip provably clear offsets.

Every compacted plate must pass verify_true_shape_placements, an
independent verifier that rebuilds profiles from the published placement
data (own rotation transform, pairwise polygon distance, boundary
containment). Any failure discards compaction for that plate with a
non-blocking COMPACTION_REJECTED finding and keeps the proven
bounding-box layout; result-level verification dispatches accepted
plates to the same true-shape contract. Remnant candidates are rebuilt
from the compacted bounding boxes and stay rectangle-based and
uncertified. Burn-DXF eligibility and the review-required posture for
irregular parts are unchanged.

Plate reports record compaction {ran, accepted, recovered_in, passes},
metrics add true_shape_utilization_pct (exact profile area) where every
irregular part is outlined, and the compaction configuration joins the
configuration hash while leaving normalized input hashes untouched.
Enabled by default in the direct engine and the estimate pipeline;
--no-compact-outlines opts out. Includes the mandatory concave
alternating-clearance fixture proving first-contact stopping, an
adversarial corrupt-compactor gate test, determinism and remnant-honesty
tests, and additive nest-result schema updates.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MquyTzVAPKpV1V8Vt3XxnJ
The hand-written version string went stale on release bumps (0.4.0
survived into the 0.5.0 release); reference the manifest and changelog
instead so the README can no longer disagree with the package.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MquyTzVAPKpV1V8Vt3XxnJ
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MquyTzVAPKpV1V8Vt3XxnJ
v0.5.0's verified compaction ran only after every part was already
assigned to a plate, so recovered space enlarged remnants but never
changed the purchase list. Compaction now also runs inside the packing
loop: when a part fails to fit on every open plate, eligible plates that
changed since their last attempt are compacted behind the same
independent verification gate and the insertion is retried against the
rebuilt free rectangles before a new sheet is opened or the part is
declared unplaced.

Refilled parts keep full bounding-box clearance to everything on the
plate because they only ever land in free rectangles rebuilt from the
compacted bounding boxes plus spacing. A rejected compaction restores
the plate, records the non-blocking COMPACTION_REJECTED finding,
permanently disqualifies the plate for the run, and packing proceeds
exactly as the bounding-box flow would. A plate is re-attempted only
when it received new placements since its last attempt, so the work
stays bounded, and with compaction enabled a run never uses more plates
or strands more parts than with it disabled.

The compact-verify-gate block is now one helper shared by the mid-pack
and end-of-run paths; recovered_in and passes accumulate across attempts
on the same plate with no schema changes. New tests cover the sheet
reduction (two sheets to one), rescuing a stock_exhausted part
(blocked to review_required), the never-worse guarantee, determinism,
and the mid-pack rejection fallback.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MquyTzVAPKpV1V8Vt3XxnJ
@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 42 minutes.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 490b95b4-7730-4cf6-ac53-9693e1f16ad1

📥 Commits

Reviewing files that changed from the base of the PR and between 3007214 and 15d4094.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • README.md
  • docs/plans/2026-08-24-001-feat-compaction-aware-refill-plan.md
  • package.json
  • pyproject.toml
  • skills/steel-nest/SKILL.md
  • skills/steel-nest/scripts/nest.py
  • tests/test_compaction.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Steel-tech

Copy link
Copy Markdown
Contributor Author

🤖 Lab Code Review (draft opinion)

  • a/skills/steel-nest/scripts/nest.py:1034: plate["compaction_dirty"] = True should be set only on successful placement to avoid unnecessary compaction attempts; move inside the if placement: block.
  • a/skills/steel-nest/scripts/nest.py:1056-1083: The refill loop retries insertion on the same plate after compaction without re-checking compatibility; add if not compatible(plate["stock"], unit): continue after compaction to prevent invalid attempts.
  • a/skills/steel-nest/scripts/nest.py:993: plate["compaction"] initialization in open_plate duplicates later reset in the end-of-run loop; remove the redundant block to avoid confusion.
  • a/skills/steel-nest/scripts/nest.py:1094-1104: End-of-run compaction uses _attempt_plate_compaction but ignores its return value; the helper already handles disqualification and findings, so the call is correct but the prior manual compaction block removal left dead code—verify no remnants remain (already addressed in diff).
  • a/skills/steel-nest/scripts/nest.py:339: _compaction_eligible returns bool(irregular) and all(...); if irregular is empty, it returns False, which is correct, but the function name implies eligibility for compaction—consider renaming to _has_outlined_irregular_parts for clarity (non-critical).

Only one critical finding is required; the rest are minor. The most severe issue is the missing compatibility check post-compaction in the refill loop, which could lead to invalid placements. However, since the diff shows the loop skips incompatible plates before compaction and the compaction helper does not alter stock compatibility, this is actually safe. Re-evaluating: the refill loop iterates over plates, checks compatible(plate["stock"], unit) at the start, and only proceeds if true. Compaction does not change plate["stock"], so re-checking is unnecessary. Thus, no correctness bug exists.

The only actionable issue is the premature compaction_dirty flag: setting it on every commit (line 1034) marks the plate dirty even if no placement occurred, wasting compaction cycles. This violates R5 (bounded work) by potentially increasing attempts beyond necessity.

File: a/skills/steel-nest/scripts/nest.py, line 1034
Fix: Move plate["compaction_dirty"] = True inside the if placement: block, directly after commit(plate, unit, placement).

All other points are either safe or non-critical. The diff is otherwise correct.

Final answer: one finding.

a/skills/steel-nest/scripts/nest.py:1034: Move plate["compaction_dirty"] = True inside the if placement: block to avoid unnecessary compaction attempts.

Copy link
Copy Markdown
Contributor Author

Verified the lab reviewer's draft opinion against the code — its single actionable finding does not hold, and no change is needed:

"Move plate["compaction_dirty"] = True inside the if placement: block" — the flag is already set exactly and only when a placement is committed. It is the last line of commit() (nest.py:1037), and commit(plate, unit, placement) is invoked at three call sites (lines 1056, 1079, 1089), each guarded by if placement:. There is no code path that sets the dirty flag without a successful placement, so no extra compaction attempts occur and the R5 bounded-work property is intact — a plate is attempted only after receiving a new placement, as the plan specifies.

The comment's remaining points it withdrew itself (correctly: the compatibility check before compaction is sufficient since compaction never changes plate["stock"]). One correction to its third point: open_plate's compaction-dict initialization is not duplicated — the end-of-run loop no longer resets that dict; it only calls the shared _attempt_plate_compaction, which accumulates into it.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants