Skip to content

SPATIAL-2: units grounding, signed components, affine space, exact Euclidean geometry - #8091

Merged
briansrls merged 16 commits into
mainfrom
spatial-2-affine
Aug 11, 2026
Merged

briansrls merged 16 commits into
mainfrom
spatial-2-affine

Conversation

@gunbai-bot

@gunbai-bot gunbai-bot Bot commented Aug 9, 2026 •

Copy link
Copy Markdown
Contributor

Follows #8086 (SPATIAL-1). Branched from main, not stacked, since affine space does not depend on the epistemic layer.

Merged to current main on 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 Millimeter stays Nat-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

MillimeterCoordinate and MillimeterDisplacementComponent were both spelled Measure<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 one SignedMillimeterComponent.

The affine distinction lives where it belongs: in Point3<F> / Vector3<F> and their operations.

Point + Vector   -> Point
Point - Point    -> Vector
Vector +- Vector -> Vector
Point + Point    -> no operation exists

Frames ride as phantom type parameters, so cross-frame subtraction does not typecheck.

Frames are right-handed orthogonal Cartesian, full stop. Handedness/LeftHanded and the CoordinateFrameDescriptor are deleted: LeftHanded was writable while every operation was unconditionally right-handed and nothing read the descriptor.

Transform3 is now ZQuarterTurnTransform3 — 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, dot3 and cross3 were documented "total and exact" while running on i64, where a vector a few million millimetres long has no representable squared magnitude. And int_to_nat_nonnegative clamped negatives to zero directly beneath a comment saying that clamping would fabricate an answer.

std.checked_arithmetic is 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 typed IntegerOverflow cause naming the operation and its operands. One declared i64 bound replaces the six literals that were open-coded inside std.induction's own checked ops, which are deleted in favour of the authority.

It is not in std.integer because std.integer imports std.induction and 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_nonnegative is gone; its replacement refuses the one unrepresentable magnitude and fabricates nothing.

norm3 keeps 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

Was Now Why
LengthExactRootNotRecognised LengthRootUnresolvedInExactKernel for (1,1,0) there is no exact integer root to fail to recognise
DirectionNotAxisAligned DirectionUnresolvedInAxisKernel (3,4,0) has the exact rational direction (3/5,4/5,0); approximation is not the only remedy

axis_direction_of is deleted and inlined into its guarded branch. It answered PositiveZ for the zero vector and PositiveX for (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 / Turn instead of a bare Nat — the one boundary where a unit changes was the one place an undimensioned number was accepted.

MilliSquared and MilliCubed are 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. Scale is the decimal factor; the alias names the unit.

extdeps.units.iso_80000_3

Pinned: 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_scope and joins scope_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:

  • nine operation-level refusal claims, each asserting which operation crossed — the dot product's sum refuses as an add, not a multiply, because both products fit and the total does not
  • the operands travel with the cause
  • a positive control proving the boundary is exactly where it is (the maximum is reachable; one past it is not), so a uniformly-refusing implementation cannot pass
  • every ready-side claim asserts through a match that makes a refusal false

Regen is at a fixed point (regen_divergence_count=0, --verify clean), all seven partition crates check, and cargo check --workspace --all-targets passes.

Known open — finding 8, with a measured reason

The type walls (Point+Point, cross-frame subtraction, AreaVector3 as 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, PotentialFlow and containment to this kernel).

Brian Searls and others added 5 commits August 9, 2026 23:31
…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>
@gunbai-bot

gunbai-bot Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Pushed ec2d9a6 addressing review 50860 and the relayed dimensional review. All five blockers were real; four were live defects rather than naming issues.

1. dot3 erased its dimension. A millimetre times a millimetre is a millimetre squared, and it returned a bare Int. Now SignedSquareMillimeter — sign retained, because a negative dot product means the displacements point away from each other.

2. cross3 returned Vector3<F>, whose components are millimetre displacements. So the numbers were areas wearing a length type, and translate_point3(point, by: cross3(a, b)) typechecked — a cross-product area consumed as a physical translation. New AreaVector3<F> carrier, deliberately not an alias of Vector3: sharing three numeric components is not sharing a dimension. Same class in normalize3, which returned ±1 millimetre components — a one-millimetre displacement, not a direction, and translating by it moved the point 1 mm. It now returns AxisDirection3<F>, the six directions integer arithmetic represents exactly, carrying no magnitude at all.

3. The scale conflation, and the serious one. Milli => -3 is a real fold arm in scale_decimal_exponent, so Measure<Area, Milli, Nat> asserted 10⁻³ of the base area unit. That is false — square millimetres are 10⁻⁶ m² — and the type stated a wrong fact regardless of the comment beside it. Added MilliSquared (-6), MilliCubed (-9), ArcsecondAngle and TurnAngle across all three Scale folds and retyped the carriers.

4. LengthRequiresApproximateRealization overclaimed. It is false for (3, 4, 0), whose magnitude is exactly 5: approximation is not required, this slice simply has no integer square root to recognise a perfect square. Renamed LengthExactRootNotRecognised — naming a limitation of the code as a property of the problem is the same class as reporting a rung the executed evidence does not establish. Same rename for the direction arm.

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: degrees_per_turn is no longer duplicated (extdeps.color.srgb derives full_turn_degrees from it, the literal 360 is gone), and std.measure derives its conversions from the cited module rather than floating.

On the evidence, stated at the scope it has. The 10 witnesses in affine_space_witness_test.dag and 4 in spatial_units_witness_test.dag are green by execution, including a hue-wrap regression control on the srgb change. But the dimensional walls above are type errors — the expressions that erase a dimension do not compile — so there is no runtime RED that can exercise them, and I have not enrolled a negative-compile harness. The claim is therefore: those erasures are unwritable, established by the carriers being distinct types, not by an executed refusal.

— sent from deep-fox-168

Brian Searls and others added 3 commits August 10, 2026 00:11
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>
@gunbai-bot
gunbai-bot Bot marked this pull request as draft August 10, 2026 01:07
@briansrls briansrls closed this Aug 10, 2026
@briansrls briansrls reopened this Aug 11, 2026
Brian Searls and others added 6 commits August 11, 2026 04:10
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>
Brian Searls and others added 2 commits August 11, 2026 11:19
…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>
@briansrls
briansrls marked this pull request as ready for review August 11, 2026 17:37
@briansrls
briansrls merged commit 341d320 into main Aug 11, 2026
3 of 4 checks passed
@briansrls
briansrls deleted the spatial-2-affine branch August 11, 2026 17:37
briansrls pushed a commit that referenced this pull request Aug 11, 2026
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>
briansrls pushed a commit that referenced this pull request Aug 11, 2026
…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>
briansrls pushed a commit that referenced this pull request Aug 12, 2026
…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>
briansrls pushed a commit that referenced this pull request Aug 12, 2026
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant