Section 3 said main was atee3fedfwith 547 passing. Section 12 transcribed a commit log ending ata8081e1, ten commits behind. Neither is corrected; both are removed. git rev-parse, git describe and git log report them, and the header already explained why the hash line could never be right - it is not known until the commit is made, so the value was always the previous commit. The same reasoning applies to the rest and nobody had applied it. Section 4 item 2 listed STL export as forthcoming work. It landed at0545b79and the bounded route atcdde394. The length_view gap that would have made every export a silent 100 mm preview was fixed before the route shipped. The roadmap entry still said the composer does not export anything. Section 3 module table was missing stl.py entirely. The baseline run is now dated 2026-08-18 and marked overdue against DIV-006, since it predates both the DNAT rule and the placeholder service replacement, which PROCESS section 9a names as requiring a run. Section 7 recorded the F-034 per-profile breakdown as Three-Fin 9, Y 7, A Frame 5, Rectangle 5, T 1, Four-Fin 1 - which sums to 28 against its own stated total of 30. The measured distribution had been recorded correctly in ACCEPTANCE section 7 and in F-034 resolution for three weeks, while the wrong numbers stayed in the document a successor reads first. Section 0 reading order gains DIVERGENCES.md and ENVIRONMENT.md. ENVIRONMENT.md was missing from it for three weeks while PROCESS section 8 carried ENVIRONMENT and omitted HANDOFF instead. Two lists, neither complete, each looking authoritative. Both now carry both. PROCESS correction: the amendment rule added at15e90fdclaimed a document replaced rather than amended loses content, citing HANDOFF.md. Checked today against both archived handoffs - it had lost nothing. The dihedral gap, the three artifact classes, the cross-project mail question and the toolchain probe recipe are all still present. The hazard is structural, a rewrite has no diff, but the loss I asserted did not happen. Corrected rather than left standing. Applied by anchored patcher, all-or-nothing across both files. Suite 643 passed, oracle intact. Documentation only. Known cosmetic defect: the baseline paragraph in section 3 now has two adjacent bold spans and a line over 80 columns, because the anchor stopped mid-paragraph. Renders correctly. To be rewrapped with the next patch that touches the file.
732 lines
38 KiB
Markdown
732 lines
38 KiB
Markdown
# HANDOFF
|
||
|
||
**This document is rewritten in place each session. It is state, not a log.**
|
||
|
||
It is the only handoff you need to read. Dated handoffs in `docs/archive/` are
|
||
historical and are not required reading — do not diff them against this to work
|
||
out what is true. If something here is wrong, correct it here.
|
||
|
||
Last updated 2026-09-13, after the documentation audit.
|
||
|
||
This line used to carry the commit hash of its own rewrite. It cannot: the
|
||
hash is not known until the commit is made, so the value was always the
|
||
*previous* commit and section 3 drifted two commits behind without anyone
|
||
noticing. Date only, from here.
|
||
|
||
---
|
||
|
||
## 0. Read these first, in this order
|
||
|
||
This document is not the first thing to read, despite being the handoff.
|
||
|
||
1. **`docs/PROCESS.md`** — how work is done here: who runs what, on which
|
||
machine, with which tools. **It constrains everything below.** Reading it
|
||
eleventh cost this session three sets of instructions the operator could
|
||
not execute.
|
||
2. **`docs/STAGING-STATE.md`** — what is true on the host right now.
|
||
3. **`docs/FAILURES.md`** — what has already gone wrong and why.
|
||
4. This document — where the code stands.
|
||
5. **`docs/DIVERGENCES.md`** — requirements that stand and are not met.
|
||
6. **`docs/ENVIRONMENT.md`** — the specification an instance is built to. It was
|
||
missing from this list for three weeks while `PROCESS.md` §8 carried it and
|
||
omitted this document instead, so a successor following either list alone
|
||
missed something. Both lists now carry both.
|
||
7. `docs/ACCEPTANCE.md`, `docs/STOCK.md`, `docs/PRECISION.md` — the
|
||
specifications the code is held to.
|
||
8. **`docs/IDENTITY-CONTRACT.md`** — how a visitor's identity arrives, and the
|
||
boundary that keeps membership out of this application. The only document
|
||
here written to be read by someone who does not work on this repository.
|
||
9. `docs/CONSUMER_INTERFACE_GATES.md` — cross-project concerns that are open,
|
||
with owners. Non-normative. **Read it before proposing any integration with
|
||
a membership or geography system**; several tempting ones are recorded there
|
||
as things to specifically not build yet.
|
||
|
||
`deploy/` holds the systemd unit and the nginx vhost as they actually run,
|
||
imported verbatim on 12 SEP and checksum-proven equal to CT 100 and CT 101. The
|
||
repository is the single source of truth: read a container when you suspect it
|
||
has drifted, then fix the drift here rather than on the host.
|
||
|
||
When documents disagree, `STAGING-STATE.md` wins on facts about the host and
|
||
`FAILURES.md` wins on what was actually observed. This one is corrected.
|
||
|
||
---
|
||
|
||
## 1. Invocation — read before typing anything
|
||
|
||
These are facts, not examples. Getting one wrong produces an error that looks
|
||
like a problem with the repository or the containers.
|
||
|
||
**`mechcomp` has `nologin` as its shell.** Use `runuser -u mechcomp -- <cmd>`,
|
||
which executes directly. **`su - mechcomp` cannot work** and fails with
|
||
`This account is currently not available` — a message that looks like a broken
|
||
account and is not. See F-035.
|
||
|
||
**The interpreter is `/var/www/mechcomp/venv/bin/python`, not `python3`.**
|
||
`Makefile` line 3 sets `PY ?= venv/bin/python`, and `make deps` installs
|
||
pytest, Shapely, numpy and `mechcomp` itself into that virtualenv. System
|
||
`python3` has none of them and fails at the first import (F-036).
|
||
|
||
**All repository operations run as `mechcomp`, never root.** The clone is at
|
||
`/var/www/mechcomp` in CT 100. Never add a git `safe.directory` exception to work
|
||
around an ownership complaint; fix the ownership (F-008).
|
||
|
||
**`mechcomp`'s home *is* the working tree.** Anything writing to `$HOME` writes
|
||
into the repository. `.cache/`, `.local/` and `.ssh/` are in `.gitignore` for
|
||
that reason (F-029). Git identity is set `--local` for the same reason —
|
||
`--global` would write into the tree.
|
||
|
||
**`pct exec` runs no shell.** A glob, a redirect, a pipe or an `&&` in a `pct
|
||
exec` command is expanded by the *host* shell, against the host's filesystem,
|
||
and whatever literal survives is handed to the container as an argument. Wrap
|
||
anything that needs a shell:
|
||
|
||
pct exec 101 -- sh -c 'grep -n listen /etc/nginx/sites-enabled/*'
|
||
|
||
Unwrapped, that glob expands on `srv-b` — where the path does not exist — so the
|
||
container receives a literal `*` and reports "No such file or directory", which
|
||
reads as a broken container and is not. Third of the same kind after F-035 and
|
||
F-036: the tool was invoked wrongly and the error described the wrong subject.
|
||
|
||
**`verify.sh` is mode `100644`.** Invoke it as `bash tools/reference-toolchain/verify.sh`,
|
||
never `./tools/...`.
|
||
|
||
**Gitea SSH is port 42022.** Remotes need
|
||
`ssh://git@host:42022/owner/repo.git`; the `git@host:path` shorthand cannot carry
|
||
a port. CT 100 pushes with deploy key `srv-b-ct100`.
|
||
|
||
**Deploy *tokens* in Gitea are account-level**, under user Settings. Repository
|
||
settings offer deploy *keys* only.
|
||
|
||
**Files reach CT 100 by upload, then `pct push`, then `chown`.** `pct push`
|
||
writes as root, so `pct exec 100 -- chown mechcomp:mechcomp <path>` immediately
|
||
after, every time.
|
||
|
||
**Every network command needs an explicit timeout** (F-030). One without hung the
|
||
operator's shell.
|
||
|
||
**Logs are in `journalctl`**, not `/var/log/`. Proxmox ships without `rsyslog`
|
||
(F-024).
|
||
|
||
**Long-running jobs go to `systemd-run --unit=<name> --collect`**, not `nohup` or
|
||
`setsid`. `pct exec` tears those down when it exits; systemd owns the job and the
|
||
output lands in the journal.
|
||
|
||
**Docker runs as root in CT 100 only.** `mechcomp` cannot reach the daemon. CT 101
|
||
has no `keyctl` and cannot run it at all.
|
||
|
||
**Assert the guest is running before interpreting any `pct exec` result** (F-027).
|
||
A command that fails because the container is stopped otherwise reads as a pass.
|
||
|
||
Useful canonical form:
|
||
|
||
```bash
|
||
if [ "$(pct status 100 | awk '{print $2}')" != "running" ]; then
|
||
echo "CT 100 NOT RUNNING - stop here."
|
||
else
|
||
pct exec 100 -- runuser -u mechcomp -- git -C /var/www/mechcomp status --short
|
||
fi
|
||
```
|
||
|
||
---
|
||
|
||
## 2. The operator's constraint
|
||
|
||
**CIVICVS has a shell on `srv-b` and a browser-based file manager. Nothing
|
||
else.**
|
||
|
||
No workstation git. No SSH into a container. No IDE. No `scp`. Files arrive by
|
||
upload to `/root/incoming` on `srv-b`; every command runs in that one shell.
|
||
`pct exec` from that shell reaches all three containers — that is the container
|
||
path, and it is not a limitation.
|
||
|
||
An assistant that assumes otherwise produces instructions the operator cannot
|
||
execute. This has happened repeatedly.
|
||
|
||
**Deliver code by upload, not by paste.** A sixty-line heredoc containing
|
||
em-dashes and nested code fences was mangled by the browser terminal. Everything
|
||
delivered since as a tarball — with a stated checksum, file list, and what it
|
||
overwrites — has worked without exception. State what the archive contains, where
|
||
it expands, and what it overwrites, every time.
|
||
|
||
When a delivery supersedes an earlier one, **ship every file in the set**, not
|
||
just the changed one, so the resulting state is unambiguous.
|
||
|
||
---
|
||
|
||
## 3. Where things stand
|
||
|
||
### Infrastructure — complete, do not revisit
|
||
|
||
`srv-b`, Proxmox VE 8.4.0, standalone.
|
||
|
||
| CT | Name | Address | Role |
|
||
|---|---|---|---|
|
||
| 100 | `mechcomp` | `10.20.0.10` | application, worker, Docker |
|
||
| 101 | `mcproxy` | `10.20.0.11` | reverse proxy, TLS |
|
||
| 102 | `kane-fabric` | `10.20.0.12` | **separate project** |
|
||
|
||
All on `vmbr1`, a portless service bridge. `srv-b` is router and bastion:
|
||
internet → WireGuard → `srv-b` → containers. Containers cannot reach the home
|
||
LAN and cannot send mail.
|
||
|
||
`ct-baseline.sh` is read-only, runs any time, exits non-zero on divergence.
|
||
Installed at `/usr/local/sbin/`. **Last run 2026-08-18: 62 passed, 0 failed —
|
||
which predates the DNAT rule and the placeholder service replacement, both of
|
||
which `PROCESS.md` §9a names as requiring a run. Overdue. See DIV-006.** **A property it
|
||
does not check is not part of the standard** — that is what makes conformance
|
||
terminate rather than recur.
|
||
|
||
**Settled decisions. Do not reopen any of these:**
|
||
|
||
- **Webmin is the operator's only remote access.** He works from Webmin on
|
||
`wg-pk`, the WireGuard hub, and from there into the `srv-b` shell — not the
|
||
other way round. Questioning this wasted a session once and was questioned
|
||
again on 11 SEP, because the decision had been recorded without the fact it
|
||
rests on. The fact is above. Do not ask again.
|
||
- **`dev.infra` does not exist on the internet.** It is a hosts-file name on
|
||
`srv-b`, CT 100 and CT 101 only, and CT 101's leaf is signed by a local
|
||
staging CA. Never tell the operator to open a `dev.infra` URL or a
|
||
`10.20.0.x` address in a browser: `10.20.0.0/24` is a portless bridge with
|
||
LAN traffic dropped. The public name is `dev.mechcomp.kane-il.us` and it
|
||
needs `WORK-ORDER-004`.
|
||
- Backup is deliberately postponed. Entry condition: `ct-baseline.sh` exits 0.
|
||
Do not raise it again.
|
||
- `4x` is the end of the N-strap family.
|
||
- The `--full` toolchain gate's Docker root-ownership hazard is understood and
|
||
handled by mounting read-only. See §6.
|
||
- **Accuracy criterion: 0.01 mm over the entire set.** Set by CIVICVS on 20 AUG.
|
||
This is the standard the software is held to, and it is met with two orders of
|
||
margin on every dimensional quantity. See `docs/PRECISION.md`.
|
||
|
||
### The port — COMPLETE
|
||
|
||
The suite is green and the composer is live.
|
||
|
||
**This section no longer records a commit hash or a test count.**
|
||
`git rev-parse HEAD` and `make test` report them. The line that used to carry
|
||
the hash was always the *previous* commit — the hash is not known until the
|
||
commit is made — which is how this section drifted two behind without anyone
|
||
noticing, exactly as the header says of its own date. Both are now derived
|
||
rather than transcribed.
|
||
|
||
The 30 expected failures are gone, resolved rather than suppressed, by the
|
||
tolerance model in `docs/ACCEPTANCE.md`. A red `make test` now means something
|
||
is actually wrong.
|
||
|
||
**The composer is live.** `mechcomp.service` in CT 100 serves it on
|
||
`10.20.0.10:8770`, behind nginx on CT 101, proven `HTTPS 200` end to end.
|
||
`mechcomp-placeholder.service` is disabled and retained as the rollback.
|
||
|
||
| Module | Ported from | Contents |
|
||
|---|---|---|
|
||
| `geom/primitives.py` | `sb-geom.scad` | **Pure geometry only.** Vectors, degree trig, exact polyline distance, corner-radius derivation, monotone solver. **`Geo` and `Member` are NOT here** — an earlier version of this table said they were, and two modules were read on the strength of it |
|
||
| `geom/records.py` | `sb-geom.scad` | `Geo`, `Member`, `place`, `local_rect`, sleeve and cavity paths, reach. The other half of `sb-geom` |
|
||
| `geom/rounding.py` | BOSL2 `92d697c2` | `round_corners`, `_circlecorner`, `arc`, `segs`, `deduplicate`, `path_merge_collinear`, `is_collinear` |
|
||
| `geom/region.py` | BOSL2 regions | Shapely booleans, nesting-parity decomposition, area, simplicity, hull, mitred offset, cleaning |
|
||
| `geom/join.py` | `sb-join.scad` | Butt joints, hull caps, fillets, derived bore, ring fit, section assembly |
|
||
| `geom/report.py` | `sb-report.scad` | Checks-as-values, metrics, five universal checks, `ProfileRejected`, report formatting |
|
||
| `geom/core.py` | `sb-core.scad` | PROFILE record, failure representation, centred assembly |
|
||
| `geom/arrangements.py` | `sb-profiles.scad` | Ring, spoke and fin arrangements, all N-generic |
|
||
| `profiles/_common.py` | both generators | Assembly pipeline and the eight shared base checks |
|
||
| `profiles/four_x.py` | `strap-beam-4x.scad` | Five profiles, defaults, 4x base checks |
|
||
| `profiles/three_x.py` | `strap-beam-3x.scad` | Six profiles, defaults, 3x base checks |
|
||
| `profiles/__init__.py` | — | `build()`, `ProfileRejected`, family registry |
|
||
| `stock.py` | — | COTS stock descriptor: `RectStock`, `RoundStock`, `Fit`, `Provenance`. A leaf module — imports nothing from `mechcomp`. See `docs/STOCK.md` |
|
||
| `design_record.py` | — | What a model was made from, at full precision, sufficient to regenerate it. `input_id` and `build_id`, and `Author` — recorded, rendered with its own limit, and in neither hash |
|
||
| `svg.py` | — | Cross-section as SVG, separately classed layers for material, cavities and stock |
|
||
| `stl.py` | — | A member as a sealed binary mesh: two triangulated caps and a quad strip down the boundary. **No CAD kernel** |
|
||
| `web/app.py` | — | The composer. Standard library HTTP server; binding from `/etc/mechcomp/mechcomp.env`. Serves `/`, `/api/build` and `/m/stl` |
|
||
|
||
`make deps` is complete in CT 100 and **must not be re-run**.
|
||
|
||
---
|
||
|
||
## 4. What to do next
|
||
|
||
**Nothing is blocked. The composer is live and the suite is green; everything
|
||
below is new capability.**
|
||
|
||
**CIVICVS stated the goal on 22 AUG: he will not print anything that is not
|
||
fully configurable.** The compiler is a half-visual composer — the T encloses
|
||
poly pallet straps in a T, the Y takes a metal electrical conduit core at its
|
||
centre. That reorders what follows, because a fixed-profile STL is not a
|
||
deliverable he wants. Two consequences:
|
||
|
||
- **The bore is a declared interface**, with a diameter and a fit class,
|
||
specified by what goes through it. It is not a byproduct of the inside
|
||
walls. Conduit for the Y, not only rebar.
|
||
- **The catalogue front end is the product, not a convenience** that comes
|
||
after export. `ROADMAP.md` §4 orders it third; that ordering predates this
|
||
and should be read against it.
|
||
|
||
**Priority order, decided 11 SEP when CIVICVS delegated it. Items 1 and 2 were
|
||
reversed on 12 SEP, for the reason given under item 1.** The reasoning is given
|
||
so a successor can disagree with the argument rather than only the sequence.
|
||
|
||
1. **An authorship field in the design record. Landed at `48d5665`.** One field,
|
||
and a door that narrows rather than closes. The original argument was that
|
||
records created before it exists can never be attributed. That was too
|
||
strong, and it was too strong *because* the field is excluded from both
|
||
hashes: 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 now tie to either. Recoverable, not free. It still went first,
|
||
because it is one field and one parser branch, and doing it first means the
|
||
first coupon off the printer carries its author.
|
||
|
||
The person is identified by an email address. A handle may be chosen later
|
||
and does not replace it. Nothing verifies the address and the record says so
|
||
on the line it appears on.
|
||
2. **STL export. Landed at `0545b79`; the bounded export route at `cdde394`.**
|
||
No new dependency and no kernel — a member is prismatic by definition, so an
|
||
STL is two triangulated caps plus a quad strip down the boundary. It was the
|
||
only item producing *physical* feedback: everything else is verified against
|
||
a frozen oracle, and a printed coupon is the first test against reality, and
|
||
the first real measurement of whether `fit_clearance_mm` suits the operator's
|
||
printer.
|
||
|
||
**`length_view` was missing from `COMMON_GROUPS`**, which would have made the
|
||
Full Length branch unreachable from the composer and every export a silent
|
||
100 mm preview — a file that looks right, slices right, and is the wrong
|
||
object. Fixed before the route shipped. The sweep is `model_length_mm(p)`,
|
||
the value already published as `LENGTH_MM`, so the record's `VOLUME_MM3` and
|
||
`MASS_G` describe the file beside them rather than something else.
|
||
|
||
`/m/stl` ships **open**, because `/m/` is not yet gated and no membership
|
||
system exists to gate it. Carried as open question 10 rather than justified.
|
||
3. **Per-member stock — the conduit core.** What CIVICVS asked for on 22 AUG and
|
||
still cannot be done: `Geo` is global to a build, so every member is the same
|
||
rectangular strap by construction. Reaches into `join.py` and
|
||
`arrangements.py`; the first change that puts the oracle's own geometry at
|
||
risk rather than merely near it.
|
||
4. **Persistence.** `MECHCOMP_DATA_DIR=/var/lib/mechcomp` has been declared since
|
||
staging and nothing has ever written to it. Every design currently exists only
|
||
for the duration of one HTTP request. This is the prerequisite for a library
|
||
of saved designs. It is **no longer** the prerequisite for access control,
|
||
which has left this roadmap entirely — but it should be built knowing that a
|
||
saved design belongs to a person whose identity arrives from outside this
|
||
application (`IDENTITY-CONTRACT.md` §3).
|
||
|
||
The filesystem is the first store, not a database. Design records are already
|
||
plain text, already content-addressed by `input_id`, and already readable by
|
||
someone with none of this software installed. A directory of them is a store
|
||
with perfect provenance and no schema to migrate. SQL earns its way in when
|
||
there is a query that walking files cannot answer — "every design by this
|
||
author since March" is that query, and it arrives with membership, not
|
||
before.
|
||
5. ~~**ACL.**~~ **Deleted, not deferred.** Authorisation lives upstream, at the
|
||
last proxy hop before this application, decided against a membership system
|
||
this repository knows nothing about. The compiler will not have a user table,
|
||
a login form, a session, or a group name in any form — including as a
|
||
configuration value, which is how that leak arrives by the side door. See
|
||
`IDENTITY-CONTRACT.md` §9. An item left the roadmap rather than moving down
|
||
it.
|
||
|
||
The service is world-reachable and unauthenticated today, and `/m/` is not
|
||
yet gated, so STL export will ship open. An earlier version of this item
|
||
called that "acceptable for a development name". **It is not.** The name is
|
||
production, `dev` abbreviates *Mechanical Compiler Developers*, and it
|
||
appears on printed material. The posture is unchanged; the excuse is
|
||
withdrawn. Carried openly as open question 10 instead of justified.
|
||
|
||
**Assemblies are not on this list and should not be added.** See `PRECISION.md`
|
||
§7: positioning is field work, ruled out by design on 11 SEP.
|
||
|
||
Roadmap items — read `ROADMAP.md` before starting any:
|
||
|
||
- Dihedral parameterisation. If two panels meet at 137°, none of the eleven
|
||
profiles gives you a member for it. This is the highest-value gap and it
|
||
blocks a catalogue that could otherwise not serve the reference structure.
|
||
- Rebar- and conduit-core bore as a declared interface. See above.
|
||
- **Per-member stock.** This is what the conduit core actually needs. `Geo` is
|
||
global to a build, so every member is the same rectangular strap by
|
||
construction and the Y's centre cannot hold something of a different shape.
|
||
Moving stock from the build to the placement reaches into `join.py` and
|
||
`arrangements.py`, and it is the first change where the oracle's own
|
||
geometry is genuinely at risk rather than merely nearby.
|
||
- **The composer, deepened.** It exists, serves, and exports STL; it does not
|
||
persist anything or show a bill of materials. `payload["defaults"]` is sent to
|
||
the browser and nothing reads it — dead weight, and it is what made a
|
||
verification grep lie on 11 SEP.
|
||
- STEP export. Needs a CAD kernel and none is installed in CT 100 (§5).
|
||
STL needs none — a member is prismatic by definition. See §5.
|
||
- `mechcomp-worker.service`. `src/mechcomp/worker/` is still a 36-byte stub and
|
||
nothing needs a worker yet.
|
||
- Nodes (non-prismatic) and panels (sheet). No representation exists for either.
|
||
|
||
**`WORK-ORDER-004` is closed. The composer is public at
|
||
`dev.mechcomp.kane-il.us`.** Executed 11 SEP at `1fdb115`: browser, DNS, TLS on
|
||
`wg-pk`, the WireGuard tunnel, a DNAT on `srv-b` scoped to the hub as source,
|
||
CT 100. No WireGuard change and no route were made — the hub's peer entry for
|
||
`srv-b` is still a `/32`, as all twenty are — because every vhost there proxies
|
||
to a tunnel address directly and following that convention removed the only step
|
||
that could have locked the operator out of `srv-b`.
|
||
|
||
An earlier version of this paragraph said the work was pending and that CIVICVS
|
||
could not see any of it. It was written thirty-two minutes after the ingress
|
||
closed, in the same session, and was wrong the moment it was committed: the §4
|
||
rewrite was made against the work order's old state rather than its new one.
|
||
Corrected 12 SEP. This is the staleness §0 exists to prevent, and it happened
|
||
anyway, inside one session.
|
||
|
||
Note for anyone tempted to shortcut a test print: the OpenSCAD reference in
|
||
`legacy/` does export printable STL today — `sb_extrude_section` does a
|
||
`linear_sweep` and `legacy/openscad/README.md` documents the invocation. That
|
||
is not what he asked for, and offering it again would be repeating a mistake
|
||
already made once.
|
||
|
||
---
|
||
|
||
## 5. Facts established by porting
|
||
|
||
### The public entry point is `mechcomp.profiles`, not `mechcomp.geom`
|
||
|
||
`conftest.py` does `importorskip("mechcomp.profiles")` and requires a `build`
|
||
attribute. `build(family, profile, params) -> Result` and `ProfileRejected` are
|
||
exported from `mechcomp.profiles`. Params use the OpenSCAD parameter names
|
||
unchanged.
|
||
|
||
**`params` carries only a case's overrides.** Everything else comes from the
|
||
family's declared defaults, which is why those defaults live in the port rather
|
||
than the test harness.
|
||
|
||
### Defaults are per-family, not shared
|
||
|
||
`ring_corner_radius_mm` is **2.00 in 3x** and **1.25 in 4x**. The two generators
|
||
declare their own parameter blocks and they are not identical. Do not assume a
|
||
value read from one file applies to the other. 4x also declares
|
||
`rectangle_aspect`, which 3x has no equivalent of, and its base-check list is
|
||
correspondingly one entry longer.
|
||
|
||
### Check order is the reporting order
|
||
|
||
The reference reports the **first** failing check, not an aggregate. Base checks
|
||
run before profile checks, and both run before any geometry is measured — a
|
||
builder that failed returns an empty profile, and `centred()` on an empty profile
|
||
has no members to measure.
|
||
|
||
### Report values are rounded to six significant figures
|
||
|
||
The oracle records what OpenSCAD's `echo` printed — C's `%g` at default
|
||
precision — not full-precision geometry. `echo_num()` does this; `echo_vec()`
|
||
handles vectors, which reach the oracle as strings like
|
||
`'[20.5209, 20.5209, 20.5209]'`.
|
||
|
||
Load-bearing, not cosmetic — and it is now the basis of the tolerance model.
|
||
Because the oracle holds six significant figures, the last recorded digit of
|
||
an area near 200 mm² is worth 0.001 mm², and the whole measured disagreement
|
||
between port and reference is one, two or three units in that place. See
|
||
`docs/ACCEPTANCE.md`.
|
||
|
||
Historical note, since it was the visible symptom for two sessions:
|
||
`VOLUME_MM3` ends in `_MM3`, so the old comparison judged it at the lengths
|
||
tolerance of 1e-4 against a magnitude near 20,000 — 5e-9 relative, demanded
|
||
of discretised geometry, by accident of key naming.
|
||
|
||
### Angles are degrees; the arbitrary constants are contract
|
||
|
||
OpenSCAD trigonometry is in degrees. The port keeps degrees throughout with
|
||
explicit `cos_d`/`sin_d`/`tan_d` so every expression matches its source line. The
|
||
44 solver iterations, the 0.999 and 0.98 scale factors, the 0.05/179.95 degree
|
||
cutoffs and the 1e9 sentinel are reproduced exactly. They produced the frozen
|
||
values.
|
||
|
||
### Environment
|
||
|
||
Shapely 2.1.2 on GEOS 3.13.1 — keep these in step, boolean results on
|
||
near-degenerate geometry can shift between GEOS releases. numpy 2.4.6. Shapely is
|
||
in `requirements-base.txt`, the 2D path's own dependency set.
|
||
|
||
**No CAD kernel is installed in CT 100.** `cadquery`, `OCP` and `build123d` are
|
||
all absent, though `requirements-cad.txt` says it is installed by default. That
|
||
is the strictest environment for developing the 2D path.
|
||
|
||
**STL export does not need one, and an earlier version of this section implied
|
||
it did.** A member here is prismatic by definition — a 2D section swept along a
|
||
straight axis — so an STL is two triangulated caps plus a quad strip down the
|
||
boundary. That is not a kernel problem.
|
||
|
||
Verified in CT 100 on 11 SEP: `shapely.constrained_delaunay_triangles` is
|
||
present in Shapely 2.1.2 and triangulates a polygon **with a hole** correctly —
|
||
summed triangle area equals the polygon area exactly, and no triangle falls
|
||
inside the hole. The second check is the one that matters: a triangulator that
|
||
fills bores would produce an STL that looks right in a slicer and prints solid
|
||
where the conduit goes.
|
||
|
||
STEP does need the kernel. `requirements-cad.txt` pins `cadquery>=2.4` for it.
|
||
|
||
### What is verified against the reference
|
||
|
||
**Every dimensional quantity is exact to 1e-4 mm across all 123 cases, with no
|
||
exceptions.** `ENVELOPE_X_MM`, `ENVELOPE_Y_MM`, `MIN_WALL_ACTUAL_MM`, and every
|
||
profile extra: `AF_*`, `FIN_*`, `SPOKE_*`, `RING_*`, `T_*`. Every count is exact.
|
||
|
||
**All ten rejections fire correctly**, including the bespoke A Frame and T paths,
|
||
which were written from the join primitives rather than from a shared
|
||
arrangement. That is strong evidence those two are structurally right.
|
||
|
||
Only `SECTION_AREA_MM2` and its two derivatives ever disagree. See §7.
|
||
|
||
---
|
||
|
||
## 6. Probing the reference directly
|
||
|
||
The pinned toolchain image is in CT 100 and OpenSCAD will echo whatever you ask
|
||
it. This settled F-034 in four exchanges and will settle any later disagreement
|
||
the same way. **Prefer it to reasoning.**
|
||
|
||
Mount the tree **read-only** and keep the probe on a separate writable mount —
|
||
Docker runs as root, and a read-write mount against a `mechcomp`-owned tree is
|
||
the F-033 hazard. A nested single-file mount inside a read-only mount fails with
|
||
`EXIT=125`; give the probe its own directory.
|
||
|
||
```bash
|
||
pct exec 100 -- bash -c 'mkdir -p /tmp/probe && cat > /tmp/probe/p.scad << "EOF"
|
||
include </repo/legacy/openscad/lib/sb-core.scad>
|
||
$fn = 48;
|
||
g = sb_geo(15.875, 0.508, 1, 0.25, 1.20, 1.20, 1.20, 1.20);
|
||
echo(str("PROBE=", sb_area([sb_sleeve_path(sb_member(0,0,0), g)])));
|
||
EOF
|
||
docker run --rm -v /var/www/mechcomp:/repo:ro -v /tmp/probe:/probe -w /probe \
|
||
mechcomp/reference-toolchain:8.0.0 \
|
||
openscad -o /tmp/o.stl --export-format=asciistl p.scad 2>&1 | grep -E "PROBE|ERROR"'
|
||
```
|
||
|
||
`EXIT=1` is normal — STL export fails on 2D geometry, the echoes still arrive.
|
||
|
||
**The oracle itself is stable.** The gate was run on 19 AUG: all 123 cases
|
||
regenerate byte-identically inside the pinned image, 113 accepted and 10
|
||
rejected, the only diff being `frozen` and the hash containing it.
|
||
`verify.sh --full`'s restore path was broken, is now fixed, and is **still
|
||
unexercised** (F-033).
|
||
|
||
**Instrumenting the port is equally cheap.** Monkeypatching
|
||
`mechcomp.geom.rounding._circlecorner` to print its half-angle and segment count
|
||
turned the F-034 diagnosis from inference into measurement in one command. The
|
||
module calls it through the module global, so patching the attribute works.
|
||
|
||
---
|
||
|
||
## 7. F-034 — measured, decided, closed
|
||
|
||
**The port is exact. The reference is noisy.** That is the opposite of what the
|
||
entry previously implied, and it changes what can be done about it.
|
||
|
||
`_circlecorner` computes `raw = (90 - angle)/180 * segs(r, None, fn)` and takes
|
||
`ceil(raw)`. At `$fn = 48` a 90° corner has half-angle 45, and `(90-45)/180*48`
|
||
is **exactly 12**. A ceiling on an exact integer is a knife edge.
|
||
|
||
Measured: the port prints `half=45 raw=12 ceil=12` at `%.17g` — landing dead on
|
||
the integer, every call, both families. The reference's arithmetic lands a hair
|
||
under 45° at some corners, pushing `raw` fractionally above 12 and the ceiling to
|
||
13. On Rectangle: **reference envelope 50 vertices, port 48** (`13+13+12+12`
|
||
against `12×4`).
|
||
|
||
**Do not try to fix this.** Rounding before the ceiling was tried and reverted —
|
||
it fixes Y exactly and breaks Three-Fin, because Three-Fin has the same asymmetry
|
||
and the oracle records it. There is nothing to correct on the port's side.
|
||
|
||
### Scale of the effect
|
||
|
||
30 of 113 accepted cases affected. 83 exact. Only three keys ever breach:
|
||
`SECTION_AREA_MM2` (29), `VOLUME_MM3` (30), `MASS_G` (22) — and volume is area
|
||
×100, mass is volume ×density/1000, so each case has **one** discrepancy reported
|
||
three times. **Worst relative error 2.24e-05.**
|
||
|
||
By profile: Three-Fin 10, Y 7, A Frame 6, Rectangle 5, T 1, Four-Fin 1. None on
|
||
Equilateral, General Triangle, Square, Diamond or Cross.
|
||
|
||
An earlier version of this line read Three-Fin 9 and A Frame 5, summing to 28
|
||
against the stated total of 30. Measured and corrected — `docs/ACCEPTANCE.md`
|
||
§7 and F-034's resolution carry the same figures. The correction had been
|
||
recorded in both of those for three weeks while the wrong numbers stayed here,
|
||
in the document a successor reads first.
|
||
|
||
### Why the test fails when the geometry is fine
|
||
|
||
At `$fn = 48` a chord deviates from its true arc by `r(1 − cos 3.75°)` =
|
||
`r × 2.1413e-3`: **2.68 µm at r=1.25, 4.28 µm at r=2.00.** The port and the
|
||
reference differ from *each other* by at most ~0.4 µm. Against the 0.01 mm
|
||
criterion (§3) that is two orders of margin.
|
||
|
||
The tests fail because `VOLUME_MM3` is compared at 1e-4 **absolute** against a
|
||
magnitude near 20,000 — demanding 5e-9 relative agreement from discretised
|
||
geometry. `3x/Y/steel0.79` breaches on `VOLUME_MM3` alone while its area passes:
|
||
the same discrepancy, judged by two wildly different standards by accident of key
|
||
naming.
|
||
|
||
### The options, and the constraint
|
||
|
||
**Do not edit `tolerance` in the oracle JSON.** It is inside the hashed document;
|
||
`test_integrity_hash` covers everything but `fixtures_sha256`. Editing it breaks
|
||
that test by design.
|
||
|
||
The change belongs in `test_oracle.py`, which is not hashed:
|
||
|
||
1. **Scale-aware bounds for the three discretisation-limited keys**, derived from
|
||
the 0.01 mm criterion and the section perimeter. Most faithful to where the
|
||
error originates. Recommended.
|
||
2. **A relative floor** — pass if within absolute tolerance *or* ~1e-4 relative.
|
||
Simplest; loosens `SECTION_AREA_MM2` from 0.001 to ~0.02 mm².
|
||
3. **Accept 30 known failures.** Honest but `make test` is never green and a real
|
||
regression hides among them.
|
||
|
||
**Closed 22 AUG.** Option 1's *scope* was kept — the three keys, the change in
|
||
`test_oracle.py`, the oracle JSON untouched — but its *derivation* was
|
||
rejected on measurement and replaced.
|
||
|
||
Measuring `|dA| / P`, the boundary displacement that would produce each area
|
||
discrepancy, gives a maximum of 1.434e-05 mm: one seven-hundredth of the
|
||
0.01 mm criterion. A `perimeter × 0.01` bound would have been 1.86 mm² at the
|
||
smallest section and 4.54 mm² at the largest, between 1,800 and 4,500 times
|
||
the worst real discrepancy, and would have caught nothing. Perimeter also
|
||
anti-correlates with the error — the largest discrepancy is at P = 209 mm and
|
||
the smallest at P = 454 mm — because the error is driven by how many corners
|
||
tip from 12 segments to 13, not by boundary length.
|
||
|
||
What replaced it: **eight units in the last place of the oracle's
|
||
six-significant-figure record**, for those three keys only. Worst case uses
|
||
37.5% of its bound, mutation-tested before landing. **`docs/ACCEPTANCE.md` is
|
||
the specification** — read it before touching any tolerance, and note E-4:
|
||
everything that positions material stays exact at 1e-4 mm and must never be
|
||
moved into that regime.
|
||
|
||
---
|
||
|
||
## 8. Method that has earned its keep
|
||
|
||
**Read the pinned source; do not recall it.** BOSL2 was fetched at
|
||
`92d697c2856de2fed93a33e858068589cefc2898` and read directly. Every function
|
||
examined had a detail that mattered and that recollection would have got subtly
|
||
wrong. The same applied to the generators: reading `sb-profiles.scad` before
|
||
writing `four_x.py` confirmed the call signatures rather than inferring them from
|
||
call sites.
|
||
|
||
**Check the invocation before concluding anything about state.** A `su` that
|
||
failed identically on two commands read as a repository problem and was an
|
||
invocation problem. That is the F-027 pattern and it cost a session's opening
|
||
exchange.
|
||
|
||
**Verify your own arithmetic before shipping it.** A claim in `PRECISION.md` that
|
||
a 50 mm radius needs `facets ≈ 460` was wrong — deviation falls with the *square*
|
||
of the segment angle, so the count grows with the square root of radius and the
|
||
answer is 158. A three-line script caught it.
|
||
|
||
**Mutation-test every suite before landing it.** Break the code deliberately and
|
||
confirm the tests notice. This found real gaps in five of six slices. It also
|
||
caught a malformed mutation of mine — cutting the cavities twice is idempotent.
|
||
**A surviving mutation is sometimes a bad mutation, not a test gap.**
|
||
|
||
**Set `PYTHONDONTWRITEBYTECODE=1` and clear `__pycache__` between mutations.** A
|
||
stale `.pyc` once masked a real defect, and every mutation result reported
|
||
before that was caught was optimistic by an unknown amount. A mutation that
|
||
survives because the test ran old bytecode is indistinguishable from one that
|
||
survives because the test is weak — F-027 in different clothes, and the reason
|
||
that rule is general rather than about one harness.
|
||
|
||
**Read-only before write.** Every command group where the answer was not certain
|
||
established the facts first.
|
||
|
||
**One task, one command group, wait for output.** Not a menu of next steps. If
|
||
you find yourself writing "and also", delete it.
|
||
|
||
---
|
||
|
||
## 9. What the project is for
|
||
|
||
Build the capacity to construct real structures — the reference case is a faceted
|
||
timber shell — from reclaimed and commodity materials, using whatever fabrication
|
||
is to hand. The compiler makes the pieces computable, qualifiable, and
|
||
reproducible by someone who was not present when they were designed.
|
||
|
||
**Codes and permitting are out of scope, deliberately.** The project records
|
||
physical claims, never verdicts. Measure and attest; never adjudicate.
|
||
|
||
Three artifact classes: **members** (prismatic, exist — the eleven profiles),
|
||
**nodes** (non-prismatic, no representation yet), **panels** (sheet, none yet).
|
||
|
||
**A node is not an assembly.** It is another artifact — non-prismatic, carrying
|
||
several declared interfaces — and it stays inside scope. What is out of scope is
|
||
positioning artifacts relative to one another, which is field work and belongs
|
||
to whoever is holding them. `PRECISION.md` §7 states the ruling and the reason.
|
||
|
||
The consequence is the project's actual obligation: **make each artifact
|
||
interchangeable.** Fully specified, independently reproducible, carrying its own
|
||
declared interfaces so the field can fit it to whatever it meets. The stock
|
||
descriptor and the design record exist for exactly that, and it is the standard
|
||
any new capability should be judged against.
|
||
|
||
**The output must eventually be sealed manifolds and printable STL.** The 2D
|
||
section is where manifold validity is decided, not downstream in the CAD kernel —
|
||
`is_region_simple` is already a build-blocking check, and a self-touching outline
|
||
extrudes into something untessellatable.
|
||
|
||
**`docs/PRECISION.md` states what this compiler does not do**: no assembly layer,
|
||
no structural analysis of any kind, prismatic shapes only, no toolpaths. That
|
||
document is scope-locked to additive and subtractive manufacturing. **Requests to
|
||
widen it should be refused, not accommodated** — see its §10.
|
||
|
||
---
|
||
|
||
## 10. Working with this operator
|
||
|
||
He is precise, keeps excellent records, and will tell you directly when you are
|
||
wrong — including when you are being unhelpful. Take it at face value; it is
|
||
accurate and it is not personal.
|
||
|
||
**He decides. Bring evidence and a recommendation, then stop.**
|
||
|
||
He has delegated all coding decisions — structure, algorithms, test design — and
|
||
does not want to be consulted on them. Still bring him scope, provenance, and
|
||
anything irreversible on the host. He runs every command, so nothing is
|
||
autonomous regardless.
|
||
|
||
When he says he does not understand something, the writing was unclear. Rewrite
|
||
it shorter; do not explain it again at greater length.
|
||
|
||
**He watches for scope creep and treats it as the primary risk** — "the greatest
|
||
enemy of software is not bugs, it's feature-creep". Documents and code are both
|
||
held to it. When in doubt, narrow.
|
||
|
||
---
|
||
|
||
## 11. Open questions, none blocking
|
||
|
||
| # | Question | Owner |
|
||
|---|---|---|
|
||
| 1 | Should `wg-pk` narrow `mynetworks` from `10.110.0.0/22` to explicit hosts? | CIVICVS, estate decision (F-025) |
|
||
| 2 | Backup strategy — USB, IPFS, optical? | CIVICVS |
|
||
| 3 | Where is the 3+ TB USB disk attached? | CIVICVS |
|
||
| 4 | Kane Fabric participant mail, send and receive | Cross-project |
|
||
| 5 | ~~Tolerance model for the three discretisation-limited keys~~ | **Closed 22 AUG.** See §7 and `docs/ACCEPTANCE.md` |
|
||
| 6 | Is the bore's fit class per-material, or one clearance for all inserts? | CIVICVS — raised by the conduit-core requirement, §4 |
|
||
| 7 | ~~Public ingress at `dev.mechcomp.kane-il.us`~~ | **Closed 11 SEP** at `1fdb115`. Executed without the WireGuard change the work order proposed |
|
||
| 8 | TLS renewal has never been observed to succeed for this name; first due before 2026-12-10 | CIVICVS |
|
||
| 9 | The composer has no acceptance criteria of its own — only the path to it does | Architect |
|
||
| 10 | The service is world-reachable and unauthenticated | CIVICVS; §4 item 5 makes this due rather than hypothetical now that it is published |
|
||
| 11 | Should `ct-baseline.sh` check the ownership of a service's working tree? | CIVICVS — host property, raised by F-037 |
|
||
| 12 | `ROADMAP.md` §5 names the production FQDN `mechanical-compiler.manufacturing.kane-il.us`. `dev.mechcomp.kane-il.us` is production and is on printed material | CIVICVS — naming |
|
||
| 13 | `ROADMAP.md` §5 says the production proxy is **not** on the WireGuard side, because the NAT was outbound-only. The DNAT at `1fdb115` resolved that, and public TLS now terminates on `wg-pk` | Architect — reconcile |
|
||
| 14 | `ROADMAP.md` §4a says no use case crosses the Kane Fabric boundary and that neither project defines the interface, deliberately. Membership-gated export is that use case, and `IDENTITY-CONTRACT.md` defines the identity half. The *siting* interface §4a is about remains undefined and should stay so | Architect — reconcile |
|
||
| 15 | `ROADMAP.md` §4 and this document's §4 are different lists. STL export appears only here; `ROADMAP.md` §3 orders dihedral ahead of the bore and neither §4 records it | CIVICVS — priority |
|
||
| 16 | `legacy/` holds the reference tier, not superseded code — the name invites a successor to treat it as dead | Architect — cosmetic |
|
||
|
||
Question 4 is real and unaddressed. Containers do not send mail by standard and
|
||
nothing can reach `vmbr1` from outside, so receiving has no path at all. It needs
|
||
a design conversation, not a configuration change.
|
||
|
||
---
|
||
|
||
## 12. Commit log
|
||
|
||
**Removed.** `git log --oneline` reports it, and the transcribed copy kept here
|
||
was ten commits behind within two days of being written — it ended at `a8081e1`
|
||
while the composer, the design record author field, STL export and the export
|
||
route had all landed above it.
|
||
|
||
Nothing derivable from the repository is kept in prose. `PROCESS.md` §10 states
|
||
the same rule for itself, and §3 above applies it to the commit hash and the
|
||
test count.
|