Kane Fabric recorded a gate register against this project at its 5df8801. This
is the same instrument pointed the other way, plus the defects that review
found in our own contract.
A gate is a disagreement, an unmade decision, or an assumption two projects
hold differently and have not had to reconcile. Recording one is not scheduling
work on it. The failure this prevents is specific: when two systems meet, the
pull is to fix the mismatch immediately, usually by one side quietly adopting
the other's model in a commit that looks like integration and is actually a
surrendered boundary.
G-1 and G-2 are the two defects, corrected in the previous commit and recorded
here because the class of mistake will recur.
G-3 is the one that does not dissolve. This compiler records a stable
identifier in a document built to stay legible for years; the civic direction
is address-bound, opaque, epoch-scoped participation that deliberately avoids
durable person identity. The mechanical part is already free -- Author.email
takes an opaque token without a schema change. What remains is that a record
naming a token whose epoch has closed has an author line nobody can ever
resolve again. That may be exactly right as privacy policy and is still a
permanent loss of provenance for a physical object somebody is holding. Two
goods in conflict, owned jointly, settled by neither side writing its document
second.
G-4 and G-5 are the residue of the two new invariants: nothing verifies that
exactly one hop decides, and fail-closed makes the eligibility endpoint a hard
dependency of every gated route. Both recorded rather than designed around.
G-6 notes that a shape is not a vocabulary -- two deployments can comply and
still disagree about what "verified" means, which is tolerable only while the
record states the method verbatim and claims nothing beyond it.
N-1 through N-4 record what was raised against us that is correctly not ours:
buildings against delivery-point geography, the local secure origin MS5-004
needs, the wg-pk fleet question, and credential hierarchies. Written down so a
successor does not mistake them for work or rediscover them as new.
Section 4 records that both projects independently drew the same junction --
geography, then a membership system, then a minimal decision, then this
compiler, one direction only. That is the strongest evidence available that the
shape is right. It also states where the two must not meet: if this compiler
ever reads parcels, delivery points or buildings, the membership layer has been
bypassed and every gate here is void.
External review of bac6120 by the Kane Fabric project found two defects, both
mine, both cheap now and expensive once anything implements against them.
The contract gave kane-fabric/oidc as an example authentication method. That
named a capability in another project which has no OIDC service, no user
database and no person-authentication role at all. An example in an interface
document is read as an expectation by the next person to implement it -- the
same failure as "acceptable for a development name", a plausible clause nobody
challenged hardening into a constraint. Method is now specified by shape rather
than by example, and the document names no system outside itself.
The headers were X-Kane-Auth-Email and X-Kane-Auth-Method. Two things wrong: a
jurisdiction in a header name is a deployment fact in an invariant place, in a
document that spends a section insisting the compiler must never learn a
deployment's membership concepts; and -Email named a format in a field the
contract explicitly allows to hold something else. Now X-Mechcomp-Auth-Id and
X-Mechcomp-Auth-Method. The receiver is the invariant, the jurisdiction is not.
Section 3 now says plainly that the identifier need not be an email address. An
opaque or epoch-scoped token fits Author.email without a schema change; the
field is named for what this deployment holds, not for what the contract
requires. Renaming it would be a format change to every stored record and is
deliberately not done.
Two new invariants, both from taking seriously that containers owned by other
projects will insert themselves into this chain.
I-1: exactly one hop decides, and it is the last before the application. Two
intermediaries both setting the identity headers is a forgery vector wearing
the costume of a deployment change -- the later wins, the earlier believes it
decided, nothing reports the conflict. CT 101 is named in section 2 because it
is what exists, not because it is the invariant.
I-4: anything that is not an affirmative permission is a refusal. Unreachable,
timed out, malformed and 5xx all deny. Fail-open and fail-closed are both
defensible and are not the same system; finding out which one was built during
an outage is the worst way to learn it.
Section 6 gains parcels and delivery points explicitly, so the boundary holds
whichever primitive the geography layer settles on. Section 7 no longer obliges
any named project to provide anything -- the authorisation decision belongs to
a membership system between geography and this application, and nothing here
asks a geography layer to become an identity provider.
STAGING-STATE recorded mechcomp.service as delivered at deploy/mechcomp.service
on 11 SEP. The unit was real; the directory was not. It existed only on CT 100,
so the repository claimed to hold something it did not, and the only way to
answer a question about the service was to read the container. The nginx vhost
had never been committed at all.
Both imported verbatim as they ran on 12 SEP, with their host checksums proven
equal at import. A first commit of a configuration file records reality, not
intentions -- every later improvement is then a diff against a known-good
starting point rather than a rewrite nobody can check.
mechcomp.env is deliberately absent. It is the one file that is supposed to
differ between instances, it is root:mechcomp 0640 because a deployment's
bindings belong to the deployment, and a committed copy would create a second
source of truth for exactly the wrong file. ENVIRONMENT.md documents the keys.
Certificates and keys likewise.
IDENTITY-CONTRACT.md is the boundary between this application and the
membership system, and the only document here written to be read by someone who
does not work on this repository. Two systems that must agree on an interface
need it written once, somewhere both can point at.
The compiler does not authenticate anyone; it is told. CT 101 decides, against
the membership system, and sets X-Kane-Auth-Email and X-Kane-Auth-Method. They
map onto Author.email and Author.method, which already exist and are tested. The
parser's refusal to recover `verified` from text was built for this: a record
read back is always self-declared, because a file cannot attest to its own
verification.
The decision belongs at CT 101 and never at wg-pk. The hub carries twenty peers
and is estate infrastructure this project does not own, so authorisation there
would make every future adjustment an escalation and would teach a shared
transport about one project's membership roll. CT 101 already shares the service
bridge with CT 100 and CT 102.
One gated prefix, /m/. nginx gets one location block, written once. Gating a new
endpoint afterwards is choosing a URL in Python -- no proxy change, no
escalation. That cheapness makes the placement of the line reversible rather
than structural. Today it sits at export: the catalogue and composer are open,
and what requires membership is producing an artifact whose design record names
an author.
Section 6 is the part most likely to erode and is written hardest. The compiler
must never learn what a building is, what a membership level is, or that group
names exist -- including as a configuration value, which is how the leak arrives
by the side door. The test is that the membership system can rename every level
and replace its storage without a line of this repository being read.
Section 9 deletes the ACL from the roadmap rather than deferring it. The
compiler will not have a user table, a login form or a session.
Recorded openly rather than assumed: the service is world-reachable and
unauthenticated, /m/ is not yet gated, and STL export will therefore ship open.
Same posture the whole service already has. The earlier justification --
"acceptable for a development name" -- was wrong and is corrected separately:
dev.mechcomp.kane-il.us is production, dev abbreviates Mechanical Compiler
Developers, and the name is on printed material.
The inbound header strip depends on none of this and should land on its own. It
costs one directive and removes a forgery that becomes possible the moment the
headers mean anything.
HANDOFF section 4 said WORK-ORDER-004 was pending and that CIVICVS could not
see any of this work. It was rewritten thirty-two minutes after 1fdb115 closed
the ingress, in the same session, against the work order's old state rather
than its new one -- so HEAD described a world where the thing you were already
looking at did not exist. Section 11 row 7 carried the same staleness. Both
corrected, and the correction says how it happened, because section 0 was added
to prevent exactly this and did not.
Three things 1fdb115 recorded as unsettled were never promoted into section 11
and are now rows 8 through 10: TLS renewal has never been observed to succeed
for this name and is first due before 2026-12-10; the composer has no
acceptance criteria of its own, only the path to it does; and the service is
world-reachable and unauthenticated, which section 4 item 5 called a conscious
decision to be made before publishing and which publishing has now made due.
Priority items 1 and 2 reversed. The original case for authorship first was
that records made before the field exists can never be attributed. That was too
strong, and too strong because the field is excluded from both hashes: a record
regenerated later keeps its input_id. What it does not keep is build_id, which
covers Shapely and GEOS, and boolean results on near-degenerate geometry shift
between GEOS releases -- the F-034 mechanism. Attribution is recoverable, not
free. The residual argument stands and the field landed first at 48d5665.
Recorded under item 2, because it is the first thing STL export runs into:
length_view is not in COMMON_GROUPS, so the Full Length branch is unreachable
from the composer and every export would silently be a 100 mm preview. The
sweep must equal model_length_mm(p), already published as LENGTH_MM, or the
record's VOLUME_MM3 and MASS_G describe a different object than the file beside
them. Also split the roadmap's STL/STEP bullet: STEP needs the kernel, STL does
not, and one line implying both was left behind by the section 5 correction.
F-037: a tar stream rooted at "." re-owned the repository root. tar x ran as
root and applied the "." entry's ownership to /var/www/mechcomp; the chown that
followed named only src and tests. Everything below the root was correct, so
547 tests passed and only git noticed. Corrected by chown on one path, owner
only, no -R. safe.directory was not added -- that is F-008 and would have
masked this and every later instance.
Two of F-037's three consequences are process defects of mine rather than facts
about the environment. The verification step ran before the landing that
destroyed it, so a git status ahead of the breaking step reads as a pass -- the
F-027 pattern in a new place. And section 7 says record a failure before
correcting it; I corrected first. Both recorded rather than quietly fixed.
PROCESS section 3 gains two REQs, because the delivery path as documented
produces F-037 every time: the chown must name the directory the files land in,
and a tar transport must name its top-level directories rather than root at
".". pct push is simpler for a single file and cannot reproduce it at all.
Section 8 gains the bytecode rule: PYTHONDONTWRITEBYTECODE=1 and clear
__pycache__ between mutations. A stale .pyc masked a real defect once and every
mutation result reported before that was optimistic by an unknown amount. A
mutation surviving on stale bytecode is indistinguishable from one surviving on
a weak test.
Open question 11 is new and is CIVICVS's: ct-baseline.sh exits 0 with the F-037
condition present, so by section 9a the ownership of a service working tree is
not part of the container standard. Whether it should be is a decision about
host property covering three projects.
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.
CIVICVS ruled on 11 SEP that the absence of an assembly layer is chosen, not unfinished. Positioning is field work. Parts made and shipped by different manufacturers have no knowledge of each other assembly tolerances, and interchangeable manufacture works because of that separation rather than in spite of it. Fit up is resolved on site against conditions no designer had. PRECISION.md section 7 now says so, and closes with read this as a boundary that was chosen, not a gap to be filled.
Section 10 corrected. Its lock is on this document subject, not on the software roadmap, and it was misread that way once. Section 7 holds two kinds of entry: limits that may be lifted by work, and boundaries that were chosen and should not be. Without that distinction section 7 reads as a to do list.
Section 7 also separates geometry interchange from machine instructions. An STL or STEP describes a shape; a toolpath describes what a machine should do. The first is in scope and planned, the second is not. The two sat one line apart with nothing saying they differ in kind.
HANDOFF section 5 corrected: STL export needs no CAD kernel. A member here is prismatic by definition, so an STL is two triangulated caps and a quad strip. Verified in CT 100 that shapely constrained_delaunay_triangles handles a polygon with a hole correctly, area exact and no triangle inside the hole. That second check is the one that matters, because a triangulator that fills bores produces an STL which looks right in a slicer and prints solid where the conduit goes. STEP still needs the kernel.
Priority order recorded with its reasoning so a successor can disagree with the argument rather than only the sequence. STL export first because it is the only item producing physical feedback. Then an authorship field in the design record, one field and a one way door. Then per member stock for the conduit core. Then persistence, which nothing has ever written despite MECHCOMP_DATA_DIR being declared since staging. Then ACL, last, because access control over nothing is machinery without a subject.
A node is not an assembly. It is another artifact with declared interfaces and stays in scope. What is out of scope is positioning artifacts relative to one another. The project obligation is therefore to make each artifact interchangeable, which is what the stock descriptor and the design record already exist for.
browser to DNS to Lets Encrypt TLS on wg-pk to the WireGuard tunnel to a DNAT on srv-b to CT 100. Proven 200 over v4 and v6, 301 from http, and real build JSON through the full chain.
No WireGuard change and no route were made. The hub peer entry for srv-b is still a slash 32, as all twenty peers are. An earlier plan widened it to carry the service network; that was abandoned once the hub convention was read. Every vhost there proxies to a 10.110.0.x tunnel address directly, so following the convention removed the only step that could have locked the operator out of srv-b.
The srv-b side is one DNAT rule in nat PREROUTING, scoped to the hub as source so the other nineteen peers cannot reach the service network through it. Added live, proven from the hub, then persisted. Rollback copy kept. POSTROUTING order verified unchanged after iptables-save rewrote the file, and mail from srv-b was confirmed delivered afterwards because it traverses the same interface.
WORK-ORDER-004 is rewritten as executed rather than as planned. Its section 0 states that the original proposed a route and a WireGuard change, that neither was done, and why. The original text stays in history at a8081e1.
Three things recorded as not settled: renewal has never been observed to succeed for this name and is first due before 2026-12-10; no acceptance criteria exist for the composer itself, only for the path to it; and the service has no authentication. Also filed an estate finding: shell.infra.civicus.us and corpusdb.infra.civicus.us publish an AAAA one hex digit off the address wg-pk holds, so both are broken for v6 preferring clients.
A new section 0 gives the reading order with PROCESS.md at the top. The handoff never mentioned it. That is the root cause of three sets of instructions the operator could not execute in one session: an SSH to a host he does not reach that way, a LAN address behind a portless bridge, and a hosts file name that resolves on three machines.
The access model is now stated rather than only defended. The settled decisions said questioning Webmin wasted a session but never recorded what the arrangement is, so it was questioned again. It now records that he works from Webmin on the hub and into the srv-b shell, and that dev.infra names and 10.20.0.x addresses are never to be given as browser URLs.
The module table filed Geo and Member under primitives.py, where they have never been, and omitted records.py entirely. Two modules were read on the strength of it and neither held what it promised. Corrected, with the error named so it is not re-derived, plus rows for stock.py, design_record.py, svg.py and web/app.py.
Sections 3 and 4 described a world with no application in it: main two commits back, 468 passing, the catalogue front end does not exist, deployment blocked on application code. Now 529 passing at a8081e1 with the composer live behind the proxy, and the next work named as per member stock, which is what the conduit core actually needs.
PROCESS.md section 8 requires host changes to reach STAGING-STATE.md before a session ends. mechcomp-placeholder.service was disabled and stopped on 2026-09-11 and mechcomp.service took 10.20.0.10:8770 in its place. nginx on CT 101 needed no change because the real service took the address the placeholder occupied. The placeholder unit stays on disk, disabled, as the rollback.
Section 5 items closed: mechcomp.service, and replacing the placeholder. Application runtime acceptance is marked partial rather than done, because no acceptance criteria have been written for the composer and it is not reachable from outside.
WORK-ORDER-004 publishes the composer at dev.mechcomp.kane-il.us. The srv-b half needs no change, verified: forwarding on, FORWARD policy ACCEPT, a direct route on vmbr1, and none of the three FORWARD rules matches hub initiated inbound traffic. The single gate is AllowedIPs on the hub peer entry for srv-b, because WireGuard drops by cryptokey routing before consulting any routing table.
Design decision recorded: the public name terminates on the hub and proxies to CT 100 directly rather than through CT 101. Routing it through CT 101 would make the public path depend on a locally signed leaf that expires 2028-11-18 with nothing renewing it. The tunnel already provides the encryption that hop would add. CT 101 keeps serving the internal name.
Section 4 not now decision on the hub route is reopened. It was correct while there was no application to reach. The consequence of leaving it closed is that the operator cannot see the application at all.
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.
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.
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.
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 f5651d3 corrected. Provenance raised on an empty source, which made an unattributed dimension unrepresentable and blocked the ordinary use of the tool: type what the caliper reads, print, measure the print, adjust. Provenance now records and travels with the output. Fit refused negative clearance on the argument that interference is not assemblable, which is a design judgement and not the compilers to make. Interference is now computed and reported. CATALOGUE read as a whitelist and is documented as starting points, with a test asserting an entry built from nothing is as valid as one pulled from the dict.
emt_template takes the diameter, fit, designation and note from the caller. There is no standards table and no lookup. A parametric compiler cannot require its subject to be catalogued before it will run.
STOCK.md section 5 amended, since the refusals were implementing it. An entry without provenance no longer fails to ship, it ships labelled unattributed. The principle that a number must not appear from nowhere looking authoritative survives; the door does not.
Tests compare records and stock against a hand transcription of the reference rather than against each other, which would be tautological after delegation. Mutation testing found four gaps before landing: a dropped lamina stacking offset, emt_template silently ignoring its fit argument, describe discarding the note exactly when provenance was unverified, and a guard on float arithmetic that asserted a tautology.
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.
The oracle records six significant figures, so the last digit of an area near 200 mm2 is worth 0.001 mm2. Every measured disagreement between port and reference is one, two or three units in that place. SECTION_AREA_MM2, VOLUME_MM3 and MASS_G are now bounded at 8 ULP of the expected value. Everything that positions material keeps the declared 1e-4 mm and remains exact in all 123 cases.
Option 1 scope kept, derivation rejected on measurement. Max |dA|/P over the accepted set is 1.434e-05 mm, one seven-hundredth of the 0.01 mm criterion, so a perimeter x 0.01 bound would have run 1800 to 4500 times the worst real discrepancy and caught nothing. Perimeter also anti-correlates with the error.
Suite 468 passed, 0 failed. The 30 expected failures are resolved, not suppressed. Mutation tested before landing: worst case uses 37.5 percent of its bound, 12 ULP offsets and 1e-4 relative scalings are caught in all 113 cases, 1e-6 and 1e-5 correctly are not.
Adds docs/ACCEPTANCE.md as the specification. Adds F-036, the venv interpreter error, same class as F-035. Corrects the F-034 per-profile distribution to Three-Fin 10, Y 7, A Frame 6, Rectangle 5, T 1, Four-Fin 1, which sums to the stated 30.
CIVICVS approved option 1 on 20 AUG: scale-aware bounds in test_oracle.py for SECTION_AREA_MM2, VOLUME_MM3 and MASS_G, derived from the 0.01 mm criterion and the section perimeter. Not implemented yet. Implementation, mutation testing and a green suite are one piece of work, not a partial landing.
Section 4 item 2 is removed. The F-034 measurement rewrite in FAILURES.md landed in b255ebb, so listing it as a next step sends the next reader to redo work already done. FAILURES.md keeps status Open, which is correct until the change lands.
Header commit line moved to b255ebb. Section 7 and the section 11 open-questions table now say decided rather than ready to decide.
HANDOFF.md rewritten in place, as it is meant to be. The port is complete,
so the document now describes that state rather than the work leading to it.
Most important change for whoever reads it next: the suite is 436 passed,
30 failed, and section 3 says plainly that the 30 are expected and a red
`make test` is not a broken port. Without that line the next assistant
spends its opening exchange rediscovering what is already known.
Also added: the 0.01 mm accuracy criterion as a settled decision; that
defaults are per-family and differ (ring_corner_radius_mm is 2.00 in 3x
and 1.25 in 4x); that check declaration order is the reporting order; that
params carries only a case's overrides; and pointers to PRECISION.md for
what the compiler does not do.
F-034 rewritten around measurement rather than estimate. The original Y
evidence is preserved verbatim -- it was specific and hard-won. What is
new:
- the same mechanism at 90 degree ring corners, where the expression is
exactly 12 rather than exactly 8. Rectangle: reference envelope 50
vertices, port 48.
- which side is noisy, which was previously unstated and turns out to
matter. Instrumented at %.17g the port prints half=45 raw=12 ceil=12
on every call. It lands on the integer deterministically; the
reference does not. A guard cannot make the port match noise it does
not have, so there is nothing left to try on this side.
- the scale: 30 of 113 accepted cases, 83 exact, worst relative error
2.24e-05, confined to SECTION_AREA_MM2 and its two derivatives.
- the physical magnitude: chord deviation is r*(1-cos 3.75deg), so
0.0027 mm at r=1.25 and 0.0043 mm at r=2.00. The two implementations
differ from each other by at most ~0.4 um. Two orders inside the
0.01 mm criterion.
- the constraint on any fix: the tolerance block is inside the hashed
oracle document, so the change belongs in test_oracle.py.
Status stays Open. The decision is CIVICVS's and has not been made.
Answers, in plain language, what "accurate" means for this project: the
difference between model precision, machine resolution and achieved
accuracy, and why a design file should be far tighter than any machine
that will realise it.
Scoped hard to additive and subtractive manufacturing (section 0).
Formative processes, crystal growth, lithography, joining, metrology and
surface finish are explicitly out. Section 10 asks that additions widening
that scope be refused rather than accommodated -- a borrowed tolerance
figure carries no evidence from this project while looking exactly as
authoritative as a measured one.
Section 7 states what the compiler does NOT do: no assembly layer, no
structural analysis of any kind, prismatic shapes only, no toolpaths,
verified only within its tested range. A part passing every check here may
still be structurally unsound.
Records the facets limit: at facets=48, corner radii up to 4.67 mm stay
within 0.01 mm of a true curve. Above that facets must rise, growing with
the square root of radius.
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.
Handoff documents were additive. HANDOFF-2026-08-19 opened by saying the
18 AUG document still applied in full and added to it. After ten sessions
a new assistant would face ten documents to read in date order and diff
mentally to work out what is currently true. That cost grows every
session and none of it is necessary.
docs/HANDOFF.md is now the only handoff, rewritten in place each session.
It is state, not a log. The dated ones move to docs/archive/ and stop
being required reading. It is standalone: everything still true from both
is carried forward.
Section 1 is invocation, stated as facts rather than demonstrated in
examples. That is the other half of the problem. runuser appeared only
inside example commands, so it could be learned by pattern matching but
not by reading, which fails exactly when an assistant composes a command
from scratch. That is what happened, and it is F-035: su cannot run as a
nologin service user, both commands returned the same message before
touching anything, and the output read as a broken repository when the
tree was clean and the suite passed. The F-027 class again.
Also stated as facts: bash tools/ not ./tools/, all repository operations
as mechcomp, Gitea SSH on 42022, pct push then chown, explicit timeouts,
journalctl not /var/log, systemd-run for long jobs, and assert the guest
is running before interpreting any pct exec result.
Not done: the same facts should be cross referenced from PROCESS.md. I no
longer had that file in view and would not patch a document I cannot see.
The shared layer is ported. What remains is the eleven catalogue
profiles and build().
Records what this session established that would be expensive to
rediscover: six-significant-figure report rounding and why VOLUME_MM3
makes it load bearing, the read-only Docker probe against the pinned
image, the oracle parameter defaults, and F-034 in full including why it
must not be fixed.
Also records the working method that earned its keep: read the pinned
source rather than recall it, mutation test every suite before landing
it, and deliver by upload rather than paste.
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.
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.
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.
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.
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.
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.
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.
set -euo pipefail aborted the script on the diff pipeline one line
before the cp that restores the pre-run oracle, so --full left a
regenerated fixture file in the working tree while printing that
nothing had been overwritten. Appended || true.
Recorded as F-033. The fix is not yet exercised: the restore branch
runs only under --full and has not been entered since the change.
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.