diff --git a/docs/FAILURES.md b/docs/FAILURES.md index 7bf9907..99c8537 100644 --- a/docs/FAILURES.md +++ b/docs/FAILURES.md @@ -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 -- `. 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 | # | Status | @@ -857,5 +904,6 @@ decision. | 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-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. diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md new file mode 100644 index 0000000..ce26090 --- /dev/null +++ b/docs/HANDOFF.md @@ -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 -- `, +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 ` 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= --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 +$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 +``` diff --git a/docs/HANDOFF-2026-08-18.md b/docs/archive/HANDOFF-2026-08-18.md similarity index 100% rename from docs/HANDOFF-2026-08-18.md rename to docs/archive/HANDOFF-2026-08-18.md diff --git a/docs/HANDOFF-2026-08-19.md b/docs/archive/HANDOFF-2026-08-19.md similarity index 100% rename from docs/HANDOFF-2026-08-19.md rename to docs/archive/HANDOFF-2026-08-19.md