docs: one canonical handoff, rewritten in place; F-035

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.
This commit is contained in:
2026-08-19 12:33:50 -05:00
parent 009fcce61c
commit af196a26f5
4 changed files with 438 additions and 0 deletions
+48
View File
@@ -836,6 +836,53 @@ decision.
--- ---
### F-035 — `su` cannot run as a `nologin` service user
CT 100. Session handover.
**Observed:** a new assistant opened a session with
```
pct exec 100 -- su - mechcomp -c 'cd /var/www/mechcomp && git log --oneline -1'
pct exec 100 -- su - mechcomp -c 'cd /var/www/mechcomp && make test'
```
Both returned `This account is currently not available` and nothing else. With
the same message for the repository check and the test run, the output reads as
a broken clone or a broken container. Neither was true — the tree was clean and
the suite passed.
**Cause:** **Proven.** `mechcomp` is a service account:
```
mechcomp:x:999:996::/var/www/mechcomp:/usr/sbin/nologin
```
`su` starts the account's login shell, which is `nologin`, whose entire function
is to print that message and exit. The account is fine. `runuser -u mechcomp --`
executes the command directly without a login shell and works, which is what
every command in the porting sessions used.
**Correction:** use `runuser -u mechcomp -- <cmd>`. Never `su`. Recorded as an
explicit fact in `HANDOFF.md` §1 rather than left to be inferred from examples.
**Consequence:** Two things.
**This is the F-027 class again — a tool failure reading as a condition
failure.** Both commands failed identically and before touching anything, so the
message describes the invocation, not the state. When every command in a group
fails the same way, suspect the invocation before concluding anything about the
system.
**Operational facts must be stated, not demonstrated.** `runuser` appeared
throughout the previous sessions only inside example commands, so it could be
learned by pattern-matching but not by reading. That fails exactly when an
assistant composes a command from scratch, which is what happened here. The same
applies to `bash tools/...` over `./tools/...`, to Gitea's port 42022, and to
running every repository operation as `mechcomp`. All are now stated as facts in
`HANDOFF.md` §1.
---
## Open, not closed ## Open, not closed
| # | Status | | # | Status |
@@ -857,5 +904,6 @@ decision.
| F-032 | **Closed** 2026-08-18. No correction required; encoded in `ct-baseline.sh`. | | F-032 | **Closed** 2026-08-18. No correction required; encoded in `ct-baseline.sh`. |
| F-033 | **Corrected** 2026-08-19. Restore path unreachable under `set -e`. | | F-033 | **Corrected** 2026-08-19. Restore path unreachable under `set -e`. |
| F-034 | **Open** 2026-08-19. Reproduced unguarded; affects an unknown number of oracle cases. | | F-034 | **Open** 2026-08-19. Reproduced unguarded; affects an unknown number of oracle cases. |
| F-035 | **Corrected** 2026-08-19. Use `runuser`, never `su`; `mechcomp` is `nologin`. |
Everything else is closed with a proven cause and a proven correction. Everything else is closed with a proven cause and a proven correction.
+390
View File
@@ -0,0 +1,390 @@
# 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-08-19 at commit `009fcce`.
---
## 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.
**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.
**`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.
---
## 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: 62 passed, 0 failed. **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. Questioning it wasted a session.
- Backup is deliberately postponed. A backup of an unverified configuration
restores the confusion along with the data. 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.
### The port — shared layer complete
Gitea `main` at `009fcce`. CT 100 clean and matching. Suite: **230 passed, 236
skipped**. The 236 are the oracle acceptance tests; they skip because
`mechcomp.profiles.build` does not exist yet.
| Module | Ported from | Contents |
|---|---|---|
| `geom/primitives.py` | `sb-geom.scad` | Vectors, GEO and MEMBER, sleeve and cavity paths, exact polyline distance, corner-radius derivation, monotone solver |
| `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 |
`make deps` is complete in CT 100 and **must not be re-run**.
---
## 4. What to do next
Port the eleven catalogue profiles and the `build()` entry point.
**Read `strap-beam-4x.scad` before `strap-beam-3x.scad`.** The 4x file is
smaller, has five profiles, no rejection cases, and all five are direct calls
into the three arrangements that are already ported and verified. If those five
build and match the oracle, the port is confirmed end to end before you touch the
3x file, where all ten rejections and the bespoke profiles live.
**The public entry point is `mechcomp.profiles`, not `mechcomp.geom`.**
`conftest.py` does `importorskip("mechcomp.profiles")` and requires a `build`
attribute. The geometry lives under `mechcomp.geom`; `build(family, profile,
params) -> Result` and `ProfileRejected` must be exported from
`mechcomp.profiles`. Params use the OpenSCAD parameter names unchanged.
| Family | Profiles | Rejections |
|---|---|---|
| 3x | Equilateral Triangle, General Triangle, A Frame, Y, T, Three-Fin | 10 |
| 4x | Square, Rectangle, Diamond, Cross, Four-Fin | 0 |
---
## 5. Facts established by porting
### 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. `VOLUME_MM3` ends in `_MM3`, so `test_oracle.py`
compares it at the **lengths** tolerance of 1e-4, not the areas tolerance of
1e-3. Volume is 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: across all 113 accepted cases, recorded volume equals rounded area
times length to within 3.6e-12.
### 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 are not tidy numbers to
improve on; 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 and will matter when STL
and STEP export begins.
### Oracle parameter defaults
`strap-beam-3x.scad` lines 65–135. `facets = 48` and no case overrides it.
`fit_clearance_mm = 0.25`, all four wall thicknesses `1.20`,
`material_density_g_cm3 = 1.24`, `y_junction_round_mm = 1.50`,
`three_fin_junction_round_mm = 2.00`, `ring_corner_radius_mm = 2.00`.
### What is verified against the reference
Three-Fin reproduces **every recorded value exactly**, including
`SECTION_AREA_MM2 146.787`, `VOLUME_MM3 14678.7`, `MASS_G 18.2016`. That case
exercises butt joints, `fillet_junctions`, `fillet_pair`, `fillet_concave`,
`round_corners`, the derived bore and the fin solver.
Y reproduces every value except section area and its two derivatives — see
F-034.
Exact across both: `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.
**The numerical machinery is right.**
---
## 6. Probing the reference directly
The pinned toolchain image is in CT 100 and OpenSCAD will echo whatever you ask
it. This turned F-034 from a guess into a measurement in four exchanges, and it
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 harvester exports to `/dev/null` for the same reason: the export only forces
evaluation.
**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` exits 1 on any diff by design and the `frozen` date guarantees
one — that is a pass, not a fault. Its restore path was broken, is now fixed, and
is **still unexercised** (F-033). The next `--full` run proves it.
---
## 7. F-034 — open, and it will force a decision
Read the entry in full. Short version: arc segment counts are
`ceil((90 - half_angle)/180 * $fn)`, and that is frequently an exact integer — a
60° half-angle at `$fn = 48` gives exactly 8. Floating point delivers it as
`8.000000000000004` or `7.999999999999998` depending on how the corner was
reached. The half-angles come from the merged polygon, whose vertices come from
the boolean kernel, and BOSL2's clipper and GEOS disagree in the last bit.
**Do not try to fix this.** It was tried and reverted. Rounding the count before
the ceiling fixes Y exactly and breaks Three-Fin, which had been matching to the
digit, because Three-Fin has the same asymmetry and **the oracle records it**.
Consequences: 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 any affected case fails on `VOLUME_MM3` and `MASS_G` long before it fails on
`SECTION_AREA_MM2`.
**Do not raise the tolerance question until `build()` exists and all 123 cases
have run.** The number of affected cases is unknown and is the only thing that
should drive the decision. It may be one case. Bring the count and a
recommendation, then stop.
---
## 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.
**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: the
three-point floor on blunt corners, on-boundary versus outside in the nesting
probe, a bore that has turned inside out carrying real area, the ring fit's
spurious lower branch below scale 1, and nothing asserting that `clean_region`
removed anything.
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.**
Check which before writing a test.
**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).
The next gap in priority is dihedral parameterisation: if two panels meet at
137°, none of the eleven profiles gives you a member for it.
**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.
---
## 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.
---
## 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 `VOLUME_MM3` — see F-034 | CIVICVS, **not yet** |
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
```
009fcce docs: handoff for the 19 AUG session
86a47c3 rounding: record F-034, arc segment counts tip on the last bit
8d79431 geom: port sb-core and sb-profiles, the N-generic arrangements
b101fe6 geom: port sb-report, validation and report formatting
ebf02d6 geom: port sb-join, the junction and envelope strategies
38ea024 geom: Shapely-backed region layer
545eee7 geom: port BOSL2 round_corners and path cleanup
dfd02a4 geom: port the pure-geometry half of sb-geom
b67cc12 verify.sh: reach the restore on the diff branch
1d3eed0 Handover push
6967eef Conformance updates.
c7e32d8 Seed repository: rev-8.0.0 reference, frozen oracle, toolchain, test harness
```