This repository is the source for the TBS-001 engineering documentation: a giant pinhole camera built inside a 20 ft shipping container. Everything published is generated from code and text in this repo — the 2D engineering diagrams (matplotlib), the 3D SketchUp models, the cost / shopping / weight figures, and the MkDocs website.
Looking for the camera itself? This README is about running the repo (regenerating drawings, building the site, the tooling). For the project overview see
project-summary.mdor the live site: https://alvinr.github.io/tbs/.
/ project root — engineering reports (*.md), config, this README
src/generators/ all 2D diagram generators + the tooling (lint.py, costing.py, …)
src/models/ SketchUp 3D model generators (emit Ruby, drive SketchUp)
diagrams/ generated PNGs/SVGs (output of src/generators/)
skills/ codified drawing/labeling conventions
published/ MkDocs input — auto-populated by publish.sh (gitignored, never edit)
site/ built site output — gitignored, never commit
.githooks/ versioned git hooks (the pre-commit drift gate)
dependencies.yml machine source: script → output-file graph (see "Tooling")
mkdocs.yml site nav + theme publish.sh sync + build + deploy
CLAUDE.md holds the working conventions (drawing style, report style, the add-a-document
workflow) in more depth — it's worth a read before adding content.
-
Interpreter: CPython 3.9 — developed and run on 3.9.6 (the macOS system
python3). -
Packages:
requirements.txt— the single, pinned dependency list (for the 2D diagram generators and the documentation site). Set up a virtual environment from it:python3 -m venv .venv .venv/bin/pip install -r requirements.txt # then activate it, or prefix commands with .venv/bin/ -
3D models additionally need SketchUp + the live-send bridge — not a pip dependency (see "The 3D SketchUp models").
The drift tooling (lint.py, costing.py, facts.py, deps.py) and the SketchUp model generators
use only the Python standard library, so the pre-commit hook runs on any python3 with nothing
installed. Only the diagram generators and the site build need requirements.txt.
Each generator writes one or more PNGs into diagrams/:
python3 src/generators/generate_<name>.py # e.g. generate_walkway_diagram.pySpatial values come from src/generators/tbs_constants.py (the single source of truth for
geometry). Don't hardcode a dimension — import the constant. After changing a constant, see
"What a change cascades to" below for exactly which generators to re-run.
Models live in src/models/ and emit Ruby that builds the model in a running SketchUp:
python3 src/models/generate_sketchup_model.py --save # write the .rb to src/models/<name>.rb
python3 src/models/generate_sketchup_model.py --send # build into the OPEN SketchUp document--saveis non-interactive (just writes the.rb);--sendrequires SketchUp open on the correct document with the live-send bridge listening — it clears and rebuilds the active model.- Open a blank document before
--sendfor a new model so you don't clobber an existing.skp; then "Save As" tomodels/<name>.skp. - Commit the generator
.py+ the regenerated.rb+ the.skptogether. (Sketchfab re-uploads are a manual step.)
Models share geometry helpers and constants from generate_sketchup_model.py (imported as ov),
so they stay in sync. See dependencies.yml for the model → output mapping.
Numbers are computed, never transcribed — each kind of value has exactly one home:
| Store | Owns | Consumed by |
|---|---|---|
src/generators/tbs_constants.py |
geometry & engineering values | generators, models, weight/cost scripts |
src/generators/parts.py |
the parts registry — every purchasable item: qty, type, supplier, low/high cost, verified size, cyanotype chemistry | the master BOM, every report's parts list, the dimension audit + cyanotype shopping list (block-injected); costing.py derives section totals from it |
src/generators/costing.py |
the scenario cost model (mid + low/high budgeting bands) on top of the registry | the cost tables in the docs (block-injected) |
src/generators/facts.yml |
cross-referenced prose numbers + the regex to police them | the linter |
dependencies.yml |
the script → output-file graph |
the linter (cascade + validation) |
parts.py is the procurement source of record; costing.py is a reconciled scenario layer. The
registry generates master-shopping-list.md (the by-type BOM + supplier consolidation), every
report's §Parts-List, component-dimension-audit.md §1, and the cyanotype chemistry-shopping-list.md
— all wrapped in <!-- BEGIN parts:KEY --> blocks:
python3 src/generators/parts.py --inject # rewrite the parts: doc blocks from the registry
python3 src/generators/parts.py --check # assert each system reconciles with costing
python3 src/generators/parts.py --master # print the by-type BOM to stdoutThe invariant is lint-gated both ways: parts.py --check-blocks (docs match the registry) and
costing.py --check-registry (each section total == the sum of its registry systems), so a cost
can't drift between the BOM and the budget.
Cost tables in the markdown are wrapped in <!-- BEGIN costing:KEY --> / <!-- END … --> blocks
and regenerated by costing.py:
python3 src/generators/costing.py --inject # rewrite the doc cost tables from the source
python3 src/generators/costing.py --check # assert the figures are internally consistentfacts.yml prose figures and the per-session energy budget are block-injected the same way:
python3 src/generators/facts.py --inject # fill the <!-- fact:KEY --> placeholders
python3 src/generators/calculate_energy_budget.py --inject # fill the <!-- energy:KEY --> figuresThe energy injector fills the <!-- BEGIN energy:KEY --> placeholders in electrical-report.md
§3.1/§3.2 from calculate_energy_budget.py — the per-session, daily-roll-up, and autonomy model.
It is lint-gated (the energy doc-blocks gate), so those figures cannot drift from the model.
Rerun --inject after any spec change that affects power, and regenerate the electrical
Sheet 1 summary panel (generate_electrical_diagram.py) to match. (daily-energy-report.md
presents the same model in prose but is not yet block-injected.)
src/generators/lint.py is a commit-time linter that stops document-side drift. It runs
automatically before every commit via the versioned hook — enable it once per clone:
git config core.hooksPath .githooks # one-time install; emergency bypass: git commit --no-verifyRun it by hand any time:
python3 src/generators/lint.py # all checks (4 gates that BLOCK + 7 advisory warnings)Gates (block the commit): costing reconciliation, costing doc-blocks match the source,
fact placeholders match the registry, energy doc-blocks match calculate_energy_budget.py.
Warnings (advisory): facts-registry agreement, dependencies.yml validity, table arithmetic,
missing-cascade, hardwired-literal, duplicated-geometry equality.
On-demand sub-commands:
python3 src/generators/lint.py --cascade PROC_TRAY_X_R # what re-runs if this constant changes
python3 src/generators/lint.py --literals # full hardwired-literal backlog scan
python3 src/generators/lint.py --duplication # compare the duplicated 3D emittersAfter editing a constant, ask the tooling rather than a table:
python3 src/generators/lint.py --cascade <CONSTANT_NAME>It greps every script that reads the constant and lists the exact PNGs / .skp / .rb each
writes. If you git add a tbs_constants.py change without the regenerated outputs, the
pre-commit missing-cascade check names the specific files you still owe.
component-dependency-map.md is the human rationale (component → constant registry, the
subsystem × diagram matrix, per-model narratives); dependencies.yml is the machine source.
src/generators/editorial_lint.py enforces the report narrative house style codified in
skills/skill_report_writing.md (the counterpart to lint.py,
which guards the numbers). It scans the published reports (publish.sh MD_FILES +
project-summary.md).
It is a manual step — not wired into the pre-commit hook. Run it by hand:
python3 src/generators/editorial_lint.py # gates + advisory
python3 src/generators/editorial_lint.py --gates # gates only (exit 1 on failure, for future CI)Gates (deterministic, exit 1 on failure): American spelling (British → American), and every
item in a Source References section carries a hyperlink.
Advisory (review; never blocks): banned stale design terms (e.g. 55-gal, colonnade,
Portacool Jetstream — extend _BANNED), a registry-known value restated raw that should be a
<!-- fact: --> placeholder (the highest-leverage check — it auto-cascades once wrapped), mixed
thousands-separator styles, and reports missing a Source References section.
Scope: it reads the report
.mdprose only — it can't see British spellings baked into diagram labels (inside the generator.py) or.skpmodels; those still need a manual sweep.
bash publish.sh # sync docs → published/, build, and gh-deploy to GitHub Pages
bash publish.sh --local # sync + serve a local preview at http://127.0.0.1:8000
bash publish.sh --build # sync + build to site/ only (no deploy)publish.sh syncs the root .md files + diagrams/ into published/, copies
project-summary.md → published/index.md (the site Home), builds with MkDocs, optionally writes
the PDF brochure, and pushes the gh-pages branch. It auto-detects whether a git remote exists and
falls back to --build if not. On some systems run it as PATH=/usr/bin:$PATH bash publish.sh.
Never edit published/ or site/ by hand — they are regenerated.
- Write
<name>.mdin the repo root (+ asrc/generators/generate_<name>.pyif it needs drawings). - Register it in
publish.sh(MD_FILES/DIAG_FILES) andsrc/generators/setup_docs.py(MD_FILES/DIAG_IMAGE_FILES+ the index table), and add anav:entry tomkdocs.yml. - If you added a generator/model, add its
script → outputsentry todependencies.yml. bash publish.shto deploy; commit the script +.md+.pngtogether.
The full checklist is in CLAUDE.md.
- Commit the related source together (script +
.md+ regenerated.png/.skp/.rb). - Stage only the files you actually changed (no
git add -A). - The pre-commit gate must pass (or
--no-verifyfor an explicit emergency bypass). published/,site/, and.claude/are never committed.
© 2026 Alvin Richards — released under the GNU AGPLv3. Live site: https://alvinr.github.io/tbs/
