This specifies a **staging environment** on `srv-b`, plus the promotion path to a production host that does not yet exist.
1064 lines
41 KiB
Markdown
1064 lines
41 KiB
Markdown
# ENVIRONMENT.md
|
||
|
||
Provisioning specification for the **Mechanical Compiler** development and
|
||
staging environment.
|
||
|
||
| | |
|
||
|---|---|
|
||
| Revision | 4 (2026-08-15) |
|
||
| Supersedes | Revisions 1, 2 and 3 |
|
||
| Basis | `ENVIRONMENT_INVESTIGATION_REPORT_2026-08-15.md` (host `srv-b`) |
|
||
| Audience | The assistant or operator provisioning the containers |
|
||
| Author role | Written by the developer who will work inside this environment |
|
||
| Status | All decisions taken. Ready to execute. |
|
||
| Repository | `https://gitea.barternetwork.us/TheRON/mechanical-compiler` |
|
||
| Licence | AGPL-3.0-or-later (committed at `e85c4f4e`) |
|
||
|
||
---
|
||
|
||
## 0. How to use this document
|
||
|
||
This specifies a **staging environment** on `srv-b`, plus the promotion path to
|
||
a production host that does not yet exist.
|
||
|
||
Everything is scripted. Nothing is provisioned by hand.
|
||
|
||
### Markers
|
||
|
||
| Marker | Meaning |
|
||
|---|---|
|
||
| **REQ** | Required. The work is blocked or wrong without it. |
|
||
| **PREF** | Preferred. Substitute freely, but report what you substituted. |
|
||
| **ASSUMED** | Decided by the author without confirmation. Defensible, but a guess. All are listed in §16. |
|
||
| **FILL** | A site value that must be supplied. Goes in `deploy/site.env` and nowhere else. |
|
||
| **VERIFY** | Must be checked at run time rather than trusted from this document. |
|
||
| **DISCOVER** | Genuinely unknown. Answer by doing, then report. |
|
||
|
||
### If you are the provisioning assistant
|
||
|
||
Four requests. **FILL** values live in one file and are referenced from there —
|
||
no literals scattered through scripts. **VERIFY** steps run inside the scripts,
|
||
not as a pre-flight a human might skip. Report **DISCOVER** outcomes verbatim,
|
||
including failures. And if a **REQ** item cannot be satisfied, say so rather
|
||
than working around it: the stated reason usually matters more than the
|
||
mechanism.
|
||
|
||
### What changed in revision 4
|
||
|
||
| # | Change | Source |
|
||
|---|---|---|
|
||
| 1 | Host is `srv-b`, not `annales` | CHG-001 |
|
||
| 2 | Staging and production are separate roles, on separate hosts | CHG-002 |
|
||
| 3 | `srv-b` is a standalone node; promotion is export/restore, not cluster migration | CHG-003 |
|
||
| 4 | Template `12.12-1`, not `12.7-1` | CHG-004 |
|
||
| 5 | Three-tier backup architecture; 32 GB removable gold media | CHG-005 |
|
||
| 6 | Backup schedule defined explicitly; none exists to inherit | CHG-006 |
|
||
| 7 | `CT_WG_ALIAS` removed | CHG-007 |
|
||
| 8 | `PROXY_HOST` not inherited; staging gets its own proxy container | CHG-008 |
|
||
| 9 | **Two containers**, not one: application and reverse proxy | this revision |
|
||
| 10 | **Second bridge** `vmbr1` isolates service traffic from management traffic | this revision |
|
||
| 11 | Container sizing raised substantially to use the host | this revision |
|
||
| 12 | Staging is internal-only; the public FQDN is reserved for production | this revision |
|
||
|
||
---
|
||
|
||
## 1. Five decisions that shape everything below
|
||
|
||
### 1.1 The 2D path and the 3D path are separable
|
||
|
||
The catalogue's product is a 2D cross-section rendered to SVG. Shapely plus our
|
||
own code covers that completely: region algebra, offsets, distance queries,
|
||
output. CadQuery/OCCT is needed only for STEP and mesh export.
|
||
|
||
These stay separate — separate requirements files, separate code paths, and a
|
||
CI job that runs the whole suite with the CAD dependencies absent. Three
|
||
reasons, none of which is packaging politics:
|
||
|
||
1. Part II §16 requires the control-plane / execution-plane separation
|
||
regardless of how the app is distributed.
|
||
2. A slow OCCT import must never land in the request path for a page that only
|
||
draws a cross-section.
|
||
3. Test-suite speed. Shapely-only tests run in seconds, which is the difference
|
||
between running the 123 frozen fixtures on every save and only before commit.
|
||
|
||
**Retained correction.** Revision 1 also justified this as a defence against
|
||
YunoHost's "resource-hungry" criterion. That was overcalibrated —
|
||
`fab-manager_ynh` is in the catalog, `paperless-ngx_ynh` declares nine apt
|
||
dependencies including postgresql and redis, and the criterion reads
|
||
"resource-hungry **compared to their features**." **CadQuery is a
|
||
default-installed dependency.** The boundary stays; the apology goes.
|
||
|
||
### 1.2 YunoHost and Docker are parallel targets, not sequential ones
|
||
|
||
YunoHost apps install natively — apt, a venv, systemd, nginx — and the project
|
||
does not want Docker inside YunoHost. Docker is a separate distribution
|
||
channel, not a stepping stone to the catalog. Neither blocks the other.
|
||
|
||
### 1.3 Staging mirrors production topology, not production exposure
|
||
|
||
`srv-b` runs two containers because production will: an application container
|
||
that never terminates TLS, and a reverse proxy that does. A single container
|
||
serving directly would never exercise the `X-Forwarded-Proto` path — precisely
|
||
the class of bug that cost us time on Hubzilla's session logout.
|
||
|
||
Staging is **not** publicly reachable and does **not** use the public FQDN. It
|
||
uses the Kane County Civic Infrastructure CA on an internal name. The public
|
||
name and Let's Encrypt belong to production, which forces the promotion path to
|
||
be exercised rather than assumed.
|
||
|
||
### 1.4 Directory layout mirrors YunoHost conventions from the start
|
||
|
||
The paths in §7 are what a YunoHost package would provision anyway
|
||
(`install_dir`, `data_dir`, a dedicated system user, a dynamically assigned
|
||
port). The eventual `manifest.toml` resource block then describes what already
|
||
exists.
|
||
|
||
### 1.5 All configuration comes from the environment, never from the filesystem
|
||
|
||
No path, port, URL, or secret may be hardcoded or discovered by convention. One
|
||
env file per container; the same variables become `ENV` in a Dockerfile and
|
||
`ynh_add_config` substitutions in a package.
|
||
|
||
The check that this holds: moving the service to a different hostname is a
|
||
one-variable change. It currently is — which is why the staging/production
|
||
split below costs almost nothing.
|
||
|
||
---
|
||
|
||
## 2. Host baseline (confirmed, not to be re-verified)
|
||
|
||
| Item | Value |
|
||
|---|---|
|
||
| Hostname / FQDN | `srv-b` / `srv-b.dev.infra` |
|
||
| Role | Development and staging. Standalone node, never clustered. |
|
||
| Platform | Proxmox VE 8.4.0 on Debian 12, kernel `6.8.12-9-pve` |
|
||
| Hardware | HP ProLiant DL360 G7, firmware P68 |
|
||
| CPU | 2 × Xeon X5650, 24 logical CPUs |
|
||
| RAM / swap | 31 GiB / 8 GiB |
|
||
| Storage | HP Smart Array P410i, 4 × EG0146FAWHU, RAID 1+0, all SMART OK |
|
||
| `local` | directory, `/var/lib/vz`, ~70 GiB free — iso, vztmpl, **backup** |
|
||
| `local-lvm` | LVM-thin `pve/data`, 166.9 GiB, 0% used — rootdir, images |
|
||
| LAN | `vmbr0` on `enp3s0f0`, `10.0.0.12/24`, gateway `10.0.0.1` |
|
||
| WireGuard | `wg0` `10.110.0.12/32`, peer `wg-pk.civicus.us:51820`, allowed `10.110.0.0/22` |
|
||
| Routing | forwarding on, `MASQUERADE 10.0.0.0/24 -o wg0`, both persistent |
|
||
| Resolver | `75.75.75.75`, search `dev.infra` |
|
||
| Egress | Direct. No proxy, no allowlist. All required hosts reachable. |
|
||
| PVE firewall | `disabled/running` |
|
||
| Guests | None. Next ID `100`. |
|
||
| Template | `local:vztmpl/debian-12-standard_12.12-1_amd64.tar.zst` |
|
||
|
||
### 2.1 Two hardware facts worth knowing before anyone is surprised
|
||
|
||
The X5650 is Westmere: **no AVX**, and single-thread performance is modest.
|
||
Geometry solvers are single-threaded, so regenerating the fixture oracle will
|
||
take noticeably longer here than on a modern laptop. That is expected, not a
|
||
fault. All compiled wheels target an SSE2 baseline, so nothing breaks.
|
||
|
||
Against that, **24 threads is a great deal of parallel test capacity**. The
|
||
test suite runs under `pytest-xdist` with `-n auto`, which is where the box
|
||
earns its keep.
|
||
|
||
### 2.2 The WireGuard path is outbound only
|
||
|
||
`MASQUERADE` means containers reach `10.110.0.0/22`, but nothing on the
|
||
WireGuard network can initiate a connection back. Irrelevant for staging.
|
||
**Relevant at promotion:** if the production reverse proxy lives on the
|
||
WireGuard side, this topology will not serve it unchanged. Recorded now so it
|
||
is not rediscovered later.
|
||
|
||
---
|
||
|
||
## 3. Network
|
||
|
||
### 3.1 Two bridges
|
||
|
||
**REQ** — `vmbr0`, existing, unchanged. `enp3s0f0`, `10.0.0.12/24`. This is the
|
||
**management and egress** network: SSH, apt, PyPI, Gitea, Docker Hub.
|
||
|
||
**REQ** — `vmbr1`, new, **no physical port**. Host address `10.20.0.1/24`.
|
||
This is the **service** network: proxy-to-application traffic and nothing else.
|
||
|
||
A portless bridge needs no cable, no switch configuration, and no spare NIC. It
|
||
exists so the application container has no listener on the LAN at all: the app
|
||
binds `127.0.0.1` and its `vmbr1` address only, and the only route to it is
|
||
through the proxy.
|
||
|
||
```
|
||
# /etc/network/interfaces — append
|
||
auto vmbr1
|
||
iface vmbr1 inet static
|
||
address 10.20.0.1/24
|
||
bridge-ports none
|
||
bridge-stp off
|
||
bridge-fd 0
|
||
```
|
||
|
||
### 3.2 The other three NICs stay unconfigured
|
||
|
||
**REQ** — Do not bond, trunk, or bridge `enp3s0f1` or the second dual-port
|
||
adapter. Leave them down and cabled to nothing.
|
||
|
||
This is deliberate. An active-backup bond on `vmbr0` would be defensible, but
|
||
it buys link redundancy on a staging host that is allowed to be down, at the
|
||
cost of a configuration that has to be understood by everyone who touches the
|
||
box afterwards. Not worth it here.
|
||
|
||
What the spare ports are *for*, when a reason appears: a point-to-point link to
|
||
the production host for promotion transfers, and a dedicated management
|
||
interface if the LAN ever becomes untrusted. Neither is now.
|
||
|
||
### 3.3 Addressing
|
||
|
||
**ASSUMED** — Static, outside any DHCP pool.
|
||
|
||
| Guest | `vmbr0` (management) | `vmbr1` (service) |
|
||
|---|---|---|
|
||
| `srv-b` host | `10.0.0.12/24` | `10.20.0.1/24` |
|
||
| CT 100 `mechcomp` | `10.0.0.20/24` | `10.20.0.10/24` |
|
||
| CT 101 `mcproxy` | `10.0.0.21/24` | `10.20.0.11/24` |
|
||
|
||
**VERIFY** — Stage 2 must confirm `10.0.0.20` and `10.0.0.21` are unclaimed
|
||
immediately before creating each container, and abort if not:
|
||
|
||
```bash
|
||
for ip in 10.0.0.20 10.0.0.21; do
|
||
if arping -D -I vmbr0 -c 3 -w 3 "$ip" >/dev/null 2>&1; then
|
||
echo " $ip free"
|
||
else
|
||
echo "ABORT: $ip answered ARP — address is in use" >&2; exit 1
|
||
fi
|
||
done
|
||
```
|
||
|
||
The investigation deliberately did not scan the LAN, and this document does not
|
||
either. An ARP probe for two specific addresses at creation time is the correct
|
||
scope. `10.0.0.160` was seen as a neighbour and is avoided.
|
||
|
||
**DISCOVER** — If the LAN has a DHCP server, report its pool range so these two
|
||
addresses can be confirmed outside it. If `arping` reports a conflict, stop and
|
||
ask rather than picking the next number.
|
||
|
||
### 3.4 Names
|
||
|
||
**REQ** — `dev.infra` has no resolver: the host points at `75.75.75.75`, which
|
||
cannot answer for it. Names resolve through `/etc/hosts` only.
|
||
|
||
Stage 1 writes to `/etc/hosts` on the host, and stages 3 and 4 write the same
|
||
block into both containers:
|
||
|
||
```
|
||
10.20.0.10 mechanical-compiler.dev.infra mechcomp
|
||
10.20.0.11 mcproxy.dev.infra mcproxy
|
||
```
|
||
|
||
**REQ — do not stand up an internal DNS server.** Two containers and one host
|
||
do not justify one, and adding a resolver here would create a second source of
|
||
truth for names that production will not share.
|
||
|
||
### 3.5 Firewall
|
||
|
||
**ASSUMED** — The Proxmox firewall stays disabled, matching the observed
|
||
staging baseline. The service network is portless and the application has no
|
||
LAN listener, which is the containment that matters here.
|
||
|
||
**REQ** — Production must revisit this. Recorded as a promotion checklist item
|
||
in §14, not as a permanent decision.
|
||
|
||
---
|
||
|
||
## 4. Containers
|
||
|
||
### 4.1 Roles
|
||
|
||
| | CT 100 | CT 101 |
|
||
|---|---|---|
|
||
| Hostname | `mechcomp` | `mcproxy` |
|
||
| Role | Application and worker | Reverse proxy, TLS termination |
|
||
| Unprivileged | yes | yes |
|
||
| Features | `nesting=1,keyctl=1` | none |
|
||
|
||
**VERIFY** — Confirm both IDs with `pvesh get /cluster/nextid` and `pct list`
|
||
immediately before creation. `100` and `101` are candidates from a host that
|
||
had no guests, not reservations.
|
||
|
||
### 4.2 Sizing
|
||
|
||
The host has 24 threads and 31 GiB with nothing else on it. Sizing reflects
|
||
that rather than a cautious default.
|
||
|
||
| | CT 100 | CT 101 | Host remaining |
|
||
|---|---|---|---|
|
||
| vCPU | 16 | 2 | 24 total, soft limits |
|
||
| RAM | 16 GiB | 1 GiB | ~14 GiB |
|
||
| Swap | 4 GiB | 512 MiB | — |
|
||
| rootfs | 40 GiB | 8 GiB | on `local-lvm` |
|
||
| `mp0` | 60 GiB at `/var/lib/mechcomp` | — | — |
|
||
|
||
Thin provisioning means `mp0` consumes only what is written. Committed total is
|
||
108 GiB of 166.9 GiB — generous but not over-committed, which matters because
|
||
thin-pool exhaustion is an ugly failure mode.
|
||
|
||
**REQ** — `mp0` is a real Proxmox mount point with `backup=0`. See §9.2 for why
|
||
the backup flag is off, and §12 for the check that proves the mount is real.
|
||
|
||
### 4.3 Base OS
|
||
|
||
**REQ** — Debian 12 bookworm from
|
||
`local:vztmpl/debian-12-standard_12.12-1_amd64.tar.zst`, both containers.
|
||
|
||
Chosen over Debian 13 and Ubuntu because it matches YunoHost 12 stable, ships
|
||
Python 3.11 (the best OCP/CadQuery wheel coverage), and is already present on
|
||
the host.
|
||
|
||
**VERIFY** — Stage 2 confirms the template is present, and downloads it only if
|
||
absent:
|
||
|
||
```bash
|
||
pveam list local | grep -q "${PVE_TEMPLATE##*/}" || pveam download local "${PVE_TEMPLATE##*/}"
|
||
```
|
||
|
||
**REQ** — `America/Chicago`, `en_US.UTF-8` in both containers, matching the host.
|
||
|
||
---
|
||
|
||
## 5. TLS and service names
|
||
|
||
### 5.1 Staging
|
||
|
||
**REQ** — Internal name `mechanical-compiler.dev.infra`, certificate issued by
|
||
the **Kane County Civic Infrastructure CA**.
|
||
|
||
**REQ** — The CA root is installed into both containers' trust stores and must
|
||
also be trusted on the workstation used to browse staging.
|
||
|
||
**REQ — the public FQDN is not used on staging.** No DNS record for
|
||
`mechanical-compiler.manufacturing.kane-il.us` may point at `srv-b`. No Let's
|
||
Encrypt certificate is issued for staging: it consumes rate limit against a
|
||
name that must be clean when production needs it, and it would put a
|
||
development box on the public internet.
|
||
|
||
### 5.2 Production, reserved
|
||
|
||
| | Value |
|
||
|---|---|
|
||
| FQDN | `mechanical-compiler.manufacturing.kane-il.us` |
|
||
| Zone | Labels within `kane-il.us`. **Not delegated.** No new DS record. |
|
||
| Certificate | Let's Encrypt, **per-name, no wildcard** |
|
||
| Challenge | **ASSUMED** HTTP-01 on the production proxy. Match existing practice if it is DNS-01; do not run both. |
|
||
|
||
`manufacturing.kane-il.us` is a namespace, not a service. Nothing is served
|
||
from it. `kane-il.us` itself stays reserved for cross-cutting infrastructure —
|
||
proxy, mail, directory, certificates — and must not acquire project records.
|
||
|
||
The leaf names the service, not the discipline, because the parent already
|
||
carries the discipline and the namespace must hold siblings:
|
||
|
||
```
|
||
mechanical-compiler.manufacturing.kane-il.us this application
|
||
field-metrology.manufacturing.kane-il.us Utility One, later
|
||
stock-processing.manufacturing.kane-il.us Utility Two, later
|
||
```
|
||
|
||
### 5.3 Three names, never conflated
|
||
|
||
| Name | Scope | Value |
|
||
|---|---|---|
|
||
| Service FQDN | What users type | staging and production differ; see above |
|
||
| Infrastructure token | System user, paths, units, YunoHost app id | `mechcomp` |
|
||
| Project name | Documents, README, catalog entry | Mechanical Compiler |
|
||
|
||
The token stays `mechcomp` in both environments. YunoHost app ids accept
|
||
lowercase alphanumerics and underscores only, so a hyphenated FQDN could not
|
||
serve as one anyway.
|
||
|
||
---
|
||
|
||
## 6. Reverse proxy (CT 101)
|
||
|
||
**REQ** — NGINX. Terminates TLS on `vmbr0`, proxies to CT 100 over `vmbr1`.
|
||
|
||
**REQ** — The `X-Forwarded-Proto` header is mandatory, not optional. Getting
|
||
this wrong is what broke Hubzilla sessions.
|
||
|
||
```nginx
|
||
server {
|
||
listen 443 ssl;
|
||
listen [::]:443 ssl;
|
||
server_name mechanical-compiler.dev.infra;
|
||
|
||
ssl_certificate /etc/ssl/mechcomp/server.crt; # Kane County CA
|
||
ssl_certificate_key /etc/ssl/mechcomp/server.key;
|
||
|
||
location / {
|
||
proxy_pass http://10.20.0.10:8770;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
proxy_read_timeout 300s;
|
||
client_max_body_size 8m;
|
||
}
|
||
}
|
||
|
||
server {
|
||
listen 80;
|
||
listen [::]:80;
|
||
server_name mechanical-compiler.dev.infra;
|
||
return 301 https://$host$request_uri;
|
||
}
|
||
```
|
||
|
||
**PREF** — `proxy_read_timeout 300s`. Compile jobs are asynchronous by design,
|
||
but the initial synchronous render can take tens of seconds on a cold cache on
|
||
this CPU, and the 60s default leaves no headroom.
|
||
|
||
**REQ** — This vhost is the production vhost with two values changed:
|
||
`server_name` and the certificate paths. Keep it that way. Any divergence
|
||
beyond those two lines means staging has stopped testing production.
|
||
|
||
---
|
||
|
||
## 7. Users, directories, permissions (CT 100)
|
||
|
||
**REQ** — Dedicated system user, no login shell, no home of its own:
|
||
|
||
```
|
||
user:group mechcomp:mechcomp
|
||
shell /usr/sbin/nologin
|
||
```
|
||
|
||
**REQ** — Layout, matching what a YunoHost package would provision:
|
||
|
||
| Path | Owner | Mode | Purpose | YunoHost equivalent |
|
||
|---|---|---|---|---|
|
||
| `/var/www/mechcomp` | `mechcomp:mechcomp` | `0750` | source and venv | `install_dir` |
|
||
| `/var/lib/mechcomp` | `mechcomp:mechcomp` | `0750` | data, on `mp0` | `data_dir` |
|
||
| `/var/log/mechcomp` | `mechcomp:mechcomp` | `0750` | logs | — |
|
||
| `/etc/mechcomp/mechcomp.env` | `root:mechcomp` | `0640` | configuration | `ynh_add_config` target |
|
||
|
||
**REQ** — `/var/lib/mechcomp` subdivides as:
|
||
|
||
```
|
||
/var/lib/mechcomp/
|
||
├── db/ SQLite database
|
||
├── artifacts/ generated SVG, STL, STEP, manifests
|
||
├── fixtures/ frozen acceptance oracles, read-mostly
|
||
└── cache/ safe to delete at any time
|
||
```
|
||
|
||
The `cache/` guarantee matters: anything not reproducible from `db/` plus
|
||
`artifacts/` must not live there. That is what keeps the backup story simple.
|
||
|
||
**REQ** — `${ADMIN_USER}` gets an ordinary shell account in the `mechcomp`
|
||
group with `${ADMIN_SSH_KEY}` installed, in **both** containers. The service
|
||
user is never used interactively.
|
||
|
||
---
|
||
|
||
## 8. Software (CT 100)
|
||
|
||
### 8.1 System packages
|
||
|
||
**REQ**
|
||
|
||
```
|
||
python3 python3-venv python3-dev
|
||
build-essential pkg-config
|
||
git curl ca-certificates
|
||
libgeos-dev # Shapely bundles GEOS in its wheel; headers make a
|
||
# source build possible if a wheel is unavailable
|
||
sqlite3
|
||
```
|
||
|
||
**PREF** — `jq ripgrep tmux htop`
|
||
|
||
**REQ — do not install** `openscad`, `openscad-nightly`, or any Qt or X11
|
||
package. This is not a weight argument: the running application never invokes
|
||
OpenSCAD, and §8.2 confines it where it belongs. If OpenSCAD appears in the
|
||
container's package list, something has gone wrong architecturally.
|
||
|
||
### 8.2 The pinned reference toolchain
|
||
|
||
**REQ** — One Docker image with the exact toolchain the fixture oracle was
|
||
frozen against, and nothing else:
|
||
|
||
```dockerfile
|
||
# tools/reference-toolchain/Dockerfile
|
||
FROM debian:12-slim
|
||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||
openscad git ca-certificates \
|
||
&& rm -rf /var/lib/apt/lists/*
|
||
RUN git clone https://github.com/BelfrySCAD/BOSL2.git /BOSL2 \
|
||
&& git -C /BOSL2 checkout 92d697c2856de2fed93a33e858068589cefc2898
|
||
```
|
||
|
||
Tag `mechcomp/reference-toolchain:8.0.0`. Its only job is to run
|
||
`make_fixtures.py` and prove the oracle still reproduces. This is why the
|
||
Debian revision skew does not matter: the container has no OpenSCAD, so its
|
||
version cannot drift.
|
||
|
||
**Acceptance test** — must reproduce exactly:
|
||
|
||
```
|
||
sha256 of strap-beam-fixtures-8.0.0.json
|
||
= ddd0f1548379205dd0c652ec07285b0dae331e52ff0a0437005dfc6cddcc2cb2
|
||
```
|
||
|
||
If it does not, stop and report. A drifting oracle is worse than no oracle.
|
||
|
||
**DISCOVER** — Docker in an unprivileged LXC is usually fine on this kernel
|
||
with `overlay2`, but it is not guaranteed. Report:
|
||
|
||
```bash
|
||
docker info --format '{{.Driver}} / {{.CgroupDriver}}'
|
||
docker run --rm debian:12-slim echo ok
|
||
```
|
||
|
||
`fuse-overlayfs` is the documented fallback. If both fail, say so — moving the
|
||
image build to a small VM on the same host is a minor change and preferable to
|
||
fighting the storage driver.
|
||
|
||
### 8.3 Python
|
||
|
||
**REQ** — Virtualenv at `/var/www/mechcomp/venv`. Debian 12 enforces PEP 668;
|
||
do not use `--break-system-packages` to get around it.
|
||
|
||
**REQ** — Pinned, hash-checked dependencies in two files, both installed:
|
||
|
||
| File | Contents |
|
||
|---|---|
|
||
| `requirements-base.txt` | shapely, fastapi, uvicorn, pydantic, sqlalchemy, jinja2, pytest, **pytest-xdist** |
|
||
| `requirements-cad.txt` | cadquery / build123d |
|
||
|
||
Both install here. The split is not about disk; it is the mechanism that keeps
|
||
§1.1 honest. CI runs the suite twice — with both files, then with
|
||
`requirements-base.txt` alone — and the second run must pass.
|
||
|
||
`pytest-xdist` is in the base set specifically so `-n auto` uses all 24 threads.
|
||
|
||
**PREF** — `uv` for resolution and locking; `pip-tools` is a fine substitute.
|
||
Either way the lock file is committed.
|
||
|
||
---
|
||
|
||
## 9. Services and backups
|
||
|
||
### 9.1 Systemd units (CT 100)
|
||
|
||
**REQ** — Two units, split along the control-plane / execution-plane boundary
|
||
Part II §16 requires:
|
||
|
||
| Unit | Role |
|
||
|---|---|
|
||
| `mechcomp.service` | FastAPI/uvicorn. Serves the catalogue, accepts jobs, returns cached artifacts. **Never runs geometry.** |
|
||
| `mechcomp-worker.service` | Consumes the queue, runs generators, writes artifacts. |
|
||
|
||
**REQ** — Both:
|
||
|
||
```ini
|
||
[Service]
|
||
User=mechcomp
|
||
Group=mechcomp
|
||
EnvironmentFile=/etc/mechcomp/mechcomp.env
|
||
WorkingDirectory=/var/www/mechcomp
|
||
Restart=on-failure
|
||
RestartSec=5
|
||
|
||
NoNewPrivileges=true
|
||
PrivateTmp=true
|
||
ProtectSystem=strict
|
||
ProtectHome=true
|
||
ReadWritePaths=/var/lib/mechcomp /var/log/mechcomp
|
||
ProtectKernelTunables=true
|
||
ProtectControlGroups=true
|
||
RestrictSUIDSGID=true
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
```
|
||
|
||
**REQ** — Worker only:
|
||
|
||
```ini
|
||
MemoryMax=8G
|
||
CPUQuota=1200%
|
||
TimeoutStopSec=30
|
||
```
|
||
|
||
A runaway geometry job must not take the container down. `MemoryMax` turning a
|
||
hang into a clean OOM-kill of one worker is the desired behaviour.
|
||
|
||
**REQ** — Logging to journald. No application-managed rotation.
|
||
|
||
**REQ** — Job queue is SQLite-backed and in-process. No Redis, no Celery, no
|
||
RabbitMQ. A table with a status column and a claim query is correct at this
|
||
scale and adds no services to either packaging target. It sits behind an
|
||
interface so it can be swapped if load ever justifies it. It will not.
|
||
|
||
### 9.2 Three backup tiers
|
||
|
||
| Tier | What | Where | Retention |
|
||
|---|---|---|---|
|
||
| Routine | `vzdump` of both container **rootfs** | `local` (`/var/lib/vz/dump`) | see below |
|
||
| Application | tar stream of the data set | `/var/lib/vz/mechcomp-app/` on `srv-b` | 14 daily |
|
||
| Gold | routine + application + manifest | 32 GB removable USB, LUKS | indefinite, offline |
|
||
|
||
**REQ — `mp0` is excluded from `vzdump` (`backup=0`).** `/var/lib/vz` sits on
|
||
`pve-root`, which is 78 GiB, and a full root filesystem is how Proxmox hosts
|
||
become interesting. Excluding the 60 GiB data volume keeps each routine archive
|
||
to the rootfs alone — a few GiB compressed — and application data flows through
|
||
the tier below, which is where it belongs anyway.
|
||
|
||
**REQ** — `vzdump` job: both containers, `snapshot` mode, `zstd`, daily at
|
||
02:30, storage `local`.
|
||
|
||
**ASSUMED** — Initial retention `keep-last=3`. Deliberately conservative: no
|
||
`vzdump` has been taken on this host, so actual size is unmeasured. **DISCOVER**
|
||
— after the first successful run, report the archive size and I will set final
|
||
retention. With 70 GiB free, three archives of an unknown size is the safe
|
||
starting point.
|
||
|
||
**REQ** — A `df` guard in the backup hook that refuses to run and alerts if
|
||
`/var/lib/vz` falls below 20 GiB free.
|
||
|
||
**REQ** — Application backup is a single script in CT 100 emitting a tar stream
|
||
on stdout, pulled host-side:
|
||
|
||
```bash
|
||
pct exec ${CT_ID_APP} -- /usr/local/bin/mechcomp-backup --stdout \
|
||
> /var/lib/vz/mechcomp-app/mechcomp-$(date +%F).tar
|
||
```
|
||
|
||
Covering exactly:
|
||
|
||
```
|
||
/etc/mechcomp/mechcomp.env
|
||
/var/lib/mechcomp/db/
|
||
/var/lib/mechcomp/artifacts/
|
||
/var/lib/mechcomp/fixtures/
|
||
```
|
||
|
||
`cache/` is excluded. `/var/www/mechcomp` is excluded because it is reproducible
|
||
from git plus the lock file. This script is, almost verbatim, the future
|
||
YunoHost `backup` script.
|
||
|
||
**REQ — pull, never push.** The container has no access to any backup
|
||
destination. The alternative is tempting and simpler, but it hands a
|
||
network-facing container write access to the last line of defence. It also
|
||
avoids idmap ownership problems entirely.
|
||
|
||
### 9.3 Gold archive
|
||
|
||
**REQ** — 32 GB USB stick, **LUKS2 full-device encryption**, ext4 inside,
|
||
filesystem label `MC-GOLD-01`, mounted at `/mnt/gold` with `noauto`.
|
||
|
||
Encryption is not optional: the archive contains `mechcomp.env`, which contains
|
||
`MECHCOMP_SECRET_KEY`, and a stick that travels between machines is exactly the
|
||
thing that gets lost. The passphrase lives in CIVICVS's password manager and
|
||
never in the repository. Encrypting the device rather than the files means the
|
||
later copy to the 3+ TB disk inherits the protection if the LUKS image is
|
||
copied as a block image.
|
||
|
||
ext4 rather than exFAT: no 4 GiB file cap, ownership preserved, journalled.
|
||
**DISCOVER** — if the stick must ever be read from Windows or macOS, say so and
|
||
this becomes exFAT with `age`-encrypted archives instead.
|
||
|
||
**REQ** — Archive layout, one directory per mint:
|
||
|
||
```
|
||
/mnt/gold/<UTC-timestamp>-<git-tag>/
|
||
├── vzdump-lxc-100-*.tar.zst
|
||
├── vzdump-lxc-101-*.tar.zst
|
||
├── mechcomp-app-*.tar
|
||
├── site.env (the FILL values, not secrets)
|
||
├── MANIFEST.txt git tag, revisions, host, PVE version
|
||
└── SHA256SUMS
|
||
```
|
||
|
||
**REQ** — `gold-archive.sh` verifies `SHA256SUMS` after writing and before
|
||
unmounting. An archive that has not been verified has not been made.
|
||
|
||
**ASSUMED** — A gold archive is minted on **every tagged release** and
|
||
**immediately before any promotion or host migration**. This ties the tier to
|
||
the existing tag discipline rather than to a calendar.
|
||
|
||
**DISCOVER** — Where the 3+ TB USB disk is attached now that `annales` is out
|
||
of scope. Until that is known, stick-to-disk copying is a documented manual
|
||
step, not a scripted one.
|
||
|
||
### 9.4 Disk health monitoring
|
||
|
||
**REQ** — Configure `smartd` for the four RAID members using the already
|
||
installed `smartmontools`. No third-party HPE repository is added, and `ssacli`
|
||
is not installed: the investigation showed the kernel and `smartctl -d cciss,N`
|
||
already provide what is needed.
|
||
|
||
```
|
||
# /etc/smartd.conf
|
||
/dev/sda -d cciss,0 -a -m root -M exec /usr/share/smartmontools/smartd-runner
|
||
/dev/sda -d cciss,1 -a -m root -M exec /usr/share/smartmontools/smartd-runner
|
||
/dev/sda -d cciss,2 -a -m root -M exec /usr/share/smartmontools/smartd-runner
|
||
/dev/sda -d cciss,3 -a -m root -M exec /usr/share/smartmontools/smartd-runner
|
||
```
|
||
|
||
These are 2010-vintage spinning disks in a RAID 1+0 holding both the staging
|
||
environment and its routine backups. Knowing about a failure before the second
|
||
one is worth the four lines.
|
||
|
||
**DISCOVER** — Whether Proxmox notifications can relay through the existing
|
||
mail infrastructure. If not, `smartd` and `vzdump` failures land in local root
|
||
mail only, which nobody reads — report this rather than leaving it silently
|
||
broken.
|
||
|
||
---
|
||
|
||
## 10. Configuration contract
|
||
|
||
**REQ** — `/etc/mechcomp/mechcomp.env` in CT 100, and nothing else, configures
|
||
the application:
|
||
|
||
```bash
|
||
MECHCOMP_ENV=staging # staging | production
|
||
MECHCOMP_BIND=10.20.0.10 # service network only; never 0.0.0.0
|
||
MECHCOMP_PORT=8770
|
||
MECHCOMP_BASE_URL=https://mechanical-compiler.dev.infra
|
||
MECHCOMP_DATA_DIR=/var/lib/mechcomp
|
||
MECHCOMP_LOG_LEVEL=info
|
||
MECHCOMP_DB_URL=sqlite:////var/lib/mechcomp/db/mechcomp.sqlite3
|
||
MECHCOMP_WORKER_CONCURRENCY=4
|
||
MECHCOMP_CAD_BACKEND=none # none | cadquery — 'none' until the 3D path exists
|
||
MECHCOMP_ARTIFACT_RETENTION_DAYS=30
|
||
MECHCOMP_SECRET_KEY= # generated by stage 3; see §11.4
|
||
```
|
||
|
||
**REQ** — The application fails loudly at startup if a required variable is
|
||
missing, and never falls back to a compiled-in default path. That single rule
|
||
is most of what makes both packaging targets work unchanged.
|
||
|
||
**REQ** — `MECHCOMP_BIND` is the service-network address, plus `127.0.0.1`.
|
||
Binding `0.0.0.0` would defeat the point of the second bridge.
|
||
|
||
Promotion changes `MECHCOMP_ENV`, `MECHCOMP_BIND` and `MECHCOMP_BASE_URL`.
|
||
Nothing else.
|
||
|
||
---
|
||
|
||
## 11. Provisioning scripts
|
||
|
||
Everything above is a specification for scripts, not a runbook for a human.
|
||
|
||
### 11.1 Repository
|
||
|
||
**ASSUMED** — A separate repository, `TheRON/mechanical-compiler-infra`.
|
||
|
||
Provisioning scripts do not belong in the application repository. A YunoHost
|
||
reviewer eventually reads that tree, and Proxmox tooling beside the app is
|
||
exactly the "non-standard directory architecture" the catalog flags. The two
|
||
also version independently, which they will.
|
||
|
||
If you prefer one repository, put the scripts in `deploy/` and add that path to
|
||
the packaging exclusion list from the first commit — not after they have history.
|
||
|
||
### 11.2 Four stages
|
||
|
||
| Script | Runs as | Where | Does |
|
||
|---|---|---|---|
|
||
| `stage1-host.sh` | root | `srv-b` | `vmbr1`, `/etc/hosts`, `smartd`, vzdump job, `/var/lib/vz/mechcomp-app` |
|
||
| `stage2-create-cts.sh` | root | `srv-b` | template check, ARP verify, `pct create` ×2, `mp0` with `backup=0`, first boot |
|
||
| `stage3-app.sh` | root | CT 100 | packages, user, layout, venv, Docker, units, config, secret |
|
||
| `stage4-proxy.sh` | root | CT 101 | nginx, CA cert install, vhost, `/etc/hosts` |
|
||
|
||
**REQ** — The only interface between stages is `deploy/site.env`. Stage 2
|
||
copies it into both containers; stages 3 and 4 source it. No other state crosses
|
||
a boundary.
|
||
|
||
**REQ** — Stage 4 installs its own vhost. Unlike revision 3, the proxy is ours,
|
||
on this host, and there is no separate operator to hand a config to.
|
||
|
||
### 11.3 Idempotency
|
||
|
||
**REQ** — All four stages are safe to re-run. Create-if-absent,
|
||
converge-if-present, never destroy. A re-run against a healthy host is a no-op
|
||
that exits zero.
|
||
|
||
This follows from "version, tag, release with discipline": a script you cannot
|
||
re-run is a script you cannot test, and a script you cannot test is ceremony,
|
||
not discipline.
|
||
|
||
**REQ** — Every stage starts with `set -euo pipefail` and validates that each
|
||
**FILL** value in `site.env` is non-empty before touching anything.
|
||
|
||
### 11.4 Secrets
|
||
|
||
**REQ** — `MECHCOMP_SECRET_KEY` is never committed and never appears in
|
||
`site.env`. Stage 3 generates it on first run and refuses to overwrite:
|
||
|
||
```bash
|
||
if ! grep -q '^MECHCOMP_SECRET_KEY=.\+' /etc/mechcomp/mechcomp.env 2>/dev/null; then
|
||
key=$(openssl rand -base64 32)
|
||
# write it; never log it
|
||
fi
|
||
```
|
||
|
||
**REQ** — `deploy/site.env` is in `.gitignore`. `deploy/site.env.example`, with
|
||
empty values, is committed. The LUKS passphrase is in neither.
|
||
|
||
### 11.5 Versioning
|
||
|
||
**ASSUMED** — The infrastructure repository carries its own semver, independent
|
||
of the application's `SB_REVISION`. They drift for unrelated reasons and tying
|
||
them would force meaningless bumps on both.
|
||
|
||
**REQ** — Tag a release before each provisioning run and record the tag in the
|
||
run log and in the gold `MANIFEST.txt`. "Which version of the script built this
|
||
container" must be answerable six months from now.
|
||
|
||
---
|
||
|
||
## 12. `deploy/site.env`
|
||
|
||
```bash
|
||
# ---- Proxmox host (confirmed 2026-08-15) --------------------------------
|
||
PVE_HOST=srv-b
|
||
PVE_STORAGE=local-lvm
|
||
PVE_DATA_STORAGE=local-lvm
|
||
PVE_BACKUP_STORAGE=local
|
||
PVE_BRIDGE_MGMT=vmbr0
|
||
PVE_BRIDGE_SVC=vmbr1
|
||
PVE_TEMPLATE=local:vztmpl/debian-12-standard_12.12-1_amd64.tar.zst
|
||
|
||
# ---- Application container ---------------------------------------------
|
||
CT_ID_APP=100 # VERIFY before create
|
||
CT_HOSTNAME_APP=mechcomp
|
||
CT_IP_APP_MGMT=10.0.0.20/24 # VERIFY free by ARP before create
|
||
CT_IP_APP_SVC=10.20.0.10/24
|
||
CT_CORES_APP=16
|
||
CT_RAM_APP=16384
|
||
CT_SWAP_APP=4096
|
||
CT_ROOTFS_APP=40
|
||
CT_MP0_APP=60
|
||
|
||
# ---- Proxy container ---------------------------------------------------
|
||
CT_ID_PROXY=101 # VERIFY before create
|
||
CT_HOSTNAME_PROXY=mcproxy
|
||
CT_IP_PROXY_MGMT=10.0.0.21/24 # VERIFY free by ARP before create
|
||
CT_IP_PROXY_SVC=10.20.0.11/24
|
||
CT_CORES_PROXY=2
|
||
CT_RAM_PROXY=1024
|
||
CT_SWAP_PROXY=512
|
||
CT_ROOTFS_PROXY=8
|
||
|
||
# ---- Network -----------------------------------------------------------
|
||
CT_GATEWAY=10.0.0.1
|
||
SVC_NET=10.20.0.0/24
|
||
SVC_HOST_IP=10.20.0.1
|
||
|
||
# ---- Service -----------------------------------------------------------
|
||
MECHCOMP_ENV=staging
|
||
SERVICE_FQDN=mechanical-compiler.dev.infra
|
||
APP_PORT=8770
|
||
TLS_CA=kane-county-civic-infrastructure
|
||
|
||
# ---- Identity ----------------------------------------------------------
|
||
ADMIN_USER= # FILL
|
||
ADMIN_SSH_KEY= # FILL
|
||
|
||
# ---- Backup ------------------------------------------------------------
|
||
VZDUMP_SCHEDULE="02:30"
|
||
VZDUMP_KEEP_LAST=3 # revisit after first size measurement
|
||
APP_BACKUP_DIR=/var/lib/vz/mechcomp-app
|
||
APP_BACKUP_KEEP=14
|
||
GOLD_LABEL=MC-GOLD-01
|
||
GOLD_MOUNT=/mnt/gold
|
||
|
||
# ---- Reserved for production, not used on srv-b ------------------------
|
||
# PRODUCTION_FQDN=mechanical-compiler.manufacturing.kane-il.us
|
||
```
|
||
|
||
`CT_WG_ALIAS` is gone. The host routes and MASQUERADEs `10.0.0.0/24` out `wg0`,
|
||
so containers reach the WireGuard network by their ordinary LAN address, and no
|
||
per-container alias mechanism exists to configure.
|
||
|
||
---
|
||
|
||
## 13. Constraints the application code will follow
|
||
|
||
Recorded so the environment is not built in a way that quietly permits their
|
||
violation.
|
||
|
||
1. No hardcoded absolute paths. Everything derives from `MECHCOMP_DATA_DIR`.
|
||
2. No writes outside `MECHCOMP_DATA_DIR`. Enforced by `ProtectSystem=strict`.
|
||
3. No dependency on systemd from application code. Signal handling only.
|
||
4. The app listens on a TCP port, on the service network and loopback only.
|
||
5. No OpenSCAD, Qt or X11 dependency in the running application.
|
||
6. The 3D backend is reached only through an interface in `src/mechcomp/cad/`.
|
||
No other module imports CadQuery or OCP directly.
|
||
7. Schema migrations are explicit and forward-only.
|
||
8. Artifacts are addressed by content hash of **inputs** — parameter set plus
|
||
generator revision — never by hash of output bytes. Mesh output is not
|
||
reproducible across toolchain versions; input hashing keeps a qualification
|
||
valid across an upgrade that did not change the geometry.
|
||
9. Every generated artifact carries the generator revision that produced it.
|
||
10. The test suite passes with `requirements-cad.txt` uninstalled.
|
||
11. AGPL-3.0 §13 requires network users be offered the source. The web tier
|
||
carries a visible source link to the Gitea repository. This is a licence
|
||
obligation, not a nicety.
|
||
|
||
---
|
||
|
||
## 14. Acceptance checklist
|
||
|
||
Run and report. This is also the first draft of the YunoHost `tests.toml`
|
||
criteria.
|
||
|
||
```bash
|
||
# ---- host ----
|
||
pveversion | head -1
|
||
ip -br address show vmbr1 # expect 10.20.0.1/24
|
||
pct list # expect 100 and 101, both running
|
||
grep -c mechanical-compiler.dev.infra /etc/hosts
|
||
systemctl is-active smartd
|
||
cat /etc/pve/jobs.cfg | grep -c vzdump # expect >= 1
|
||
pct config 100 | grep mp0 # must contain backup=0
|
||
|
||
# ---- CT 100 ----
|
||
pct exec 100 -- bash -c '
|
||
cat /etc/debian_version
|
||
python3 --version
|
||
timedatectl show -p Timezone --value
|
||
id mechcomp
|
||
findmnt /var/lib/mechcomp # must be a mount, not rootfs
|
||
stat -c "%U:%G %a" /etc/mechcomp/mechcomp.env
|
||
dpkg -l | grep -Ei "openscad|libqt|xserver" || echo "clean"
|
||
ip -br address | grep 10.20.0.10
|
||
ss -lntp | grep 8770 # must NOT bind 0.0.0.0
|
||
grep -c "^MECHCOMP_SECRET_KEY=.\+" /etc/mechcomp/mechcomp.env
|
||
/var/www/mechcomp/venv/bin/python -c "import shapely; print(shapely.__version__)"
|
||
/var/www/mechcomp/venv/bin/python -c "import cadquery; print(cadquery.__version__)"
|
||
docker info --format "{{.Driver}}"
|
||
docker run --rm mechcomp/reference-toolchain:8.0.0 openscad --version
|
||
'
|
||
|
||
# ---- egress from CT 100 ----
|
||
pct exec 100 -- bash -c '
|
||
for h in deb.debian.org pypi.org files.pythonhosted.org gitea.barternetwork.us; do
|
||
curl -sS -o /dev/null -w "$h %{http_code}\n" "https://$h"
|
||
done
|
||
git ls-remote https://gitea.barternetwork.us/TheRON/mechanical-compiler HEAD | head -1
|
||
'
|
||
|
||
# ---- CT 101 and end to end ----
|
||
pct exec 101 -- nginx -t
|
||
pct exec 101 -- curl -sS -o /dev/null -w '%{http_code} %{ssl_verify_result}\n' \
|
||
https://mechanical-compiler.dev.infra/
|
||
|
||
# ---- forwarded-proto reaches the app ----
|
||
pct exec 101 -- curl -sS https://mechanical-compiler.dev.infra/_debug/headers \
|
||
| grep -i x-forwarded-proto # expect https
|
||
|
||
# ---- idempotency: second run is a clean no-op ----
|
||
./stage1-host.sh && ./stage2-create-cts.sh && echo "re-run clean"
|
||
|
||
# ---- backup round trip ----
|
||
vzdump 100 --storage local --mode snapshot --compress zstd
|
||
ls -lh /var/lib/vz/dump/ # REPORT the size
|
||
pct exec 100 -- /usr/local/bin/mechcomp-backup --stdout | wc -c
|
||
```
|
||
|
||
### Promotion checklist, for later
|
||
|
||
Not now, but recorded so it is not invented under pressure: production must
|
||
revisit the Proxmox firewall (§3.5), issue a Let's Encrypt certificate for the
|
||
public FQDN (§5.2), confirm the reverse proxy is not on the WireGuard side
|
||
(§2.2), and change exactly three variables in `mechcomp.env` (§10).
|
||
|
||
---
|
||
|
||
## 15. Deliberately out of scope
|
||
|
||
- Hubzilla addon development, or any change to existing Hubzilla containers
|
||
- Federation, identity, or qualification storage
|
||
- PostgreSQL — SQLite is sufficient and will remain so for a long time
|
||
- Redis, Celery, or any external queue
|
||
- CI runners — the suite runs locally until there is a reason otherwise
|
||
- Monitoring beyond journald, smartd, and Proxmox's own metrics
|
||
- NIC bonding, VLANs, or any further network configuration (§3.2)
|
||
- An internal DNS server (§3.4)
|
||
- The YunoHost package and the Docker image themselves
|
||
|
||
Each has a natural moment. None of them is now.
|
||
|
||
---
|
||
|
||
## 16. Every assumption in one place
|
||
|
||
| § | Assumption | If wrong |
|
||
|---|---|---|
|
||
| 3.3 | `10.0.0.20` and `.21` are free and outside any DHCP pool | ARP verify aborts stage 2; supply replacements |
|
||
| 3.3 | Static addressing, no DHCP reservation needed | Reserve in DHCP instead; addresses unchanged |
|
||
| 3.5 | Proxmox firewall stays disabled in staging | Enable per-container; production revisits regardless |
|
||
| 4.1 | CT IDs 100 and 101 | Verified at create time; any free pair works |
|
||
| 4.2 | 16 vCPU / 16 GiB for the app container | Lower freely; nothing depends on it |
|
||
| 5.1 | Kane County CA issues the staging certificate | A self-signed cert works; trust store step changes |
|
||
| 5.2 | Production uses HTTP-01 | Switch to DNS-01 if that is existing practice; never both |
|
||
| 9.2 | `keep-last=3` initially | Raise once the first archive size is measured |
|
||
| 9.3 | LUKS2 on the gold stick, ext4 inside | exFAT plus `age`-encrypted files if cross-OS reading is needed |
|
||
| 9.3 | Gold minted per tagged release and before promotion | Any trigger works; this one ties to existing discipline |
|
||
| 11.1 | Separate `mechanical-compiler-infra` repository | Use `deploy/` in the app repo, excluded from packaging |
|
||
| 11.3 | All stages idempotent | Only matters if you want create-once semantics; I would argue against |
|
||
| 11.5 | Infra versions independently of `SB_REVISION` | Tie them together if you prefer one release train |
|
||
|
||
---
|
||
|
||
## 17. Provenance disclosure
|
||
|
||
This project's documents, code and roadmap are LLM-generated under human
|
||
direction. That is stated plainly here, in the README, and in any eventual
|
||
catalog submission. We do not obscure it.
|
||
|
||
**The YunoHost policy is a quality bar with a disclosure requirement, not a
|
||
ban.** Revision 1 overstated it. The catalog repository rejects generated
|
||
packages *that do not follow the `example_ynh` template*, citing verbose code,
|
||
hallucinated helpers and non-standard directory architectures — and then
|
||
explicitly permits AI use provided the maintainer is transparent and can
|
||
explain every line. The failure mode guarded against is sprawl, not provenance.
|
||
The defence is discipline: minimal divergence from the template, no invented
|
||
helpers, no extra files.
|
||
|
||
**Package provenance is not upstream provenance.** The policy text concerns the
|
||
`_ynh` repository — a few hundred lines of shell and one TOML file. Nobody
|
||
audits whether an upstream Rails application was written by a human. The
|
||
reviewable surface is small.
|
||
|
||
**Disclosure is right; headlining it is a tactical mistake.** Part II §29 holds
|
||
that early use cases should demonstrate the system's distinct value. If
|
||
provenance becomes the pitch, the project is judged on that axis rather than on
|
||
whether it makes distributed manufacturing capacity legible. State it clearly
|
||
in the README; keep the pitch about manufacturing. The argument is won by an
|
||
artifact a reviewer finds shorter and more conventional than average, not by
|
||
the claim attached to it.
|
||
|
||
---
|
||
|
||
## 18. The packaging path, when it comes
|
||
|
||
**DISCOVER** — Unknown territory, to be documented as it is walked rather than
|
||
researched ahead: `manifest.toml` v2 with helpers 2.1, the `scripts/` set
|
||
(install, remove, upgrade, backup, restore, change_url), `conf/` templates,
|
||
`tests.toml`, and `package_check` levels.
|
||
|
||
Two orienting notes. The useful comparison for acceptable weight is
|
||
`paperless-ngx_ynh` or `fab-manager_ynh`, not a hello-world package. And per
|
||
§17, the package is drafted against `example_ynh` and reviewed by CIVICVS as
|
||
his own work — anything in it he would not defend in a review thread does not
|
||
ship.
|
||
|
||
---
|
||
|
||
## 19. First work item after acceptance
|
||
|
||
Port `sb-geom` to Shapely and get `pytest -n auto` green against the 123 frozen
|
||
cases in `strap-beam-fixtures-8.0.0.json`. The ten rejected cases are part of
|
||
the contract: a port that accepts them is wrong.
|