Decisions
Status: architecture decision records for Strido. Decision date 2026-10-31 unless noted.
ADR-001 — Keep the Yertle mechanics, add a modular sensor head
Context. Mini Pupper 4 is a pre-production product without published CAD, BOM or PCB design. Options. (A) Re-model a Mini Pupper 4 clone from photos. (B) Design a new robot. (C) Keep the published, physically built Yertle legs and frame and add the sensor head, electronics and software. Decision. C. Consequences. Mechanics inherit a real build history; every change is a CAD boolean on the upstream STEP/STL, verified by the harness. Reconsider only if: MangDang publishes Mini Pupper 4 CAD under an open licence.
ADR-002 — CAD as code, verification as gates
Context. People print and assemble from this repository alone; a wrong hole costs a reprint. Decision. All new and modified parts are CadQuery/manifold booleans in strido_cad; every check (interference, ROM, fits, clearance, sensors, electronics, BOM coverage) is an executable gate with a tracked report. Tolerance bands and contact rules live in hardware/verification/*.yaml with their rationale. Consequences. Physical claims are reproducible; a gate may only be relaxed by a recorded decision. Reconsider only if: a parametric CAD system with equivalent scripting replaces CadQuery.
ADR-003 — uv workspace with layered packages
Context. The robot runtime must stay light (Pi, ROS 2 image) while tooling needs CadQuery, MuJoCo, PyTorch and WeasyPrint. Options. (A) One package with extras. (B) A uv workspace: core, cad, verify, publish, sim, ntx, workspace. Decision. B, with import-linter contracts (core imports nothing; publish → verify → cad → core; sim and ntx never import CAD). Consequences. pip install packages/strido-core --no-deps is all the robot needs. Reconsider only if: the package count becomes a maintenance burden without dependency benefit.
ADR-004 — Fix upstream defects as derived parts, not by editing upstream files
Context. The verification found upstream issues: the front servo mounts lack the hip-pivot pilot on the face the front legs use; the right-leg femur horn pattern is 30° off the STEP; the connector registration is 22° off. Decision. Keep upstream/yertle/ byte-identical; express fixes as data (hardware/assembly/corrections.yaml) and derived parts (left_servo_mount_fl, right_servo_mount_fr in the print kit). Consequences. The print kit contains two extra part types; upstream updates can be diffed cleanly. Reconsider only if: upstream publishes corrected files.
ADR-005 — Joint limits include the mirrored partner leg and running clearance
Context. Per-leg limits allowed both front feet to meet under the body when both hips swing inward. Decision. The hip sweep places the axle partner at the mirrored angle and requires ≥ 0.3 mm clearance as well as no overlap. Consequences. Narrower inward hip limits (≈ −17.5°); firmware, gait, simulation and NT-X share them through code generation. Reconsider only if: a controller coordinates legs to avoid inter-leg contact.
ADR-006 — Feedback-free learned controller
Context. SPT5435LV hobby servos report no position or torque. Decision. The policy observes only IMU, command, previous action and a gait clock; the firmware's slew limit and clamps are modelled in training; the numpy runtime is shared by simulation and the robot. Consequences. Lateral and reverse tracking are weak; transfer depends on measured servo response. Reconsider only if: servos with feedback (e.g. serial bus servos) replace the SPT5435LV.
ADR-007 — NT-X integration through an extension, not core edits
Context. NT-X dispatches hardware only to Unitree SDK2. Decision. Ship catalog assets and a registry robot adapter in strido-ntx; document the one core change NT-X needs; never edit the NT-X repository from here. Consequences. Simulation, Studio and training work today; live hardware sessions wait for the NT-X change. Reconsider only if: NT-X adopts registry-based hardware dispatch (then add the target file).
ADR-008 — VitePress site and WeasyPrint book
Context. Builders need a browsable catalogue, a 3D model and a printable A4 book. Decision. VitePress (as in the other workspace projects) with a three.js viewer of a GLB exported from the CAD; the book is HTML + CSS rendered by WeasyPrint so fasteners can be drawn in true millimetres. Consequences. Node 26 for the site; Pango for the PDF. Reconsider only if: the site must be served without a build step.
ADR-009 — Accept the upstream servo-mount M3 pilots, with a slicer action
Context. The fit check grades a printed M3 pilot above 0.93 d (printed size, CAD − 0.15 mm) as "no grip". 29 screws of the upstream servo-mount family (left_servo_mount, right_servo_mount, their derived _fl/_fr variants) and servo_mount_top_bracket thread into 2.94–2.97 mm CAD pilots (0.93–0.94 d printed) with 4–12 mm of engagement. Upstream Yertle robots are printed and assembled with exactly these holes; most FDM printers print small holes 0.2–0.3 mm undersize, which lands them in the weak-grip band. Redesigning the four mount parts would change upstream geometry for a printer-dependent margin. Decision. Keep the band at 0.93 d and list these parts in accepted_deviations of hardware/verification/tolerances.yaml with a builder action: print them with X-Y hole compensation −0.15 mm (pilot ≈ 2.65 mm) or, on an uncompensated print, harden the pilot with a drop of thin CA glue before driving the screw and check that it bites before loading the joint (anaerobic thread locker does not cure on plastic and can stress-crack PETG; Loctite 243 data sheet). The fit report shows them as warnings carrying that action; any other part over the band still fails. Consequences. The gate stays red for new parts and for any further widening; the accepted list is explicit and reviewable. Reconsider only if: a printed mount strips a thread (then add Ø2.5 mm re-pilot variants as in ADR-004).
ADR-010 — Long link buckling: a known weak point, accepted only until the Strido long link replaces it
Context. The strength check (strido_verify.strength) sweeps the knee linkage over every pose inside the joint limits with the knee servo at stall (3.43 N·m). The upstream long_link is a bar 5 mm wide in the leg plane and 8 mm across it, 130 mm between its pins. In the leg plane its ends are pinned (K = 1) and its walls give I ≈ 75 mm⁴, so its Euler buckling load is π² · 1500 · 75 / 130² ≈ 65 N. The worst pose drives it with 192 N, so S ≈ 0.34. At the reference pose it is 127 N (S ≈ 0.51); standing (0.70 N·m at the knee, MuJoCo) it is about 29 N (S ≈ 2.2). Across the leg plane it is adequate (S ≈ 1.8 with K = 0.7). Every other check passes S ≥ 1.5. Upstream Yertle robots run this link with servos of the same torque class, so in normal walking it holds. A knee servo pushing at stall against a blocked foot or in an extreme pose bends it sideways in the leg plane. Decision. Treat it as a known weak point, not as an acceptable margin. It is listed under accepted in hardware/strength.yaml with min_safety: 0.3, so the gate reports a warning with the builder action (never command the knee against a blocked foot or a hard stop; after hard landings check the long links for a bow and replace a bent one), and fails again if the margin gets worse. Consequences. A Strido long link with a shaft about 9 mm wide in the leg plane (I ≈ 330 mm⁴ with walls only, S ≥ 1.5 at the worst pose) is the planned fix (TODO.md). It needs the clearance, ROM, printability and strength gates re-run. Once it exists, the accepted entry is removed.
Resolved (2026-11-01). The Strido stiff long link (strido_cad.long_link, printed part stiff_long_link) replaces the upstream link on all four legs: same pins (131 mm apart), bushing eyes and eye thickness, a 10 × 10 mm shaft instead of 5 × 8 mm. Centred on the pin line it came within 0.25 mm of the femur at the stretched knee and the range-of-motion sweep cut the knee range by 5°; its centre line therefore sits 2 mm off the pin line, away from the femur, and every neighbour keeps at least 1 mm (the joint limits are unchanged). The strength check now gives S = 2.53 for buckling and no check needs an acceptance (accepted: {} in hardware/strength.yaml). It prints flat without supports. The builder actions above are no longer required; a bent upstream link still means replacing it with the stiff one.
ADR-011 — Wiring harness routed and checked in the CAD
Context. Until v0.2.11 the wiring existed only as a list with estimated lengths; nothing modelled a cable. Routing it through the assembly exposed defects the list could not show: the 25 A fuse holder on the deck strip left 4 mm for its 14 AWG lead to leave towards the front hip servo; Dupont housings fit neither on the IMU header (2.5 mm under the Pi) nor on the XIAO pins (9 mm under the upper shoulder frame); the LiDAR USB adapter covered the top-shell cable slot; the XIAO-to-PCA9685 wires need ~0.3 m, not 0.1 m; one STEMMA QT cable was missing; the 18 AWG stock was short; and a 60 mm 18 AWG tap sat behind the 25 A fuse alone. Options. (A) Keep the list and fix the defects by hand. (B) Model every cable: anchors on the connectors, tie nodes, a router through the free space (parts plus everything the legs sweep), and a gate. Decision. B: hardware/wiring.yaml is the single source; strido_cad.wiring routes, strido_cad.wiring_geometry adds cables, sleeves, ties and tie mounts to the assembly (renders, viewer, manual, model masses); strido_verify.wiring checks reach, stock, hip-loop slack, ports and clearance in every operating pose. Servo leads run without extensions, cut to length (signal re-crimped). The main fuse moves to the rear cavity (its leads exit through the frame openings), the logic fuse to the back shell with the holder's own 14 AWG lead as the tap, the battery input to the pocket beside the battery. Consequences. W01 (battery input to main fuse, ≈ 0.15 m) is unfused, like the pack's own lead; it runs in spiral wrap clear of moving parts. Builders cut every wire from the generated cut list. Bend radii and the shape of a hanging loop are not modelled (route corners are sharp; loops are checked by length). Any CAD change re-routes and re-checks the harness. Reconsider only if: a compact fused battery input (fuse within a few centimetres of the XT60) fits under the deck, or the cable mass/stiffness measured on a real build differs enough to matter for the loops.
ADR-012 — German site from translation catalogues; assistant service on Cortex Keyring
Context. The site needed German, an AI assistant that answers from the documentation (with images, history and a pinnable panel) and a way for language models and coding agents to use the same knowledge. About a third of the 110 000 words are generated (assembly manual, BOM, verification reports) and change with every make run; the site is static nginx behind Cortex SSO. Options. German: (A) a translated copy of every Markdown file; (B) German strings inside every Python generator; (C) a catalogue per page that maps each English segment (paragraph, list item, table row, heading, diagram) to its translation, rendered at build time. Assistant: (D) a model call from the browser; (E) a service next to the site that holds conversations, searches the pages and calls the model through Cortex Keyring. Decision. (C) and (E). A changed English segment falls back to English (the page says it is partly translated) and make drift fails until it is translated; numbers inside a translated segment carry over, so regenerated reports keep their German; German headings keep the English ids, so every anchor works in both languages. The assistant (assistant/, Node, no dependencies) stores conversations and attachments per signed-in user in SQLite, answers with tools over the same pages (search_docs, read_page, list_pages, search_parts) and serves them as an MCP server to agents holding a Cortex app token; /llms.txt and the Agent Skill package are generated by the docs sync. The app ships its own gateway route: /api/* and /mcp go to the assistant without compression (server-sent events), everything else to nginx. Consequences. English edits need a translation pass (make i18n); the catalogues are tracked source. The Keyring subscriber is the owner, so until the pool can serve a shared key only the owner's account gets answers (others see why). The gateway secret reaches the assistant through the gateway's generated env file (the Sign/Folio contract), which also exposes the stack's other generated values to that container; copying only that key into the host .env (the Canvas contract) is the safer follow-up (TODO). The support widget is off: its proxy would sit in the answer stream. Answers are generated: the bar and docs/ai.md say to check figures on the linked page.