v0.4.1
18
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cdde394bd8 |
composer: /m/stl, a bounded export route
The route lives under the gated prefix from IDENTITY-CONTRACT.md section 5, so gating it later is a proxy change and not a code change. It ships OPEN, because /m/ is not yet gated and no membership system exists to gate it -- recorded in section 8 of that document and cited in the module docstring, so a successor reads it as a decision rather than an oversight. model/stl with a Content-Disposition filename carrying the input_id. The browser saves it without JavaScript assembling a blob on the happy path. The route rebuilds from the query parameters rather than caching what /api/build just made: the file is a function of the URL, and the composer goes on holding no state between requests. Persistence is its own piece of work and should not arrive here by accident. MECHCOMP_MAX_EXPORT_MM bounds it, defaulting to 3048 -- one ten-foot member. The ceiling exists because this endpoint is unauthenticated on a public name and member_length_ft is an unbounded number whose value alone decides the size of the computation and the download. /api/build has the same exposure with a constant-size answer; export is where bounding becomes worth it. 3048 is not a claim that longer members are wrong. A hundred-foot member is a real artifact and nobody prints one in a piece -- it gets sectioned. The export ceiling and the member catalogue answer different questions and conflating them would be the mistake. The limit is configuration rather than a query parameter because a limit the caller can raise is not a limit, and an unparseable or non-positive setting falls back to the default rather than disabling the bound: a typo must not leave a limit that exists in the documentation and nowhere else. Over the ceiling is a 413 naming the length, the limit and the key, and it refuses rather than truncates. A truncated export would ship a 3048 mm file whose design record describes a 30480 mm member -- the same silent wrongness the length_view control was added to prevent, arriving by a different door. ProfileRejected is a 422 with the reason intact, an unknown family a 404, and only a genuinely unexpected exception a 500. Refusals are text/plain so the download control can show them and a person who hit the URL by hand can read it. A rejection is the compiler working. Two things changed while building rather than after. The type coercion was extracted from build_payload into typed_overrides and is now shared: had the export coerced differently from the view, the downloaded file would not be the part on screen, and neither would have looked wrong on its own. And the button re-sends the query the current drawing came from rather than reading the controls when pressed, so a half-typed number in a text box cannot export something that was never displayed. The page's JavaScript was parsed with node --check before landing. The download handler rebalanced braces around the data.ok block, which is exactly the kind of edit that compiles as a Python string and breaks in a browser. 16 tests. Mutation-proven: never applying the ceiling fails 4, truncating instead of refusing fails 4, a bad environment value disabling the bound fails 5, the export using its own coercion fails 1, a rejection becoming a 500 fails 2. Restoration verified by checksum, PYTHONDONTWRITEBYTECODE=1 throughout. One narrow margin worth recording: the shared-coercion guarantee rests on a single test, test_the_export_is_the_part_the_view_shows. It is the only thing that failed under M4. Deleting it would silently remove the only check that the file matches the drawing. Suite 643 passed. |
||
|
|
0545b79674 |
stl: export a member as a sealed mesh
prism_mesh takes a region and a length, not a Result. A member, its support, or the support alone are then three calls with different regions rather than three special cases inside one function -- support is a designed part of an artifact where no printable orientation exists, not something a slicer adds, and flattening everything into one mesh now would be expensive to undo later. Named and documented as a MEMBER exporter. ROADMAP section 2 has three artifact classes: members are prismatic and exist, nodes are non-prismatic and do not. A prism sweep will never produce a node and no amount of extending this should be attempted. A module called stl.py would invite exactly that. No CAD kernel. Caps are a constrained Delaunay triangulation of the section with its holes, walls are a quad strip per ring. That is why STL costs no new dependency while STEP would. Winding is forced, never assumed. region_parts returns each part outer-clockwise with holes counter-clockwise because region_area depends on that to make holes subtract; a prism swept along +Z needs the opposite, since traversing the outer boundary counter-clockwise puts material on the left and the wall normal on the right, which is outward. Both are reversed here. The identifiers in the 80-byte header are PROVENANCE, NOT A CHECKSUM. Principle 4: mesh bytes are not reproducible across toolchain versions, so two exports carrying the same build_id may differ byte for byte and both be correct -- a GEOS release can move a triangulation without moving the geometry, which is the F-034 mechanism applied to tessellation. The docstring says so where someone diffing two exports will find it. Facet normals are written as zeros: every consumer recomputes from vertex order, and a stored normal would be a second source of truth for which way a face points. A section failing is_region_simple is refused rather than exported. Its own docstring names the case: an outline that measures perfectly, touches itself at a point, and cannot be sealed. Emitting it would produce a file that opens in a slicer and prints wrong. member_stl defaults to the report's LENGTH_MM, the value the record's VOLUME_MM3 and MASS_G were computed against. Any other sweep makes the record describe a different object than the file beside it. 61 tests across all eleven profiles in both families, four properties each, every negative assertion with a positive control. Edge pairing catches a missing wall and inconsistent winding; Euler catches a filled or invented hole; area overlap catches a bore paved over; volume against region_area times length catches inverted normals and a wrong length together. Two test defects found by mutation and fixed before landing, both worth recording because both passed while being wrong. The hole check originally tested triangle centroids. Ignoring holes at triangulation time slipped through it -- two triangles can pave a bore with both centroids outside it. Replaced with intersection area, which catches it. The sweep-length test built at the family defaults, where preview_length_mm is 100, so a mutation hardcoding 100.0 passed the entire suite: the assertion was comparing a value against the constant that had replaced it. Now parametrised over 37.5 mm and 3048 mm, and proven against hardcoded mutations at both values, each caught by the other case. Mutation results against the real sections: holes not reversed fails 23, exterior-only triangulation fails 45, outer ring not forced CCW fails 25, simplicity precondition removed fails exactly 1 -- the refusal test written for it. Restoration verified by checksum rather than by a green run, after a same-length mutation plus a cached .pyc produced a false clean reading earlier in the session. PYTHONDONTWRITEBYTECODE=1 throughout, per HANDOFF section 8. Suite 627 passed. None of the 566 moved. |
||
|
|
ecac644204 |
composer: enumerated parameters, and length_view becomes reachable
length_view was in both families' defaults and in neither COMMON_GROUPS, so the Full Length branch of model_length_mm could not be reached from the composer. Every model was a 100 mm preview whatever member_length_ft said. Found while specifying STL export: the sweep must equal model_length_mm(p), the value already published as LENGTH_MM, or the record's VOLUME_MM3 and MASS_G describe a different object than the file beside them. Adding it to the list would have been one line and the wrong fix. The type coercion falls through to the raw string for anything that is not a bool, int or float, and model_length_mm compares against the literal "Full Length" and silently falls back to preview for anything else -- the reference behaved the same way, so the port is right to keep it. A free-text box would have replaced an unreachable control with a silent one: "Full Length " with a trailing space builds a 100 mm model and says nothing. So ENUM_PARAMS declares which parameters take one of a fixed set of values. The browser renders a select instead of an input, and a value outside the set is dropped before the build rather than passed through -- the family default stands, which is a valid model. The guard sits before the coercion, not after, where it would be dead code. The next enumerated parameter needs no new machinery. Deliberately not mirrored as a check in the families. geometry_checks is shared with the oracle and the reference accepted any string here; adding a check there would put the 123 frozen cases at risk to fix a user-interface problem. The guard belongs in the composer, which is not oracle-bearing. 19 assertions, mutation-proven: removing the guard fails 8 of them. Six are the parametrised bad values. The other two assert the mechanism rather than its effect -- that the string never enters values at all, and that the fallback reaches input_id. Without them, a pass-through that happened to be ignored downstream would look identical to a working fallback, and a later change to model_length_mm would turn a passing test into a defect somewhere else. length_view is a build parameter and stays in both hashes, unlike the author. Two members of different lengths are different designs, and a test asserts it. Suite 566 passed, of which 19 are new. None of the 547 moved. |
||
|
|
48d566574e |
design record: an authorship field, recorded and in neither id
The person is identified by an email address. A handle may be chosen later and does not replace it: the handle is a display name, the address is the id. Excluded from input_id and build_id. input_id answers whether two records describe the same part as specified, so two people specifying the same part must collide there; hashing the author would make identical parts claim to be different designs. The guarantee is structural rather than careful -- the ids are computed from the resolved parameter set and the author never enters it. That exclusion narrows the one-way door it was scheduled against but does not close it. A record regenerated later with the author filled in keeps its input_id, but build_id covers the code revision and the Shapely and GEOS versions, and boolean results on near-degenerate geometry can shift between GEOS releases -- the F-034 mechanism. Regenerate after a GEOS bump and you have attribution under a different build identity beside a printed part nobody can tie to either. Recoverable, not free. Priority order in HANDOFF section 4 is reversed accordingly: this before STL export, so the first coupon off the printer carries its author. The rendered line carries its own limit -- "(self-declared, unverified)". Nothing checks that the address belongs to whoever typed it, and a bare email in a field called author reads as identity to someone finding the file in five years. The record is meant to outlive everyone present, so it states what it knows. When an authenticated identity exists the qualifier changes and the distinction stays legible. Same discipline as Provenance.describe(), a separate type: provenance is about measurement, and design_record imports nothing from mechcomp, which is what keeps a failure in the record off the build path. parse() gets its own branch because authorship is neither input nor output and inherits neither rule. It recovers the address and never the verification: a text file cannot attest to its own verification, so reading a verified record back as self-declared understates the claim, which is the only safe direction. REVISION stays 8.0.0. The field reaches no geometry. 547 passed. The 529 are unchanged and no oracle case moved. The eighteen new assertions were mutation-tested with PYTHONDONTWRITEBYTECODE=1 and caches cleared between runs: leaking the author into input_canonical, deafening the parser, trusting the adjective, dropping the qualifier, and turning an empty author into an empty claim each fail the suite. |
||
|
|
14514b0bac |
web: the composer, served behind the existing proxy chain
A browser page with controls on one side and the cross section on the other. Three separately classed layers: printed material, the cavities the stock passes through, and the stock drawn inside its cavity so the fit gap is visible. Under it the report and the design record, so the identity of a part is visible while tuning rather than discovered afterwards. Standard library only, no framework, no build step, no new dependency. Binding comes from /etc/mechcomp/mechcomp.env, which already declared 10.20.0.10:8770. An earlier draft invented a port on 0.0.0.0, which would have placed a second unproxied plaintext copy beside the proxied one. The fallback with no env file is loopback, never every interface: behind a proxy, binding too narrowly fails loudly as a 502 and too widely fails silently as an open service. Controls are filtered to the selected profile, with no catch all group. A knob that moves nothing is worse than an absent one. Renderer tests assert what an eye cannot: the vertical flip happens exactly once, holes render as holes, and the cavity is larger than the stock inside it. Seven mutations on the renderer, all caught. |
||
|
|
e921cacf0e |
design record: every build emits one
Result gains a record field with a default. Nothing existing moves and the report dict is untouched, so all 123 oracle cases are unmoved at 506 passed. A rejected profile still raises before this point and gets no record, which is correct because there is no model to preserve. The record learns the stock and fit from the Geo the build actually used, so it describes what was built rather than what was asked for. Those agree today and the record will keep saying so if they ever stop. code_revision resolves once per process from MECHCOMP_REVISION, then git rev-parse, then unknown. Never a guess: unknown tells you the part may not be reproducible, while a plausible wrong sha would send someone to the wrong commit. A dirty tree is reported as such, since a model built from uncommitted edits cannot be regenerated from a revision alone. Caching matters because assemble runs 123 times in the suite and would otherwise shell out to git twice per build. The end to end test builds a part, renders its record, parses it back from nothing but the text, rebuilds from the parsed parameters, and asserts the report is identical key for key across five profiles. If that fails the record is a description rather than a recipe. Method correction. The mutation harness had been letting pytest write bytecode, and a write landing inside one mtime tick could be masked by a stale pyc. That made one real defect appear to survive and, more importantly, means every mutation result reported earlier in this work was optimistic by an unknown amount. Rerun with bytecode disabled, all nine mutations caught. The earlier sets are worth rerunning under the corrected method. |
||
|
|
d2827c23b3 |
design record: what a model was made from, sufficient to regenerate it
A parametric compiler is meant to be tuned: print, measure the print, adjust, print again. That loop also destroys the value of everything already printed, because once a clearance moves the parts on the bench become unidentifiable and unreproducible. Configurability and reproducibility conflict unless something records what each part was made from. The record carries the fully resolved parameter set at full precision, the stock and fit lines with whatever provenance exists, the code revision and the toolchain. Plain text, one fact per line, readable without this software. It is deliberately not built from the report. geom.report rounds to six significant figures because that is what echo printed and the oracle records what was printed. A part cannot be regenerated from SECTION_AREA_MM2 equals 135.574. Inputs reproduce, outputs confirm: reported values are carried separately as verification, to be checked with a caliper against the actual part. Two identities. input_id covers family, profile and parameters, the design intent. build_id adds code revision and toolchain. Shapely and GEOS are in build_id because boolean results on near degenerate geometry can shift between GEOS releases, which is the F-034 mechanism, and a record omitting them could not explain why the same numbers produced a different part. Timestamp, note and verification values are excluded from both. Not on the build path. No profile imports it, so the frozen oracle cannot move. Twelve mutations tested and caught, including two defects of mine: the record carrying only overrides rather than the resolved set, and the toolchain never reaching build_id. |
||
|
|
70787ea17d |
stock: records delegates to the descriptor, and the descriptor stops gating
geom.records no longer computes the section, cavity or laminae. It calls mechcomp.stock, so the shape of a piece of stock is defined once. All 123 oracle cases unmoved: 482 passed. stock.py also becomes a leaf module, ending an import cycle with geom that resolved only by accident of ordering.
Three defects in
|
||
|
|
f5651d3a62 |
stock: name the COTS descriptor; the strap becomes the first catalogue entry
The compiler describes off-the-shelf hardware and generates the printed part that encloses, interfaces with, or augments it. The pallet strap is not the subject of the library, it is the first entry, and it was inlined into Geo rather than described. STOCK.md states what every entry must declare: designation, section, nominal versus actual, fit, stock tolerance, provenance. Geo conflates three things. Width, thickness and count are the stock. Clearance is the fit, a property of the joint. The wall thicknesses are the printed part policy. This commit names the first two and leaves Geo untouched, so no profile imports the new module and the frozen oracle cannot move. test_stock.py proves faithfulness by exact float equality against geom.records across 36 parameter combinations, 5 placements and 3 face modes. Mutation tested before landing: reversed vertex order, halved clearance, dropped lamina offset and a naive round cavity are each caught. Round cavities are circumscribed rather than inscribed. A vertices on circle polygon lies inside the nominal diameter and bites into it by r times one minus cos 180 over n, about 9.6 micron at 9 mm radius and 48 facets, which is enough to stop a press fit. Conduit is deliberately absent from the catalogue until a measurement or citation exists. |
||
|
|
7b3182c0b9 |
port: the 3x and 4x profile catalogues; build() now exists
Completes the port. mechcomp.profiles.build and ProfileRejected are live,
so all 123 oracle cases execute rather than skip.
_common.py assembly pipeline shared by both families, and the eight
base checks both generators declare identically
four_x.py five profiles, all direct library calls with N=4
three_x.py six profiles; four are library calls with N=3, while
A Frame and T are built from join-layer primitives
because they are specific arrangements rather than
instances of a family
466 tests: 436 pass, 30 fail. THE 30 FAILURES ARE EXPECTED. Do not treat a
red `make test` here as a broken port.
All 30 are test_accepted_case_matches_oracle, and all 30 breach on
SECTION_AREA_MM2 alone -- VOLUME_MM3 and MASS_G are that value times 100
and times density, so each case has one underlying discrepancy reported
three times. Worst relative error 2.24e-05.
Everything that positions material is exact. ENVELOPE_X_MM, ENVELOPE_Y_MM,
MIN_WALL_ACTUAL_MM and every profile extra (AF_*, FIN_*, SPOKE_*, RING_*,
T_*) pass at 1e-4 mm in all 123 cases. Every count is exact. All ten
rejections fire correctly, including the bespoke A Frame and T paths.
Cause is F-034, now characterised precisely: the port is exact and the
reference is noisy. _circlecorner computes (90-angle)/180*segs(), which at
$fn=48 on a 90-degree corner is exactly 12. The port lands on 12.0 every
time and takes ceil 12; OpenSCAD's arithmetic lands a hair under 45 degrees
at some corners, pushing the value fractionally above 12 and the ceiling to
13. Measured on Rectangle: reference envelope 50 vertices, port 48.
There is nothing to correct on this side. Both boundaries sit within
0.0027 mm (4x) and 0.0043 mm (3x) of a true arc and within ~0.4 um of each
other -- far inside the project's 0.01 mm criterion. The tests fail because
VOLUME_MM3 is compared at 1e-4 absolute against a magnitude near 20000,
demanding 5e-9 relative agreement from discretised geometry.
Resolving that means changing the comparison policy in test_oracle.py, not
the oracle: the tolerance block is inside the hashed document and editing
it would break test_integrity_hash by design. Deferred pending a decision.
See docs/PRECISION.md for what the 0.01 mm criterion means and what this
compiler does not do.
|
||
|
|
86a47c3c06 |
rounding: record F-034, arc segment counts tip on the last bit
No behaviour change. Comments and a FAILURES entry. The Y profile builds a section area of 135.572973 against a recorded 135.574, out by 0.001027, while every other value for that case matches exactly. Three-Fin matches on everything including area. Isolated by probing the reference inside the pinned toolchain image. The hull cap is identical to nine figures, the bare union is identical, and a single filleted pair is identical at 30 vertices and 125.699057 mm2. The difference appears only when the three filleted pairs are combined, and the three pairs, which are related by 120 degree symmetry and must be identical, come back as 125.699057, 125.698029, 125.699057. Cause proven. The arc segment count is a ceiling on a quantity that is frequently an exact integer: a 60 degree half-angle at $fn=48 gives exactly 8. Floating point delivers that as 8.000000000000004 on one corner and 7.999999999999998 on the others, so one corner gets a whole extra segment. The half-angles come from the merged polygon, whose vertices come from the boolean kernel, and BOSL2 clipper and GEOS disagree in the last bit. Reproduced unguarded because the reference is unguarded. Rounding the count before the ceiling was implemented and reverted: it makes the three pairs identical and fixes Y exactly, and breaks Three-Fin, which had been matching to the digit. Three-Fin has the same asymmetry and the oracle records it. BOSL2 tipped the same way GEOS does there and the opposite way on Y. Two consequences for the project rather than the code. Some recorded values encode float noise rather than geometry, so a port that is geometrically more correct than the reference will fail those cases. And the tolerance model may need revisiting: VOLUME_MM3 is compared at the lengths tolerance of 1e-4 despite being area times 100 mm, so a 1e-3 area difference becomes a 1e-1 volume difference. MASS_G is derived the same way. No decision yet. The number of affected cases is unknown and is the only thing that should drive it, and that is not knowable until build() exists and all 123 cases can run. |
||
|
|
8d79431016 |
geom: port sb-core and sb-profiles, the N-generic arrangements
The PROFILE record, centred assembly, and three complete arrangements: ring, spokes, fins. Each written for N members, exercised at N=3 and N=4. Failure travels as an empty profile carrying one failing check, as in the reference, so profile rejections and universal-check rejections stay in one order-sensitive list and the first failure is what surfaces. The section is cleaned before it is measured, not after. A Three-Fin section carries 17 collinear vertices from exact butt joints; they are harmless in 2D and leave zero-area triangles the tessellator cannot resolve, so measuring first would report on geometry that is not what gets extruded. Centred now carries the shifted members. Measuring a shifted shell against unshifted members reports every cavity as escaping the envelope, a leak of the whole cavity area from geometry that is fine. That trap caught me while smoke testing, so the opportunity is removed rather than documented. Verified against the oracle where the fillet does not affect the result: SPOKE_RADIUS_MM 9.1713, FIN_CORE_SIDE_MM 13.4028, FIN_SETBACK_MM 3.77783, FIN_JUNCTION_WEB_MM 2.29919, RING_CORNER_R_MAX_MM 2.87663, the ring edge vector, and both envelope dimensions all match to the recorded digit. The solvers are right. OPEN: with a junction fillet of 1.5 the Y section area is 135.572973 against a recorded 135.574, off by 0.001027 and just past the area tolerance, while every other quantity for that case matches exactly. Either the generator fillet default is not 1.5, or there is a difference of about seven parts per million concentrated in the fillet. The generator settles it. 35 tests. Nine mutations, two of which found real gaps: nothing asserted that cleaning removed anything, and the spoke zero-fillet check was untested. Oracle acceptance still skips; 236 unchanged. |
||
|
|
b101fe6adf |
geom: port sb-report, validation and report formatting
Checks as values, the five universal checks, metrics, ProfileRejected, and the report block. Completes the shared layer; only the profiles and build() remain. Report numbers are rounded to six significant figures on the way out, matching OpenSCAD echo, which is C %g at default precision. The oracle records what OpenSCAD printed, not full-precision geometry. This is load bearing rather than cosmetic. VOLUME_MM3 ends in _MM3, so test_oracle.py compares it at the lengths tolerance of 1e-4 and not 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 against the oracle: across all 113 accepted cases the recorded volume equals the rounded area times length to within 3.6e-12, which holds only if volume is computed unrounded and rounded at print. That is what this layer does. The consequence is that geometry must agree with the reference to better than one part in a million before rounding. Near a rounding boundary a smaller error can still tip the last digit, and that will show up as a single case failing by one unit in the last place rather than as something mysterious. 28 tests. Nine mutations, all caught first pass, including rounding to decimal places instead of significant figures and computing volume from the already-rounded area. Oracle acceptance still skips; 236 unchanged. |
||
|
|
ebf02d6573 |
geom: port sb-join, the junction and envelope strategies
Face lines, structural butt joints, hull caps, concave fillets, the derived bore, ring fit, ring envelope and section assembly. N-generic throughout, as the reference is. Junctions are structural, not cosmetic: one sleeve runs through its neighbour and is cut flush against that member the far surface, so the two share a full-width overlap whether or not a fillet is applied on top. The bore is derived from the members own inside-wall lines rather than a separately scaled shape, which is what makes the declared inside wall exactly what remains beside each cavity. 43 tests. Mutation testing found two of the reference own warnings to be load-bearing and untested by me. A bore that has turned inside out can carry over a square millimetre of area, so the area guard alone accepts it and only the interior-side test rejects it. And the ring fit really does have a spurious lower branch: a thin triangle meets a 1.2 mm web at relative scale 0.425, where members overhang their own corners and the solve looks converged. Both now covered. A third mutation was malformed on my part rather than a gap -- cutting the cavities twice is idempotent -- and was replaced with one that does change behaviour. Nine mutations caught. Oracle acceptance still skips; 236 unchanged. |
||
|
|
38ea024fdc |
geom: Shapely-backed region layer
Booleans, decomposition, area, simplicity, hull, bounds, mitred offset and vertex cleaning. Booleans go to GEOS, which is the reason Shapely was chosen. The decomposition does not. BOSL2 region_parts counts by nesting parity, not connectivity: a path takes a level from how many others contain the midpoint of its first edge, even levels are outer boundaries, their odd children are holes. SECTION_PARTS == 1 is an exact assertion, and Shapely agreeing with that count is a coincidence that holds for well-formed input and not otherwise, so the decomposition is transcribed and both the part count and the area derive from it. is_region_simple is treated as a manifold precondition rather than a diagnostic. An outline that touches itself measures perfectly and cannot be tessellated, so it must fail here and not at export. Developed against Shapely 2.1.2 / GEOS 3.13.1, matching CT 100. Boolean results on near-degenerate geometry can shift between GEOS releases; if the oracle ever disagrees by one part after an upgrade, look there first. 39 tests, all arithmetic on rectangles. Mutation run found a real gap: nothing distinguished on-boundary from outside in the nesting probe until a shared-edge case was added. Eight mutations now caught. Oracle acceptance still skips; 236 unchanged. |
||
|
|
545eee7217 |
geom: port BOSL2 round_corners and path cleanup
Shapely has no corner rounding, so the round_corners -> _circlecorner -> arc -> segs chain is transcribed from BOSL2 at the pinned commit 92d697c2, read from source rather than recalled. Also deduplicate, path_merge_collinear, is_collinear and approx, which the cleanup path depends on. Segment counts are contract, not a quality setting. An arc becomes straight segments and the count sets the enclosed area, compared against the oracle at 1e-3 mm2 -- and the extruded solid is those segments, so this is the definition of the surface. Both generators set $fn = facets with facets = 48 and no oracle case overrides it, so segmentation depends on swept angle alone. A right angle gives 12 points. round_corners raises where BOSL2 asserts, rather than clamping: silently fitting a roundover the reference refused would diverge without any visible failure. sb_corner_radii exists to derive safe radii up front. 33 tests. The exact-fit boundary raises rather than passing, because tan(45) is under 1 in both languages -- a test asserting the tidy behaviour would have looked right and been wrong. Mutation run found a real gap: nothing exercised the three-point floor on blunt corners until a 170-degree case was added. Seven mutations now caught. Oracle acceptance still skips; 236 unchanged. |
||
|
|
dfd02a4fd8 |
geom: port the pure-geometry half of sb-geom
Vectors, GEO and MEMBER records, member placement, sleeve and cavity paths, exact polyline distance, corner-radius derivation, and the monotone solver. Direct translation of legacy/openscad/lib/sb-geom.scad at rev 8.0.0. Angles stay in degrees, matching OpenSCAD, so every expression reads the same as its source line. The 44 solver iterations, the 0.999 and 0.98 scale factors, the 0.05/179.95 cutoffs and the 1e9 sentinel are reproduced exactly: they shaped the frozen oracle. Region operations are not included -- they need a 2D boolean kernel and follow with the Shapely layer. 49 unit tests, none of which touch the oracle. Harness proven by mutation: radians for degrees, a shortened solver, a dropped scale factor, a skipped crossing test and a flipped offset sign are each caught. Oracle acceptance still skips; 236 unchanged. |
||
|
|
c7e32d8e07 |
Seed repository: rev-8.0.0 reference, frozen oracle, toolchain, test harness
Reference implementation of the strap-beam generators at revision 8.0.0, kept so the acceptance oracle can be regenerated. Not a live target; the running application has no OpenSCAD dependency. The oracle holds 123 frozen cases, 113 accepted and 10 rejected, produced by OpenSCAD 2021.01 with BOSL2 at 92d697c2. The ten rejections are part of the contract: a port that accepts them is wrong. tests/test_oracle.py specifies the port API and was written before the port, so the interface follows from what must be verified rather than what is convenient to implement. Proven by adversarial stub: a build() that rejects everything passes all 10 rejection tests and fails all 226 acceptance tests. |