Files
mechanical-compiler/docs/archive/HANDOFF-2026-08-19.md
T
TheRON af196a26f5 docs: one canonical handoff, rewritten in place; F-035
Handoff documents were additive. HANDOFF-2026-08-19 opened by saying the
18 AUG document still applied in full and added to it. After ten sessions
a new assistant would face ten documents to read in date order and diff
mentally to work out what is currently true. That cost grows every
session and none of it is necessary.

docs/HANDOFF.md is now the only handoff, rewritten in place each session.
It is state, not a log. The dated ones move to docs/archive/ and stop
being required reading. It is standalone: everything still true from both
is carried forward.

Section 1 is invocation, stated as facts rather than demonstrated in
examples. That is the other half of the problem. runuser appeared only
inside example commands, so it could be learned by pattern matching but
not by reading, which fails exactly when an assistant composes a command
from scratch. That is what happened, and it is F-035: su cannot run as a
nologin service user, both commands returned the same message before
touching anything, and the output read as a broken repository when the
tree was clean and the suite passed. The F-027 class again.

Also stated as facts: bash tools/ not ./tools/, all repository operations
as mechcomp, Gitea SSH on 42022, pct push then chown, explicit timeouts,
journalctl not /var/log, systemd-run for long jobs, and assert the guest
is running before interpreting any pct exec result.

Not done: the same facts should be cross referenced from PROCESS.md. I no
longer had that file in view and would not patch a document I cannot see.
2026-08-19 12:33:50 -05:00

11 KiB
Raw Blame History

HANDOFF — 2026-08-19

For the assistant taking over. Read this, then the documents it points to.

The 18 AUG handoff still applies in full: the operator's constraint, the working discipline, and everything in §7 "Things that will bite you". This document adds what one session of actual porting established. It does not replace it.


1. Read these first, in this order

All in mechanical-compiler/docs/ at commit 86a47c3.

# File Why
1 HANDOFF-2026-08-18.md Still current on process, constraints, and hazards
2 PROCESS.md How work is done here. Read before issuing any command.
3 FAILURES.md 34 entries now. F-033 and F-034 are from this session.
4 STAGING-STATE.md What is true on srv-b

ENVIRONMENT.md and ROADMAP.md as needed. The port work is development mode, not infrastructure mode — see PROCESS.md §2.


2. Where the port stands

The entire shared layer is ported, tested and pushed. Six slices, each committed separately with its own tests, each proven by mutation before landing.

Module From What it holds
geom/primitives.py sb-geom.scad Vectors, GEO and MEMBER, sleeve and cavity paths, exact polyline distance, corner-radius derivation, the monotone solver
geom/rounding.py BOSL2 92d697c2 round_corners, _circlecorner, arc, segs, deduplicate, path_merge_collinear
geom/region.py BOSL2 regions Shapely-backed booleans, nesting-parity decomposition, area, simplicity, hull, mitred offset, cleaning
geom/join.py sb-join.scad Butt joints, hull caps, fillets, the derived bore, ring fit, section assembly
geom/report.py sb-report.scad Checks-as-values, metrics, the five universal checks, ProfileRejected, report formatting
geom/core.py sb-core.scad The PROFILE record, failure representation, centred assembly
geom/arrangements.py sb-profiles.scad The three N-generic arrangements: ring, spokes, fins

230 unit tests, none of which touch the oracle. The 236 oracle acceptance tests still skip: mechcomp.profiles.build does not exist yet.

What remains

Read strap-beam-3x.scad and strap-beam-4x.scad. Port the eleven catalogue profiles and the build() entry point into src/mechcomp/profiles/. Then the oracle runs for the first time.

The port's public entry point is mechcomp.profiles, not mechcomp.geom. conftest.py does importorskip("mechcomp.profiles") and requires a build attribute on it. The geometry lives under mechcomp.geom; build() and ProfileRejected must be exported from mechcomp.profiles.


3. Things established this session that are expensive to rediscover

Report values are rounded to six significant figures

The oracle records what OpenSCAD's echo printed, which is C's %g at default precision — not full-precision geometry. echo_num() in geom/report.py does this; echo_vec() handles vectors, which reach the oracle as strings like '[20.5209, 20.5209, 20.5209]'.

This is load-bearing, not cosmetic. VOLUME_MM3 ends in _MM3, so test_oracle.py compares it at the lengths tolerance of 1e-4 rather than the areas tolerance of 1e-3. Volume is section area times a 100 mm length, so an unrounded port reporting 13557.402 against a recorded 13557.4 fails by twenty times the tolerance while being geometrically correct.

Verified: across all 113 accepted cases the recorded volume equals the rounded area times length to within 3.6e-12.

Angles are degrees, and the arbitrary constants are contract

OpenSCAD trigonometry is in degrees. The port keeps degrees throughout with explicit cos_d/sin_d/tan_d helpers so every expression matches its source line. The 44 solver iterations, the 0.999 and 0.98 scale factors, the 0.05/179.95 degree cutoffs and the 1e9 sentinel are reproduced exactly. They are not tidy numbers to improve on; they produced the frozen values.

You can probe the reference directly

The pinned toolchain image is in CT 100 and OpenSCAD will echo whatever you ask it. This turned F-034 from a guess into a measurement in about four exchanges, and it will settle any later disagreement the same way.

Mount the tree read-only and keep the probe on a separate writable mount. Docker runs as root and a read-write mount against a mechcomp-owned tree is the F-033 hazard. Note that a nested single-file mount inside a read-only mount fails with EXIT=125 — put the probe in its own directory instead.

pct exec 100 -- bash -c 'mkdir -p /tmp/probe && cat > /tmp/probe/p.scad << "EOF"
include </repo/legacy/openscad/lib/sb-core.scad>
$fn = 48;
g = sb_geo(15.875, 0.508, 1, 0.25, 1.20, 1.20, 1.20, 1.20);
echo(str("PROBE=", sb_area([sb_sleeve_path(sb_member(0,0,0), g)])));
EOF
docker run --rm -v /var/www/mechcomp:/repo:ro -v /tmp/probe:/probe -w /probe \
  mechcomp/reference-toolchain:8.0.0 \
  openscad -o /tmp/o.stl --export-format=asciistl p.scad 2>&1 | grep -E "PROBE|ERROR"'

EXIT=1 is normal — STL export fails on 2D geometry. The echoes still arrive. The harvester itself exports to /dev/null for exactly this reason: the export only forces evaluation.

The oracle's parameter defaults

strap-beam-3x.scad lines 65–135. Notably y_junction_round_mm = 1.50, three_fin_junction_round_mm = 2.00, ring_corner_radius_mm = 2.00, facets = 48, material_density_g_cm3 = 1.24. All 123 cases run at facets = 48; nothing overrides it.

Environment facts

Shapely 2.1.2 on GEOS 3.13.1. Shapely is in requirements-base.txt, the 2D path's own dependency set. No CAD kernel is installed in CT 100 — cadquery, OCP and build123d are all absent, though requirements-cad.txt says it is installed by default. That is the strictest possible environment for the 2D path and will matter when STL and STEP export starts.

Git identity is now set --local in the clone as TheRON <webmaster@kane-il.us>, matching the two most recent commits. It must stay local: mechcomp's home is the working tree (F-029), so --global would write into the repository.


4. Verified against the reference

Three-Fin at the oracle's defaults reproduces every recorded value exactly, including SECTION_AREA_MM2 146.787, VOLUME_MM3 14678.7 and MASS_G 18.2016. That case exercises butt joints, fillet_junctions, fillet_pair, fillet_concave, round_corners, the derived bore and the fin solver.

Y reproduces every value except section area and its two derivatives.

Exact across both: SPOKE_RADIUS_MM 9.1713, SPOKE_WEB_MM 1.2, FIN_CORE_SIDE_MM 13.4028, FIN_SETBACK_MM 3.77783, FIN_JUNCTION_WEB_MM 2.29919, FIN_BORE_SIDE_MM 7.5, RING_CORNER_R_MAX_MM 2.87663, the ring edge vector, and both envelope dimensions on both profiles.

The numerical machinery is right. Where the port disagrees, it is F-034.


5. F-034, and the decision it will force

Read the entry in full. The short version:

Arc segment counts are ceil((90 - half_angle)/180 * $fn), and that expression is frequently an exact integer — a 60° half-angle at $fn = 48 gives exactly 8. Floating point delivers it as 8.000000000000004 or 7.999999999999998 depending on how the corner was reached. The half-angles come from the merged polygon, whose vertices come from the boolean kernel, and BOSL2's clipper and GEOS disagree in the last bit.

Do not try to fix this. It was tried and reverted. Rounding the count before the ceiling makes the three symmetric pairs identical and fixes Y exactly — and breaks Three-Fin, which had been matching to the digit, because Three-Fin has the same asymmetry and the oracle records it.

Two consequences:

Some recorded values encode float noise rather than geometry. A port that is geometrically more correct than the reference will fail those cases.

The tolerance model may need revisiting, and that is CIVICVS's call. Any case affected fails on VOLUME_MM3 and MASS_G long before it fails on SECTION_AREA_MM2, because volume is checked at 1e-4 while being area times 100 mm.

Do not raise this until build() exists and all 123 cases have run. The number of affected cases is unknown and is the only thing that should drive the decision. It may be one case. Bring the count and a recommendation, then stop.


6. How this session worked, and what to keep

Read the source, do not recall it. BOSL2 was fetched at the pinned commit and read directly — round_corners, _circlecorner, arc, segs, deduplicate, is_collinear, vector_angle. Every one of those had a detail that mattered and that recollection would have got subtly wrong.

Mutation-test every suite before landing it. Break the code deliberately and confirm the tests notice. This found real gaps five times in six slices:

  • nothing exercised BOSL2's three-point floor on blunt corners
  • nothing distinguished on-boundary from outside in the nesting probe
  • the area guard alone accepts a bore that has turned inside out
  • the ring fit really does have a spurious lower branch below scale 1
  • nothing asserted that clean_region removed anything

It also caught one malformed mutation of mine — cutting the cavities twice is idempotent — which is worth knowing: a surviving mutation is sometimes a bad mutation, not a test gap. Check which before writing a test.

Deliver by upload, not by paste. PROCESS.md §3. A sixty-line heredoc with em-dashes and nested code fences was mangled by the browser terminal. Everything since went as a tarball with a stated checksum, file list, and what it overwrites. That has not failed once.

One command group per message, and wait. Every command group in this session was read-only first where the answer was not certain.


7. State

Gitea main at 86a47c3. CT 100 clean and matching. Suite: 230 passed, 236 skipped.

86a47c3  rounding: record F-034, arc segment counts tip on the last bit
8d79431  geom: port sb-core and sb-profiles, the N-generic arrangements
b101fe6  geom: port sb-report, validation and report formatting
ebf02d6  geom: port sb-join, the junction and envelope strategies
38ea024  geom: Shapely-backed region layer
545eee7  geom: port BOSL2 round_corners and path cleanup
dfd02a4  geom: port the pure-geometry half of sb-geom
b67cc12  verify.sh: reach the restore on the diff branch
1d3eed0  Handover push

The toolchain gate passed at the start of this session: all 123 cases regenerate byte-identically inside the pinned image, 113 accepted and 10 rejected, the only diff being frozen and the hash containing it. The oracle is a stable target.

verify.sh --full exits 1 by design on any diff, and the frozen date guarantees one. That is a pass, not a fault. Its restore path was broken and is now fixed but still unexercised — see F-033. The next --full run proves it.