placement: the reconstruct ladder, reseat, orbits, and instruments that stopped lying - #623
placement: the reconstruct ladder, reseat, orbits, and instruments that stopped lying#623edgehero wants to merge 29 commits into
Conversation
KiCad writes 0 into min_hole_to_hole / min_copper_edge_clearance for 'not configured', and board_floor_knobs already treats a non-positive declared value as unset -- but each of the four routing mains kept a private copy of the raw read: board_constraint(...) if ... is not None else <default>, an is-not-None test with no positivity guard. Eight sites (route.py, route_diff.py, route_planes.py, repair_planes.py), so one declared floor got two answers depending on which half of the loop asked. Measured, on a board declaring min_copper_edge_clearance: 0.0 -- placement, floorplan and render used 0.55 [fixed default] while route.py used a REAL 0.0 (printing 'using the board min_copper_edge_clearance 0.0mm', announcing a declaration that was not one) and the plane tools dropped their 0.5 inset to 0.0 on the very comment saying they keep it 'only when the board declares no edge rule of its own'. The GUI plane tab already guarded > 1e-9, so this also closes a CLI/GUI divergence. list_nets gains resolve_cli_floor (one resolver, _FLOOR_SOURCES provenance) and the declared-0 guard in board_floor_knobs; the four mains route through it. tests/test_board_floors.py pins the shared-resolver wiring by AST on all four mains and runs each of them live on a declared-0.0 board (this file re-creates the checks from the test upstream removed with drandyhaas#579, minus the removed feature's blocks, under a new name). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011o1wnFiHT1HEBnuVyJdjgs
A board can declare min_hole_clearance in its .kicad_pro and KiCad grades NPTH-to-copper at it -- but the router priced every NPTH obstacle at the flat NPTH_TO_TRACK_CLEARANCE constant, and check_drc never consulted the key at all. Measured: a real, kicad-cli-confirmed 0.0237 mm routing-introduced violation; and neo6502 ships 3 NPTH at 0.2126/0.2263 mm against an authored 0.25, graded clean here because nothing read the declaration. obstacle_map gains resolve_hole_clearance (board-first, cached, raise-only via max-wrap) consumed by add_drill_hole_obstacles and plane_obstacle_builder through the new routing_config.hole_clearance field; check_drc gains --hole-clearance with the project auto-read as a third max() term; and min_hole_clearance joins fix_kicad_drc_settings.FAB_FLOOR_KEYS so the relaxation disclosure stops being blind to it. tests/test_npth_seed_floor.py fails when the stamp is mutated back to the constant. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011o1wnFiHT1HEBnuVyJdjgs
The underpad escape spaced its drills at the flat HOLE_TO_HOLE_CLEARANCE constant in all three comparison sites, whatever the board declared. Measured on ulx3s (declares min_hole_to_hole 0.4): drill gap 0.3657 mm before, 0.6000 mm after; a control board with no declaration is byte-identical. The floor is read board-first and fab-wrapped (tighten-only), after clamp_stats so the disclosure names what was used. Also documents manage_vias' structural self-blindness where the next reader will find it: both guards that gate a via append iterate pcb_data.vias only, and vias_to_add is appended to but never read back, so two vias placed in one call are never checked against each other. Stated as UNMEASURED -- filed as an issue rather than patched here, because the honest fix wants a measured fine-pitch reproduction first. tests/test_bga_fanout_underpad.py: 3 of 5 checks fail on the unpatched tree. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011o1wnFiHT1HEBnuVyJdjgs
… laid The escape candidate was clearance-tested against the board's PRE-EXISTING copper only: run_output_conflict did not exist, so a candidate was never tested against the vias and stubs THIS run had already emitted -- including cross-side -- and a reused via was priced as if it added a new drill and new copper (adds_via=True), losing reuse ties it should win. The pinned regression: on kicad_files/routed_output.kicad_pcb U2 the fix moves the outcome 29/13/31 -> 28/12/30 (escapes/vias/stubs). The original headline symptom (39/46 rejected escapes) is honestly stated as NOT reproduced -- the structural gap plus the regression pin are the evidence. The hole-to-hole floor is board-first with the fab wrap, disclosed once. tests/test_qfn_underpad.py carries the mutation checks: reverting either the cross-side half or the pricing flips named checks red. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011o1wnFiHT1HEBnuVyJdjgs
…id not _effective_geometry_floor took board_val if board_val is not None with no positivity guard, while the plane-edge helper beside it already required > 1e-9 -- so a board writing 0 for 'not configured' drilled at hole-to-hole 0.1 in the GUI where the CLI resolved 0.2, same board, same settings, under --fab-overrides. Two lines close the divergence. tests/gui_parity/test_geometry_floor_leak.py pins it under real KiCad python; .gui-parity-checked gains a scoped addendum naming this PR's one GUI-relevant change and the parity evidence (copper sets identical, 1307 segments, 144 vias). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011o1wnFiHT1HEBnuVyJdjgs
…n as utf8 A slow test and a hung one cost different things, and both rendered as FAIL. run_all now prints TIME with its own summary count and a note that a timeout is not a failed assertion; children run with -X utf8 so a degree sign in a test's output is not a crash of the runner's reader thread. run_utils gains the check/evidence helpers the run-era suites share. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011o1wnFiHT1HEBnuVyJdjgs
argparse reads --nets -12V as a flag and dies with a usage error; a lap was lost to exactly this. cli_banner.install_dash_hint hooks the parser error to say the one thing the user needs: write --nets=-12V. Adopted where net names arrive: the routing mains. tests/test_run14_dash_net_hint.py pins the hint text and that a real parse still fails cleanly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011o1wnFiHT1HEBnuVyJdjgs
…success falsely Two families land together because they share the same four files and the second emits through the first. Deadline (krt_deadline.py, new): an external kill leaves no summary and no exit code of the tool's own, so a harness cannot tell killed-mid-write from crashed. --deadline arms a cooperative budget: arm/stamp/seal, exit 7, a guaranteed JSON_SUMMARY with complete:false on the way out. The wiring is one closure into the cancel_check/progress_callback parameters batch_route, batch_route_diff_pairs, create_plane and repair_planes already have; a reserve band keeps clock for the end-of-run reconnect so a cancelled run ships a real, gated board. place_route_loop treats exit 7 as a partial result with a summary, not a failed step. False success, per file, each measured: - route.py: a --nets list where every name missed wrote a passthrough copy and exited 0 (run 11 lost two laps to boards KiCad refuses to load); CRLF in net names stripped; the summary stamps scope and names ripped_open_uncounted, so the LAST JSON_SUMMARY of a log (often the reconciliation subset's) cannot masquerade as the run's. - route_diff.py: zero matched pairs exited 0; a partial pair with BOTH members incomplete counted 'successful' (engine-side -- the GUI reads the successful field raw). - route_planes.py: 'GND:B.Cu' was accepted as a layer name and KiCad refused the written file -- engine-side validation now; the pad tally reaches the JSON summary, recording geometric_failed WITHOUT flipping status on it (under pours-first those are expected at the plane step; only failed_pads flips status). - repair_planes.py: emits through krt_deadline so a partial repair says so instead of exiting 0 with a contradicting summary. Pinned by tests/test_deadline.py and tests/test_run12_tools.py in the skill PR that consumes these exits; the refusal behaviors are exercised by tests/test_board_floors.py's live main runs in this PR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011o1wnFiHT1HEBnuVyJdjgs
…9/70 graded as 70/70 A zone naming a layer the board does not have was counted as copper, so a disconnected board graded one net better than it was. The census now cross-checks against board_info.copper_layers; a --nets scope that matches nothing exits 2 instead of reporting a clean empty set; and when the oracle never ran, the output says so instead of implying it agreed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011o1wnFiHT1HEBnuVyJdjgs
The support loop credited a via with support from copper on ANY layer it touches; KiCad's via_dangling wants support on at least two. Rebuilt as per-layer sets (a behavior superset of the old loop) and a new dangling-via category, measured against kicad-cli on the board that exposed it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011o1wnFiHT1HEBnuVyJdjgs
A pair with both members incomplete was still counted toward the custody tally it had just been removed from. Ten guarded lines, exercised by the existing tests/test_diff_pair_custody.py suite. (The drandyhaas#549 rule-pair classification test also covers this area but depends on the skill-side board_score, so its layout repair rides with that PR.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011o1wnFiHT1HEBnuVyJdjgs
Each chain step routes to a fresh output path, so the memo of an earlier kicad-cli timeout never matched and the chain re-burned the full timeout at every step -- measured, 240 s per step. The memo now keys on the fill-cost signature of the board content. KICAD_CLI (env) is consulted FIRST instead of after the unix candidates, and Windows discovery covers the installs the old glob missed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011o1wnFiHT1HEBnuVyJdjgs
…pped' merge_summaries took the LAST oracle_check, so a subset pass that skipped its oracle erased an earlier pass's real KiCad disagreement. Oracle news now merges worst-first, and complete:false is sticky across merges. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011o1wnFiHT1HEBnuVyJdjgs
…documented None refill_islands documents returning None and one caller called .items() on it anyway -- the misattributed NoneType error. Guarded; the refill honors a krt_deadline budget when one is armed (try/except import, degrades to the stock timeout when the module is absent). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011o1wnFiHT1HEBnuVyJdjgs
…, 3-5mm from the region find_open_space_point answers 'emptiest cell within 5mm of the anchor centroid' against an obstacle map that excludes the plane net's own copper, so clearance saturates and the strict first-maximum tie-break picks the far corner of the search box in raw grid order; _try_route_between_regions then seeds A* with a point nowhere near the region it should join. Seeding on region material (centroid-ranked validity probe), a COINCIDENT rule for one-point routes, and union-find over merged fill islands take neo6502's GND from 15 of 41 region joins dead-by-construction to 0. Two healthy boards re-run byte-identical; validity=None preserves the old path verbatim. Pinned by tests/test_region_join_nearest_pair.py. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011o1wnFiHT1HEBnuVyJdjgs
…r showed The caption strip clipped at the image edge, so the oob count and a fab blocker fell off the render silently. Captions wrap instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011o1wnFiHT1HEBnuVyJdjgs
…wn scope rescue_by_width tallied the whole board where it meant its own work, and delivered_mm is kept as the minimum of the honest candidates. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011o1wnFiHT1HEBnuVyJdjgs
…anything with no pins The reconstruct ladder gets its missing rungs: fit_family_orbits learns per-group orbit families (with a guard matrix so a two-member "family" cannot vote itself in), the hole-pattern fit learns mid-edge slots and positional at-seat instead of corners only (the run-8 finding: a slot model that knew only corners could not seat a mid-edge connector), and straddle-vs-displaced classification stops charging a part that never left its seat. Two honesty fixes ride the same rungs: A2 prunes unevidenced moves -- an equal gate tuple is no tie-break in an unevidenced move's favour -- and A3 takes a damage_witnesses pad-centre census so "damaged" is measured, not inferred. The frame tier keys on the netlist rather than pin_count == 0, which had promoted every pinless part to structural. Straddling files ride here per earliest-theme: legality.py carries format_oob_clause/oob_pad_refs (also the instrument-honesty basis), pose_score.py carries extra_locked_refs (--lock plumbing) plus the cancel_check hook the deadline commit threads. place_reconstruct's reseat stage calls seeder.reseat_scope, which lands in the next commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011o1wnFiHT1HEBnuVyJdjgs
…ng at 40 mm seeder.reseat_scope is the lift-and-reseat engine: pick the scope up off the board, then seat it from scratch at its net centroids, behind a 3-conjunct gate so it never fires on a placement a nudge can fix. Repair searches start from the part's CURRENT pose, which carries no information once the part is tens of millimetres from where it belongs -- measured, cost grows sharply with the displacement cap while a re-seat stays flat. place_seed grows --reseat, and place_reconstruct's reseat stage (previous commit) previews candidates in honest order instead of the order the search happened to visit. Also here: seeder feeds --lock globs into the two entry points that re-parse the board and build their own state -- --lock was honoured by five reconstruct stages and silently ignored by the two that move the most parts. Straddles noted: seeder.py and place_seed.py also carry their share of the deadline threading (a later commit wires the rest); floorplan.py rides here for empty_intent, and brings EDGE_BAND_SANITY_MM -- emit-intent stops laundering 160 mm of damage into an "edge band" declaration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011o1wnFiHT1HEBnuVyJdjgs
… bounding box, not the outline render_placement now measures off-outline pad copper against the true Edge.Cuts outline, per pad -- the skill's top-priority gate (run 10: 11 such parts produced ALL 13 unrouted nets), previously approximated by a bounding box that blessed copper hanging off a notch. It grows a findings-in-words panel, worst-N --view crops, --pair named diffs, --gate, and net_lists missed-pattern disclosure so a glob that matched nothing says so. The checkers stop lying by omission: check_assembly keys its oracle memo on fill cost and reports buildable/verdict/locked_contacts in JSON with board-first floors; check_reachability gets exit-code discipline (2 = not-answerable, 1 reserved for CAGED, REF.PAD dot-resolution); check_floorplan --require-rules refuses the vacuous pass. legality's oob_pad_refs basis landed with the reconstruct commit; placement_state's pile check goes through-pads so locked piles are no longer exempt; lock_advisor corroborates orbits and demotes straddles; recovery adds home_curve/per_part_median and dirty_net_ids. fab_tiers rides here for the same theme: the escalation warning now says that --via-size cannot prevent the clamp (raising it fires the clamp more readily), instead of letting the user reach for the one lever that cannot help. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011o1wnFiHT1HEBnuVyJdjgs
…the sweep Cooperative deadline threading (krt_deadline, exit 7) through every placement search: quench, the portfolio generator, place_optimize and place_portfolio all take a cancel_check and stop at a granularity of one candidate, instead of running to completion under a harness that already gave up. pose_score's rank_poses (hook landed with the reconstruct commit) documents the trap that motivated it: --limit truncates the sorted RESULT while every candidate has already paid a full cost evaluation -- a 30 mm / 0.25 mm ring set is 231,392 candidates and --limit 12 evaluated all of them. seeder.py and place_seed.py carry their share of this threading in the reseat commit (earliest-theme rule). place_portfolio's _replay_argv/_render subprocess strings were hand-checked against the upstream drandyhaas#522 layout: py_placer/place_portfolio.py and py_tools/render_placement.py under ROOT. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011o1wnFiHT1HEBnuVyJdjgs
…sured to be converge.py grows the machinery a measured stop needs: a lens grammar with a lens-vs-score contradiction check (DONE-EXHAUSTED can never sit beside a FAIL lens), commensurability so laps of different halves are not compared as if they were the same experiment, --exhausted with a mandatory reason, --score-file, FAIL_COMPATIBLE_STOPS, and a verdict with 4 distinct exit codes so a caller can tell refused from failed from done. check_complete.py (new, repo root -- precedent check_rigid_consistency.py; the loop driver prints it by that path 8x and D1 pins "runs as printed") is the single is-this-board-actually-finished gate: DRC at the board's own floor, connectivity, and --authored-from against a frozen baseline, exit 5 meaning NOT complete. Its floor keys come from fix_kicad_drc_settings' FAB_FLOOR_KEYS (PR "board floors"). The suites ride along: test_converge, test_run9_verdict, test_run9_check_complete, and test_broken_worklist -- the last pins board_score's broken-net WORK LIST (which net, how many pieces, where the stranded pads sit); a count with nothing behind it left `broken` at 14 for two iterations while `unrouted` went 4 -> 0 on per-net detail. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011o1wnFiHT1HEBnuVyJdjgs
…to GIF in silence Two channels in the film chain said things the artifact contradicted. Pillow's GIF writer collapses runs of byte-identical frames -- card holds and the end hold are literal repeats -- so animate_route's "wrote N frames" was the encoder's input, not the file: 377 printed, 348 delivered, reconciled only by counting the GIF by hand. The writer now counts the artifact itself, and make_film labels its own number "assembled" and defers to the writer's. The other silence: a missing imageio-ffmpeg made _write_mp4 return False without a word, producing a .gif where the caller asked for .mp4 -- the only trace a `wrote ...` line with a different extension. The fallback now says so, with the pip line to fix it. test_film_composition also stops asserting "not a crash" without checking for one: exit 2 with a Traceback passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011o1wnFiHT1HEBnuVyJdjgs
…audit is a SyntaxError this repairs The perturbed-corpus rig, plus the repair of tests/stress/fence_audit.py at upstream tip: a drandyhaas#522 sys.path shim was inserted mid-expression (:57-60), so the file has been a SyntaxError since -- ast.parse now passes. Three channels carried ground truth nobody meant to send. (1) fence_audit compared POSES and never opened copper: a subject staged from a routed board keeps the human routing, so track ends go on marking where each part belongs -- run 14 shipped exactly that and the audit said CLEAN. It now checks copper, baseline-relative, with an empty DEFAULT_ALLOW and byte-reads. (2) perturb's writer rewrote (at) with six decimals for exactly the moved footprints: 24 six-decimal forms of 235 on run 14's subject, 0 on untouched boards -- the moved set was readable off the text while the docstring said it was "disclosed nowhere". Every part now goes through one formatter, and control_out moves the control board plus the perturb record OUTSIDE the work dir, destination always printed. (3) stage_blind checks the dose against the material floor and redraws instead of printing "damage applied" on a dose clipped to one grid step. New tools: arm_report (the recovery table as a tool: pad-space home, frozen member list, degeneracy guard), qualify_subject ("is this a usable perturbed subject" in seconds), strip_copper_only, calibrate_congestion_ratio (its first result refuted the shipped 0.25 threshold -- the P-close gate now reports instead of gating). grade_final/qualify_subject regex fixes, perturb_batch _truth staging, and the RUNBOOK's "choosing a subject before staging" section ride along. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011o1wnFiHT1HEBnuVyJdjgs
The doctrine section the placement work keeps re-learning: the objective is a board that ROUTES -- zero unrouted and zero broken -- not restoring parts to the poses they had before. The perturbed rig grades on recovery/home, which measure distance to the original pose; run 10 scored recovery -0.0014 and home 0/30 on a run that took a board from NOT BUILDABLE to buildable. Lead with blocking, keep recovery as a diagnostic, treat the human original as a benchmark to approach. Off-outline pad copper is the top-priority defect (run 10: 11 such parts produced all 13 unrouted nets); scope searches to the refs the gate names (2 freed parts: 63 s; whole-board sweeps: 10+ min without touching them); prefer re-seating over nudging at tens of mm. Whole-file restore also carries the Windows/Git Bash MSYS2_ARG_CONV_EXCL section (net names are /-prefixed and get silently rewritten into Windows paths: 61 nets passed, 4 survived) -- same provenance, noted rather than split out. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011o1wnFiHT1HEBnuVyJdjgs
…he focus test pins the refusal-free behavior Two test-only adaptations re-extracted from integration: - test_place_reconstruct.py: test_ladder_order and test_reseat_measures_with_SCOPE_FILTERED_edge_bands did in-method sys.path.insert(0, ROOT) + flat imports (place_reconstruct, kicad_parser, placement, pose_score) -- pre-drandyhaas#522 paths. Both errored ModuleNotFoundError on the split layout. They now add the same three "# placement split" path lines the file's control-board test already carries. Was 2 errors + 3 skips; now 0 errors (1 pass, 4 skips -- the absent wk/b2 corpus). - test_431_render_placement.py: test_focus_without_summary_json_warns asserted the run-4 G7 refusal ("--focus emits nothing without --summary-json") that render_placement deliberately removed -- --focus now clusters legality findings into pocket panels instead of refusing. Renamed to test_focus_without_summary_json_clusters_legality_findings and pinned the new path: exit 0, no warning, pocket siblings written. Suite was 1 failed / 27 passed; now 28 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011o1wnFiHT1HEBnuVyJdjgs
…locking 0)
Measured twice in run 19: SW17+SW34+REF_PUCK_R all at (128.399, 61.001)
and the gate said `buildable (blocking 0)` -- the pair currency counts pad
INTERSECTIONS, and the rotated pads happened to interleave. A stack of
parts at one origin is unbuildable whatever the pads do, so it is now its
own blocking channel.
The grouping and the exoneration reuse placement_state's prior art
(assess_placement: round(coord,3) buckets, per-side partition where a
drilled part is on both sides, all-marker side-groups exonerated) rather
than re-deriving them; a bucket is a finding only when it holds >= 2
suspect NON-marker parts, so mouse-bites and graphics markers never flag.
One predicate variable `not_buildable` now feeds all three re-derivation
sites (verdict, JSON `buildable`, exit code) -- three inline copies of
`blocking or locked_contact` is how a new channel reaches two of them and
silently misses the third. JSON gains `coincident_origin_groups`
([{point, refs}], never fake N*(N-1)/2 pair entries) and scalar
`coincident_origins`; the findings block prints each group as refs @ (x, y).
loop_driver's L2 gate reads only the five L2_CHECKS keys, so the
`verdict`/`buildable` flip is what gates; no driver change. A run that
hits it legitimately uses --accept-residue with the residue named.
New script test tests/test_assembly_coincident_stack.py pins the run-19
shape exactly: a stack whose pads don't intersect (blocking 0) exits 4
NOT BUILDABLE with the group naming both refs; the unmodified board stays
exit 0; a marker pair moved onto one point is exonerated by the channel.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011o1wnFiHT1HEBnuVyJdjgs
Measured in run 19 cycle 1: every conjunct of LegalityContext.pads_ok is relative to seed_baseline, and on a pile the seed pair carries huge base.pad with pad_overlap and stack both True -- so ANY smaller intersection passed the gate, producing the SW23/SW28<->U2 blocker pairs on the shipped board. Fix at the single choke point every consumer routes through (seed_baseline, feeding both pads_ok and swap_pads_ok): __init__ buckets every part's SEED pose by round(coord, 3); a bucket of >= 3 parts is degenerate (the pile signature -- deliberately above 2, so run 18's legitimate two-part by-design under-module R1<->U2 overlaps KEEP their baseline license), and any pair touching a degenerate ref gets ZERO_SHORTFALL: the pose must be absolutely clean toward that neighbor, which is exactly the semantics a pile deserves. Second gate, same shape: portfolio.score_candidate compared pad_conflict_pairs against the seed baseline's count; above 50 the baseline is degenerate, so the candidate is compared against 0 instead and the reason string says so. New unit test tests/test_pile_baseline_license.py pins both sides of the >= 3 line: the piled part's baseline is ZERO_SHORTFALL and pads_ok REFUSES an intersecting pose (mutation-checked: with the guard emptied the same pose is accepted), while the 2-part designed overlap keeps its license. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011o1wnFiHT1HEBnuVyJdjgs
…undecidable subject Two riders on the recovery rig: - qualify_subject.py wrote --json with a bare open(); run 19 pointed it into a not-yet-created directory and lost the qualification JSON after all the draws had been paid for. os.makedirs(dirname or '.', exist_ok=True) before the write. - RUNBOOK's "Choosing a subject" section qualified the BOARD but said nothing about the damage KIND, and run 18 staged an undecidable one: on a grid-homed board, swap/translate recovery is byte-identical to truth BY CONSTRUCTION, so a perfect result and a truth-file LEAK produce the same bytes and the fence cannot tell them apart (run 18's undecidable LEAK verdict). New subsection 1b: prefer pile/scatter on grid-homed boards, or pre-declare the secondary tell before the damage is drawn; references KINDS at placement/perturb.py:56. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011o1wnFiHT1HEBnuVyJdjgs
|
Three commits added after opening, all from the run 19 exercise of exactly this PR's code (the blind piled board written up in discussion #118):
The larger placement-engine gaps the same run exposed are deliberately NOT patched here — filed as #628, #629, #630 with the measurements. Full learnings: https://github.com/edgehero/KiCadRoutingTools/blob/run19-assets/LEARNINGS.md |
|
Merged as 12 of 13 commits, pushed to What landedCherry-picked with
I checked the Reading the diffFor anyone finding this later: the PR page showed 29 commits / +13,661, but the real delta against What did not land:
|
Stacked on #616 — this branch includes #616's commits; the placement work is the 9 commits from "reconstruct: the ladder trusted any equal-cost move…" onward. After #616 merges this rebases to show only its own diff.
The placement engine work from perturbed-corpus runs 8–17, re-rooted onto the #522 layout (
py_placer/,py_tools/), plus the stress rig that grades it.Engine
pin_count == 0; off-board damage witnesses come from a pad-centre census.place_seed --reseatlifts a far-from-home part and re-seats it on the fitted family orbit instead of nudging it, behind a 3-conjunct gate.check_assemblypublishesbuildable/verdict/locked_contactsand keys its oracle memo on fill cost, not a path every step renames;check_reachabilityreserves exit 1 for CAGED and exits 2 when the question was not answerable;check_floorplan --require-rulesrefuses a vacuous pass; the emit-intent edge band is capped so 160 mm of damage cannot launder itself into "by the edge, by design".--deadline(exit 7, summary guaranteed) through thekrt_deadlinemodule routing: boards graded at constants they contradict, and tools that reported success falsely #616 introduces.--exhausted <half>declarations with mandatory reasons,--score-file(a 49 kB score no longer dies on argv limits), and a four-code verdict;check_completeis the close-out instrument that fails closed — orphan stubs, weird copper, pad overlaps, and fab floors compared against the AUTHORED project so a board cannot grade itself against floors it rewrote.Stress rig (
tests/stress/)stage_blind (the dose is checked against the material floor and redrawn — "damage applied" on a clipped dose voids the recovery half before it starts), fence_audit (content-based truth fence; this commit also repairs the file at tip, which is currently a SyntaxError — a #522 shim was inserted mid-expression at line 58), arm_report, qualify_subject ("is this board a usable perturbed subject" in seconds), strip_copper_only, calibrate_congestion_ratio (its first result refuted the threshold it was built to calibrate; the gate now reports instead of gating), and
_truth/staging throughout.Live validation
This exact engine + rig closed a blind perturbed board end to end yesterday: piantor staged blind (kind sealed), placement half repaired a ±(72.6, 6.4) mm two-group exchange to all-gates-green with an independent verifier PASS, routing half took it to blocking 0 / DRC 0 at authored floors. The run's report and ledger are reproducible from the rig in this PR.
Testing
Targeted suites green on this branch: place_seed 37/37, reconstruct (0 errors; corpus-dependent checks skip), slot-fit, stage-blind dose 6/6, fence-and-channels 8/8, arm_report/verdict/check_complete suites, converge 27, render_placement 28, reachability. Five tests in this PR's set are deliberately red until the skill PR stacks on top (they pin
board_score/loop-driver shares that land there) — each is named in its commit. Fullrun_allon the stack: 294 passed with every red pre-existing at tip on our test box (tip baseline measured; see #615 for why a fresh tip clone cannot run at all).