Repository navigation
SPATIAL-2: units grounding, signed components, affine space, exact Euclidean geometry - #8091
Conversation
…clidean geometry The question 'can a measurement be negative' was the wrong one. A length is never negative; a COORDINATE and a DISPLACEMENT are, in different senses. So Millimeter stays Nat-backed and is NOT widened - a wall of length -3 m remains unwritable - and what lands is the affine layer that says what a signed spatial number means. extdeps.units.iso_80000_3 - space/time quantity grounding, cited not invented. Turn is the exact canonical angle unit because degree-to-turn is rational; 1 turn = 2*pi rad is irrational, so NO exact integer radian conversion exists and none is offered. Area and volume factors are carried explicitly because a generic linear scale conversion is wrong for both. Citation is pin-pending: the standard is paywalled and was not fetched, so the locator is the catalogue page. The relations carried are definitional and independently checkable. std.measure - Area, Volume, PlaneAngle quantities; MillimeterCoordinate and MillimeterDisplacementComponent (Int-backed, quantity stays Length; Nat-backed and Int-backed measures do not unify, so a magnitude cannot pass where a component is required); SquareMillimeter, CubicMillimeter; Arcsecond and ArcsecondDisplacement, magnitude separated from signed displacement exactly as length is. std.affine_space - Point3/Vector3 (and 2D), with the operations carrying the distinction: Point + Vector -> Point, Point - Point -> Vector, Vector +- Vector -> Vector, and Point + Point enforced by ABSENCE - no such function exists and the types do not unify. Frames ride as PHANTOM TYPE PARAMETERS, which the language already supports (Measure<Q,S,M> holds only a count), so a cross-frame subtraction does not typecheck rather than being refused at runtime - the stronger of the two options the ruling offered. Rotation is quarter-turns about a principal axis only: exact integer component permutation. There is no general rotation constructor at all - not one that rounds, not one that refuses. std.euclidean_geometry - the numeric policy made structural. norm_squared is TOTAL and exact (a sum of squares is an integer); norm and distance are PARTIAL, exact for axis-aligned displacements and refusing otherwise, with the refusal CARRYING the exact squared magnitude so an approximate realization can finish without re-deriving. Comparison (closer_than3) is total and needs no root, which is what most callers of 'distance' actually want. normalize separates DirectionZeroVectorHasNone - a mathematical absence - from DirectionRequiresApproximateRealization - a numeric limitation; they have different remedies. No trigonometry, no flooring. Ten witnesses green by execution with four discriminating REDs: norm flooring a diagonal, zero vector reported as having a direction, quarter turn degraded to a reflection, and Point-Point losing its sign. The reflection mutation initially passed - a real hole in the witness, not the implementation. The test vector was (1000, 0, 7), and with y = 0 a reflection and a rotation agree, so the claim was vacuous. Found by mutating and watching it stay green; the specimen is now asymmetric and the RED fires. Also recorded: an explicit 'as Nat' cast is refused by the interpreter, which is correct - std.coercion grounds Int->Nat as an IDENTITY rather than a cast, so there is no conversion to perform and the crossing needs no operator.
…han helper CI (regen): witness naming hygiene refused dag/test/claim/affine_space_witness_test.dag vec_components - a plain fn unreachable from any test fn, i.e. silent de-enrollment. It was written and never used. Deleted; no other orphans in the new witness files. Review 50860, both findings verified against the tree and fixed: 1. extdeps.color.srgb minted 'data full_turn_degrees: Int = 360', a second authority for the same definitional fact as degrees_per_turn(). Two extdeps rows for '360 degrees per turn' can drift independently. ISO 80000-3 owns plane angle, so iso_80000_3 is the surviving anchor and srgb now imports it; the literal is gone and full_turn_degrees is derived. 2. std.measure cited iso_80000_3 in prose while importing only iec_80000_13, iso8601 and dimensionless - the new carriers had no edge to the module claiming to ground them, and nothing in the tree imported iso_80000_3 at all. A citation naming an authority the module does not depend on is a floating island, and the factor here and the factor there could drift with nothing to notice. std.measure now imports iso_80000_3 and DERIVES square_metres_to_square_millimetres, cubic_metres_to_cubic_millimetres, full_turn_arcseconds, degrees_to_arcseconds and quarter_turn_arcseconds from the cited factors. New witness spatial_units_witness_test proves the wiring by execution rather than by import presence: area/volume scale nonlinearly (1 m2 = 1e6 mm2, not 1e3), the subdivision chain is exact (1296000 arcseconds per turn), a quarter turn divides exactly, and a REGRESSION CONTROL asserts srgb's negative-hue wrap is unchanged after the consolidation - the one place that constant does real work.
dot3 returns SignedSquareMillimeter and cross3 returns a new AreaVector3<F>: a millimetre times a millimetre is a millimetre squared, and returning Vector3<F> made translate_point3(point, by: cross3(a,b)) writable - an area consumed as a physical translation. A separate carrier makes that unwritable rather than wrong. normalize3 returns AxisDirection3<F>, the six directions integer arithmetic represents exactly. The previous +-1-millimetre 'unit vector' was a one-millimetre displacement, and translating by it moved the point 1mm. std.measure gains MilliSquared (-6), MilliCubed (-9), ArcsecondAngle and TurnAngle. Milli is -3, so Measure<Area, Milli, Nat> asserted 10^-3 of the base area unit - false, and the type stated a wrong fact regardless of the comment beside it. LengthRequiresApproximateRealization -> LengthExactRootNotRecognised: the old name was false for (3,4,0), whose magnitude is exactly 5. That is a missing integer square root in this slice, not a property of the problem. The ISO 80000-3 citation drops its numeric catalogue locator, which review reports names Part 2. iso.org refuses automated fetches (HTTP 403) so it could not be checked here, and an unverifiable id that may name the wrong standard is worse than none. The designation is the citation; edition is deliberately unstated. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Pushed 1. 2. 3. The scale conflation, and the serious one. 4. 5. The ISO anchor. Review reports the numeric locator names Part 2 and that the 2006 edition is withdrawn. I could not verify either — iso.org returns HTTP 403 to automated fetches — so I removed the locator rather than correcting it on a guess: an unverifiable catalogue id that may name the wrong standard is worse than none. The designation is now the citation (§3 cite-the-symbol; a catalogue id is a second positional naming scheme, and the positional one is what rots). Edition is deliberately unstated, which costs an edition-sensitive claim against this module until a pin lands. The relations carried are not edition-sensitive — a turn is 360 degrees by definition of the degree — which is why they were safe ahead of the pin. Review 50860's two findings are in the same push: On the evidence, stated at the scope it has. The 10 witnesses in — sent from deep-fox-168 |
The heal job derives its authored-conflict exclusion list from the generated stage0 crate members, so adding an extdeps module drifts ci.yml. heal cannot push workflow paths (ActionsJobCredentialScopeUnavailable), so the regen is committed by the author. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The self-host fixed point emits a stage0 .rs per module in the regen source closure and compares it against the committed file. Auto-heal added iso_80000_3 to stage0_emit_plan_generated.dag, so regen emitted extdeps_units_iso_80000_3.rs and found nothing committed to compare against: read committed src/v1/stage0/src/extdeps_units_iso_80000_3.rs: No such file Committing regen_stage0's output. std_measure.rs and lib.rs move with it - the new Scale arms and the module registration. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
std_measure imports the new module, and std_measure lives in v1-stage0-std-core, so building that crate standalone could not resolve crate::extdeps_units_iso_80000_3. The partition roster and its module edges are hand-listed in src/v2/workflow/rust_crate_partition.dag, so a new module in the stage0 emit set is NOT picked up by regen - the generated projection was self-consistent and the crate still failed to compile. Exact precedent: #7865, std_unicode_types missing from the same roster, same four files. Verified by execution: all seven partition crates cargo check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Generated projections and the stage0 seed regenerated from the merged authorities rather than resolved by hand; std/measure.dag took both sides' independent appends. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ames, honest names, typed conversion inputs One SignedMillimeterComponent replaces MillimeterCoordinate and MillimeterDisplacementComponent, which were the same structural type under two names; the affine distinction stays in Point3/Vector3 and their operations. MilliSquared/MilliCubed deleted - they were Micro and Nano under second names, one decimal factor spelled twice, and every Scale fold had grown two arms answering what an existing arm already answered. Conversions take SquareMeter/CubicMeter/Degree/Turn instead of a bare Nat, so the one boundary where a unit changes is not the one place an undimensioned number was accepted. Handedness/CoordinateFrameDescriptor deleted: LeftHanded was writable while every operation was unconditionally right-handed and nothing read the descriptor. Transform3 renamed ZQuarterTurnTransform3 to match what it does. Result arms renamed to name the kernel rather than the mathematics, and axis_direction_of inlined into its guarded branch - it answered PositiveZ for the zero vector and PositiveX for (1,1,0) to any caller. extdeps.units.iso_80000_3 pinned to ISO 80000-3:2019 (catalogue 64974) plus Amd 1:2025 (87100), with the unfetched-normative-text standing stated; it now carries extdeps_model_scope and joins scope_carrier_paths, which is what the merge with main newly requires. The turn-is-canonical numeric policy moved to std.measure, where the decision is actually made; the BIPM attribution is named as a separate subject rather than an uncited aside. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tial operation std.checked_arithmetic is the single home for the finite-machine integer boundary: one declared i64 bound replacing the six literals open-coded inside std.induction's checked operations, add/subtract/multiply/negate as PRE-checks (the decision is made before the operation, so nothing inspects a product the machine already wrapped), and a typed IntegerOverflow cause naming the operation and its operands. It is not in std.integer, which is where the integer authority otherwise lives, because std.integer imports std.induction and the import graph's one structural law is acyclicity - homing it apart is what lets both reach it. The dissolve-on is recorded in the module. std.induction's own int_add_checked/int_mul_checked are deleted rather than left beside it; its three call sites consume the authority. Every spatial operation that can cross the bound now answers SpatialArithmeticResult and carries the cause verbatim: translation, point subtraction, vector add/subtract/negate/scale, quarter-turn rotation (a permutation still negates), the two-stage transform, dot, cross, squared norm and distance, norm, distance, comparison, and normalization. norm3 keeps its two failure kinds apart - the machine bound outside, the kernel's unresolved root inside - because their remedies differ. int_to_nat_nonnegative is deleted. It clamped negatives to zero directly beneath a comment saying that clamping would fabricate an answer. Its replacement refuses the one unrepresentable magnitude and fabricates nothing; the residual unguarded crossing it still needs is named in the module with its trigger. Evidence: nine operation-level refusal claims, each asserting WHICH operation crossed (the dot product's sum refuses as an add, not a multiply - both products fit), the operands travelling with the cause, and a positive control proving the boundary is exactly where it is rather than everything refusing. Every ready-side claim asserts through a match that makes a refusal false, so a uniformly refusing implementation cannot read as green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The witness naming-hygiene walk refused it: a plain fn in a *_test.dag unreachable from every test fn is silently de-enrolled code. It was a leftover from an earlier shape of the result assertions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eal exclude list The heal job's authored-conflict filter excludes every generated stage0 file, and that list is a projection of the generated-file roster. Adding a module to the stage0 emit set therefore changes ci.yml, and workflow paths are author-commit-required, so heal refused rather than pushing it itself. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The floor refused ORDINARY-FLOOR-OVER-BUDGET at population_index=4 with elapsed_ms == budget_ms == 3300000 (run 31467399193, job 93706807817). Signed by briansrls, 2026-08-11; the dated line with run id, sizing and its assumptions is appended to gunbc_ci_two_population_budget_note per the raise discipline. 55 minutes is a censored lower bound, not a measurement - the walk was killed while running - so the number is an extrapolation: 3300s at 892/1074 entry groups extrapolates to ~3973s for the full selection, times the 1.2 fleet spread this budget family already funds, rounded to 80 at whole-minute grain. The wrapper sum and both generated timeout-minutes follow derivedly (75 -> 100, 110 -> 135). Recorded with it, because the raise is not the fix: eval was 122s, 3.7% of the budget, while rewire_import_str alone was 1189s, 36% of the whole ordinary allowance. That cost shape belongs to the per-entry assembly lane, and this row should tighten when it lands. And the clamp note sizes the discovery batch against ~2316 witnesses while this run discovered 8681 - a basis ~3.7x stale against the corpus it governs, owed and not done here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…minutes floor_plan_carries_disjoint_ordinary_and_postcondition_budgets asserts the two budgets separately against their authority rows and then pins their SUM as a literal. The first two conjuncts followed the raise derivedly; the sum did not, so it redded. The literal stays a literal rather than becoming the derived sum of the same two rows: a policy budget stating the resource cap it enforces is one of the oracle forms DESIGN section 5 admits, whereas rewriting it as ordinary + postcondition == ordinary + postcondition would collapse the assertion to measure() == measure() and assert nothing. It earned its keep this run by catching exactly the half-applied raise it exists for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The witness conflict is one conjunction: main deleted ci_job_backstop_dropped_the_selection_control_term (a sentinel magnitude pin that its floor-receipts upload step had just made reachable by an unrelated route), this branch raised the ordinary floor budget to 80. Both sides kept - main's deletion, this branch's number. ci.yml regenerated from the merged authorities rather than resolved by hand; regen at fixpoint, and the two budget witnesses plus both spatial witnesses re-run green after the merge. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
main has been red since #8129 and every branch that merges it inherits the red. Two failures, one root cause, and the cause is NOT where it looks. `unresolved type 'Memory'` fired in dag/product/budget_tree.dag, dag/gunbc/ci_budget_tree.dag and dag/test/claim/budget_tree_witness_test.dag -- files dating to #6165 that were compiling. The obvious suspect was #8091, which added 213 lines to dag/std/measure.dag where `Memory` lives. That was a red herring: measure.dag's `Memory` lines are untouched. #8129 added a SECOND declaration spelled `Memory` -- a variant of BoardCapability in dag/product/compute_board/intent.dag -- and adding a name to the flat namespace is a corpus-wide edit, so `Memory` became ambiguous and every module importing it from std.measure as a type stopped resolving. The same PR added `unsatisfied_capabilities` to dag/product/compute_board/composition.dag, where gunbc.auth.github_credential already declared one, which is the second failure. THE NEWER NAME YIELDS, per the single-authority rule: `Quantity.Memory` in std.measure is the older and broader authority with consumers across the tree, and `unsatisfied_capabilities` in the auth module has five declaration sites and two consumers in heal_push_plan. So BoardCapability.Memory becomes MemorySubsystem -- accurate on an axis whose sibling constructor is already MemoryCapacity -- and the board fold becomes unsatisfied_board_capabilities. Nine call sites across four files; no behaviour changes and no emitted seed file mentions either name. Green by execution: compute_board_readiness, github_app_registry and budget_tree witnesses all pass here and all three fail on the base. WHAT THIS DOES NOT DO, stated so the repair is not mistaken for the wall. There is no mechanism preventing the next PR from doing exactly this again: the class is a flat-namespace homonym, which is decidable and therefore a construction wall waiting on the namespace lane's containment authority, not on diligence. Until that lands, this class recurs whenever two lanes add the same bare name, and it will keep arriving as an unrelated-looking build break in a third file. Co-authored-by: gunbc-ci-auto-heal <gunbc-ci-auto-heal@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…8143) Three lookups answered with the first match, so a duplicated identity was resolved silently by authoring order rather than reported. binding_for: two rows for one component chose a model standing by position, so an ideal-lumped row and a vendor-subcircuit row for the same part emitted or refused depending on which was typed first — a verdict decided by an editing accident. spelling_for: two rows for one signal picked a Verilog spelling the same way, so a rename touching one row emitted the stale name silently. The third is the one that matters. net_of_terminal returns the first net containing a terminal, and terminals_are_netted only asked whether it answered at all — which collapsed "on several nets" into "netted". A terminal on two nets is a SHORT between them, a genuinely different circuit, and the projection emitted a deck naming whichever net was authored first. That deck simulates cleanly and describes a board that is not the modelled one, which is the exact failure this module's own note claims to prevent for floating terminals and left open for shorted ones. net_membership_count makes the three states distinguishable: no net, one net, several. All three now refuse — DuplicateModelBinding, DuplicateSignalBinding, TerminalOnMultipleNets — typed and located, never resolved. Each wall is proven in BOTH authoring orders, because a check that tried one order would pass against a lookup that still silently prefers a position. The short refusal carries a positive control sharing every fixture except the extra net, so the refusal is attributable to the short and not to the fixture. Also discharges RequiresSpatialAffineSubstrateLanded: SPATIAL-2 (#8091) landed on main at 341d320, so the cause is deleted rather than kept as a satisfied row. The remaining causes are independent of it. Green by execution: 40/40 witnesses (article 7, readiness 14, SPICE 10, Verilog 9). Co-authored-by: Brian Searls <briansearls1@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…efusal (#8156) #8091 claimed that a phantom frame parameter made cross-frame subtraction fail to typecheck, and called that the stronger of the two available options. Measured on the production compile-and-run path 2026-08-11, that program compiles clean and returns 9 -- a plausible number for a question with no answer, which is the fabricated output DESIGN 5 forbids. The cause is a v1 seed limitation, not a modelling slip: the argument-position judge compares nominal HEAD NAMES, and applied type arguments live in the type node's children where every name-level comparison discards them. Point3 matched Point3, so the frames were never compared. A separate compiler PR closes the call boundary; it cannot close the producer side, because return-position conformance declines to judge record types at all pending feature:conformance-produced-type-identity. So the carried identity is the authority and the honest claim is frame consistency BY CHECKED DERIVATION, not by construction: - std.coordinate_frame mints CoordinateFrameIdentity, declaration-owned and never derived from a name or from contents (the HostResourceIdentity precedent). Recorded as the third minted-occurrence carrier in the corpus, which is evidence the subject-parameterized extraction is due -- left as a trigger rather than built here. - Phantom <F> is removed rather than demoted. Two authorities for one fact is the DESIGN 3 violation whichever is right, and the weaker one is the one the compiler cannot enforce end to end. - SpatialResult carries three arms: ready, arithmetic refusal, frame refusal. One coproduct rather than nested result types -- there is one question at the operation boundary and the causes are what differ. - The refusal names ROLES, not expected/observed: for a symmetric operation neither frame is normatively expected, so an operation identity and the required relation replace a report of argument order. - The frame check precedes the arithmetic everywhere, because operands in different frames pose no question to overflow. Green by execution, with discriminating controls: cross-frame subtraction, addition, subtraction, translation and transform application each refuse and name both frames; same-frame operations still answer (the positive sibling a refusal-only control cannot see); frame and overflow are asserted distinct in both directions; a transform's translation must be in its destination frame. The decisive one is the lying-producer control: a function handing back a room-frame value under a building-frame annotation cannot rewrite the carried frame, and the next operation refuses. Mutation-tested -- disabling a single frame guard reds the suite. Co-authored-by: Brian Searls <briansearls1@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* SPATIAL-2b: carried coordinate-frame identity and operational frame refusal #8091 claimed that a phantom frame parameter made cross-frame subtraction fail to typecheck, and called that the stronger of the two available options. Measured on the production compile-and-run path 2026-08-11, that program compiles clean and returns 9 -- a plausible number for a question with no answer, which is the fabricated output DESIGN 5 forbids. The cause is a v1 seed limitation, not a modelling slip: the argument-position judge compares nominal HEAD NAMES, and applied type arguments live in the type node's children where every name-level comparison discards them. Point3 matched Point3, so the frames were never compared. A separate compiler PR closes the call boundary; it cannot close the producer side, because return-position conformance declines to judge record types at all pending feature:conformance-produced-type-identity. So the carried identity is the authority and the honest claim is frame consistency BY CHECKED DERIVATION, not by construction: - std.coordinate_frame mints CoordinateFrameIdentity, declaration-owned and never derived from a name or from contents (the HostResourceIdentity precedent). Recorded as the third minted-occurrence carrier in the corpus, which is evidence the subject-parameterized extraction is due -- left as a trigger rather than built here. - Phantom <F> is removed rather than demoted. Two authorities for one fact is the DESIGN 3 violation whichever is right, and the weaker one is the one the compiler cannot enforce end to end. - SpatialResult carries three arms: ready, arithmetic refusal, frame refusal. One coproduct rather than nested result types -- there is one question at the operation boundary and the causes are what differ. - The refusal names ROLES, not expected/observed: for a symmetric operation neither frame is normatively expected, so an operation identity and the required relation replace a report of argument order. - The frame check precedes the arithmetic everywhere, because operands in different frames pose no question to overflow. Green by execution, with discriminating controls: cross-frame subtraction, addition, subtraction, translation and transform application each refuse and name both frames; same-frame operations still answer (the positive sibling a refusal-only control cannot see); frame and overflow are asserted distinct in both directions; a transform's translation must be in its destination frame. The decisive one is the lying-producer control: a function handing back a room-frame value under a building-frame annotation cannot rewrite the carried frame, and the next operation refuses. Mutation-tested -- disabling a single frame guard reds the suite. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * SPATIAL-3A: exact orthogonal geometry Everything here is closed under integer millimetre arithmetic -- no roots, no trigonometry, no rounding policy -- which is why the band sits before an approximate-numeric realization rather than after it. Rectilinear architectural geometry is exactly representable, so it is represented exactly, and the layer that needs a real number pays for it there. CARRIERS: Axis2/Axis3, AxisAlignedSegment2, AxisAlignedRectangle2, Plane3, AxisAlignedBox3, BoundingBox3, OrthogonalLoop2, AxisAlignedSurface3, OrthogonalPrism3, OrthogonalRotation3. THE WALLS ARE CONSTRUCTION, NOT VALIDATION. Every carrier is sole_constructor, so its record literal cannot be written and no cast reaches it from another module: the only route to a value is the mint beside it, and the mint refuses. A degenerate segment, an unclosed or self-intersecting loop, a zero-extent box and a cross-frame composition have no constructor outside this module rather than being checked by a caller who might not check. THE 24 ROTATIONS ARE 24 BY CONSTRUCTION. A rotation is fixed by the images of +X and +Y (six choices, then four perpendicular ones); +Z is DERIVED as their cross product, so orientation-preservation is not a determinant check that rejects reflections -- a reflection has no representation. Enumerating 24 variants would be a table to get wrong once and maintain forever, and storing three images concedes the reflected state is writable. TYPED REFUSALS, one per invalid state, because the remedies differ: coincident endpoints are an authoring slip, a diagonal is a limit of the exact kernel, a frame mismatch is a composition error. Extent refusals NAME THE AXIS that collapsed. TWO CARRIERS DELIBERATELY NOT MINTED, recorded rather than silently omitted: OrthogonalSegment2 and OrthogonalSurface3. In 2D an axis-aligned segment and an orthogonal segment denote the same set, as do an axis-aligned rectangle and an orthogonal surface in an axis-perpendicular plane; minting both spellings forks one concept across two authorities (DESIGN section 3). NO POLYGON BOOLEANS. Union, intersection and difference are absent, not stubbed and not approximated: rectilinear Booleans need a sweep with a canonical output ordering and a touching-edge degeneracy policy, and inventing that policy under the name `union` is how a plausible wrong region reaches a consumer. Green by execution, eight witnesses, every negative control with a positive sibling in the same witness -- a mint that refused everything would satisfy the refusal half of each pair, and that is exactly the gap that let SPATIAL-2 ship a wall which did not exist. Two controls earned their keep by failing first. The orientation control asserts a DERIVED z-image rather than that construction succeeded, and caught a sign inversion that made the identity rotation left-handed. The self-intersection control is a figure-eight whose eight edges are all axis-aligned, all adjacent pairs perpendicular, and no vertex repeated -- so nothing local to an edge distinguishes it, which is why the pairwise non-adjacent edge test exists rather than a repeated-vertex check standing in for it. Boundary convention, asserted rather than commented: touching faces are adjacent, not overlapping. Two rooms sharing a wall plane must not report as a collision. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Delete two unreachable refusal variants, a duplicated note, and a cubic loop scan Three findings from review, all confirmed against the code and all fixed rather than argued. UNREACHABLE REFUSAL VARIANTS (cursor review 51202). LoopNotClosed and SurfaceNotInPlane were declared in OrthogonalRefusal and emitted by no mint. Closure is structural -- the last vertex connects to the first by construction, so there is no unclosed loop to reject -- and a surface is a Plane3 plus a 2D rectangle, so non-coplanarity has no representation either. An unreachable arm on a refusal sum is rung inflation: a consumer reads it as a live failure mode and writes a handler for a state nothing can produce. Both deleted; the reasoning is kept as a comment beside the loop walls so the next author does not re-add them. DUPLICATED PROSE (same review). orthogonal_segment_not_minted_note restated the // block immediately above it and had no reader. DESIGN 4c is explicit that a String declaration whose sole purpose is commentary is misplaced or dead data, and two copies of one decision is the section 2/3 redundancy. The comment stays; the row is gone. CUBIC LOOP SCAN (claude review 51199 raised it as quadratic; measured worse). The loop mint read vertex i through `vertices |> skip(i) |> first`, which is linear in i. Inside a fold over all vertices that made each linear pass quadratic, and inside the NESTED fold for the pairwise edge test it made that test cubic rather than the quadratic it appears to be. The pairwise comparison is inherently quadratic and stays; the indexing was not, and that was the defect. Edges are now materialised once in a single linear pass over the vertices, with an accumulator carrying the previous vertex, consing and reversing rather than appending (concat(list, [x]) is itself quadratic). Perpendicular- ity is read off consecutive edges in one pass that carries the first edge's axis so the closing pair is compared without re-indexing. DESIGN 6's bare-minimum-cost rule is why this is fixed rather than noted: a proven quadratic fold is always fixed regardless of realized n, and "fine for architectural n" is not a time-stable fact because reuse changes n. All eight witnesses re-run green after the rework. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * SPATIAL-3A: delete duplicated point2 helper copies point2_x, point2_y and point2_same_position were each defined twice with identical bodies. The duplicates came from restoring helpers an earlier splice had over-deleted; three of the restored set had in fact survived. A duplicate-declaration scan over the module now finds none. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * SPATIAL-3A: make the surface plane and prism axis derived, home Axis3 with AxisDirection3 Three findings from review 51213, all real, all fixed by construction rather than by adding a refusal arm. The prism took an extrusion_axis: Axis3 and accepted any of the three once the height was positive. OrthogonalLoop2 is a loop of Point2, whose components ARE the frame's X and Y, so extruding along X or Y was writable and geometrically undefined. The parameter and the field are deleted; Z is the only axis a 2D footprint can be extruded along, so the invalid state now has no representation instead of a check (DESIGN section 5). The surface took a Plane3 and checked only frame equality, so an XY rectangle could be paired with a Y-normal plane and mint a surface whose coordinates do not lie in its own plane. The plane is now derived from the extent's frame at the given offset. Its frame-mismatch arm went with the parameter - there is one frame in play. The lost generality is stated in-carrier with its dissolve-on: a genuinely X- or Y-normal surface needs a 2D carrier that records which plane it spans, which the parameter did not deliver, it mis-delivered. axis_direction_axis / _is_negative / _of are projections over AxisDirection3, which std.euclidean_geometry owns; keeping them here forked that authority (DESIGN section 3). They move beside the type, and Axis3 moves with them because AxisDirection3 IS Axis3 crossed with a sign - leaving Axis3 behind would have made the helpers unable to import the type they decompose. The dead cross-frame surface clause is replaced by a control that pins the new property (Z-normal, right offset, extent's frame), discriminating by execution: minting the plane with Axis3X reds it. Eight witnesses green; the four in-body annotations this work introduced are hoisted (33 hard diagnostics, down from 37, none in these files). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * SPATIAL-3A: axis_aligned_surface3 is total, so its return type says so Flagged non-blocking in review 51225, but acted on: once the plane is derived there is no invalid input left for the mint to reject, so wrapping the result in OrthogonalConstruction advertised a refusal no arm could produce - the same rung inflation on the shape that the deleted LoopNotClosed/SurfaceNotInPlane arms were on the sum. The reviewer excused it as consistent with its siblings, but the siblings genuinely refuse and there is no generic consumer that the uniform shape buys anything for; every caller was paying a match for an arm that cannot arrive. The witness's success arm becomes a direct read of the minted surface (extent width preserved), discriminating by execution: expecting 4001 reds it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Brian Searls <briansearls1@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follows #8086 (SPATIAL-1). Branched from
main, not stacked, since affine space does not depend on the epistemic layer.Merged to current
mainon 2026-08-11.The correction this implements
"Can a measurement be negative" was the wrong question. A length is never negative; a coordinate and a displacement are, in different senses. So
MillimeterstaysNat-backed and is not widened — a wall of length −3 m remains unwritable. What was missing was never a signed length; it was the affine layer that says what a signed spatial number means, and an honest account of what a finite machine can compute about one.One signed component, not two
MillimeterCoordinateandMillimeterDisplacementComponentwere both spelledMeasure<Length, Milli, Int>— the same structural type under two names, implying a distinction the typechecker did not carry, in work whose subject is that distinction. They are now oneSignedMillimeterComponent.The affine distinction lives where it belongs: in
Point3<F>/Vector3<F>and their operations.Frames ride as phantom type parameters, so cross-frame subtraction does not typecheck.
Frames are right-handed orthogonal Cartesian, full stop.
Handedness/LeftHandedand theCoordinateFrameDescriptorare deleted:LeftHandedwas writable while every operation was unconditionally right-handed and nothing read the descriptor.Transform3is nowZQuarterTurnTransform3— it rotates about +Z only, and no X/Y rotation was added to make a broader name true.The mathematics is exact; the finite machine realization may refuse
The largest correction.
norm_squared3,dot3andcross3were documented "total and exact" while running oni64, where a vector a few million millimetres long has no representable squared magnitude. Andint_to_nat_nonnegativeclamped negatives to zero directly beneath a comment saying that clamping would fabricate an answer.std.checked_arithmeticis the new single home for the bound: add / subtract / multiply / negate as pre-checks — the decision happens before the operation, so nothing inspects a product the machine already wrapped — returning a typedIntegerOverflowcause naming the operation and its operands. One declared i64 bound replaces the six literals that were open-coded insidestd.induction's own checked ops, which are deleted in favour of the authority.It is not in
std.integerbecausestd.integerimportsstd.inductionand the import graph's one structural law is acyclicity; the module records that and its dissolve-on.Every spatial operation that can cross the bound now answers
SpatialArithmeticResult<T>and carries the cause verbatim.int_to_nat_nonnegativeis gone; its replacement refuses the one unrepresentable magnitude and fabricates nothing.norm3keeps two failure kinds apart deliberately — the machine bound outside, the kernel's unresolved root inside — because their remedies differ.Result standing, named for the kernel rather than the mathematics
LengthExactRootNotRecognisedLengthRootUnresolvedInExactKernelDirectionNotAxisAlignedDirectionUnresolvedInAxisKernelaxis_direction_ofis deleted and inlined into its guarded branch. It answeredPositiveZfor the zero vector andPositiveXfor (1,1,0) to any caller, safe only because its one call site happened to be guarded and its precondition lived in a comment.Units
Conversions take
SquareMeter/CubicMeter/Degree/Turninstead of a bareNat— the one boundary where a unit changes was the one place an undimensioned number was accepted.MilliSquaredandMilliCubedare deleted: they wereMicroandNanounder second names, one decimal factor spelled twice, and everyScalefold had grown two arms answering what an existing arm already answered.Scaleis the decimal factor; the alias names the unit.extdeps.units.iso_80000_3Pinned: ISO 80000-3:2019 (catalogue 64974), amended by Amd 1:2025 (87100), so the amendment is a second citation of one subject rather than a second subject. Evidence standing is stated rather than implied: iso.org returns HTTP 403 to this environment, so the normative text has not been read here and no claim rests on its wording; what was verified is the designation-to-catalogue mapping. The relations carried are definitional and independently checkable.
The module now carries
extdeps_model_scopeand joinsscope_carrier_paths— required by the external-model-scope gate that landed on main while this branch was dormant.The "turn is the exact canonical unit" policy moved to
std.measure, where the decision is actually made; the BIPM attribution is named as a separate subject rather than an uncited aside.Evidence
Both witnesses green by execution, and the arithmetic evidence is new:
falseRegen is at a fixed point (
regen_divergence_count=0,--verifyclean), all seven partition crates check, andcargo check --workspace --all-targetspasses.Known open — finding 8, with a measured reason
The type walls (Point+Point, cross-frame subtraction,
AreaVector3as a displacement, magnitude as component, wrong transform frame) still have no executing negative control. The intended harness,compile_dag_diagnostic_census, was tried and cannot serve: it compiles synthetic source against a witness-root module index that does not resolve corpus imports — a probe importing a single real module yields 82 blocking diagnostics, identical to one importing the whole spatial surface. Inlining copies of the carriers into the fixture would test the walls of a copy, not of these types, so no witness was shipped rather than a fake control. Closing this needs either the harness's roots extended or a different mechanism, and that is its own increment.Scope
Not attempted:
LineSegment,Plane,Polygon,Surface,Opening,SpatialSolid,EnclosedSpace, topology, placed objects, rendering. Those are SPATIAL-3 onward, which stays parked behind a real dependency: a sound affine substrate precedes sound topology.History
Frozen 2026-08-10 for portfolio sequencing, then unfrozen by operator override because concrete spatial consumers exist (compute-fabric owes
MountedTo,RequiresClearance,PhysicalRoute,PotentialFlowand containment to this kernel).