Last Updated: 2026-09-29
A pull request adds or changes exactly one <slug>/. That keeps the render lane
small enough to run geometry on every changed cartridge, and keeps a licence or
provenance question scoped to one object.
Search the tree and the catalog first. A near-duplicate of an existing cartridge should extend that cartridge's parameter space, not open a second slug for the same family — a hyperobject is the family, so two slugs for one family is a bug.
Every human-readable string in project.json — names, descriptions, parameter
labels, mode and part labels, presets — carries both en and es. This is
enforced: y4d-spec check fails on a missing locale, it does not warn. Translate
at authoring time; a retrofit pass never catches up.
Per RFC 0039 the commons is moving to quadrilingual (en/es/fr/pt), starting with the lexicon and with new catalog waves. The machine-checked floor in this repo today is en/es; fr/pt are welcome now and will become required.
Cartridges here are licensed CERN-OHL-W-2.0 (ADR-011, RFC 0038 §9 — an owner
ruling, not a per-PR choice). Ship a LICENSE in your cartridge that matches.
If your cartridge includes files from an upstream project, that upstream's terms travel with them:
- Permissive upstream (MIT/BSD/Apache/CERN-OHL-P) — vendor the files, add a
<slug>/NOTICEnaming the author, source, licence and what was used, and add the carve-out to the rootNOTICE.md. - Copyleft or NonCommercial upstream (GPL, CC-BY-NC-*) — do not vendor.
Consume it as a submodule fetched at build time, declare it in the root
.gitmodules, and record it in both<slug>/NOTICEandNOTICE.md. If it cannot be kept separate, the cartridge does not belong in this commons.
Never ship a LICENSE that contradicts your manifest's declared licence:
y4d-spec check fails on that too.
pip install "hyperobjects-spec[geometry] @ git+https://github.com/madfam-org/hyperobjects-spec@70fe8213de20bc9a699b2bde2c32ee0b02a6d52d"
y4d-spec check ./<slug> # manifest + files
y4d-spec check ./<slug> --render --require-openscad --parity \
--openscad-path libs --openscad-path . # + geometry, both kernels, defaults AND every preset--render is fail-closed. Each mesh must be watertight, of positive volume,
free of inverted bodies, valid as a B-Rep before tessellation, distinct per
part, and in agreement with the other kernel. A preset that raises or produces
broken geometry is a failure, not a note. The declared body count is not
part of that bar yet: the keystone compares it note-first from hyperobjects-spec
0.16.0 (body-count), which SPEC_PIN does not include yet (see Declaring
body counts).
Printability findings (thin walls, overhangs, build volume) are measurements,
never failures.
CI runs exactly this command (see the README's How CI verifies a change). There is no second, hidden bar.
A macOS-green render is not proof. The gate checks BRepCheck plus a
per-solid signed volume before any mesh exists, because a Reversed() solid is
IsValid() yet negative, and because the two OCCT builds disagree: the
tripod-hub fuse is invalid on macOS OCCT and valid on Linux OCP. Run the
command above locally, then read CI's Linux result as the verdict.
- Include libraries by search path —
include <BOSL2/std.scad>,use <dotSCAD/src/…>— never by relative../../libs/paths; CI, the platform worker and the local harness all resolve<Lib/…>throughOPENSCADPATH(libs/for the six pinned third-party libraries, the repository root for the first-party helpers incommons-lib/). BOSL2 isinclude-only:usedrops its$tags_showndefault and every attachable primitive then fails BOSL2's own assertion. - Dispatch every declared part on
render_mode(and, in a CadQuery twin, on thetarget_partglobal): the checker rejects a cartridge whose parts render the same fallback body. - Both kernels are rendered in CI; the local CLI (
/Applications/OpenSCAD.app/Contents/MacOS/OpenSCAD --backend=Manifoldon macOS) is what you run before pushing:y4d-spec check ./<slug> --render --require-openscad --openscad-path libs --openscad-path ..
All 512 cartridges declare a body count; a new cartridge is expected to. Declare the design's count, never the count a defect happens to produce — the declaration is how a later regression becomes visible.
Overrides use stage-qualified keys ("geometry.body_count"), and an
override replaces the base value; it never merges. A mode/part that is more
than one body by design declares it and says why in the source's docstring.
Parametric counts are expressions over parameter ids.
The count is declared, not yet enforced. Until hyperobjects-spec 0.16.0 the
keystone counted bodies but never compared them with the declaration. From 0.16.0
y4d-spec check --render compares them and prints a body-count: note per
mismatch (a part with part_quantities may render one copy or all of them). It
becomes a failure only after a whole-commons nightly at that pin explains every
note. Until SPEC_PIN reaches it, CI says nothing about a wrong count.
- Never leave a cut ending exactly on a face, a pocket wholly inside a solid, or a decorative plate unioned
after the
difference()that vents a cavity — all three export as inverted shells and fail the bar.
--parity compares the CadQuery and OpenSCAD meshes of the same (mode, part)
on three gates: extents (AABB), volume, and shape. The shape gate reports the
placement offset (AABB-centre delta) as a note — or fails it under a per-part
"placement": "strict" — and computes the Hausdorff distance after
alignment, failing above max(0.5 mm, tolerance).
Fix the geometry first. Only where the two kernels genuinely cannot express the
same surface does the manifest declare it, under verification as a base or a
per-part "geometry.parity" override (which, like every override, replaces the
base object whole):
"geometry.parity": {
"enabled": false,
"reason": "The OpenSCAD side cuts a real BOSL2 helical thread; the CadQuery side models it as a revolved sawtooth stack because OCCT booleans against an exact helical solid fail. The divergence is 0.902 x the ISO thread depth on every preset: the thread idiom, not a dimension error."
}enabled (bool), tolerance (mm — widen only with cause), reason (required),
placement ("strict" to fail on offset). A missing reason, or a widened
tolerance without one, is a conformance failure — caught without --render,
so it costs a second, not an hour. Reasons are read in review: name the idiom,
the measured divergence, and where the real fix is tracked. The five standing
exemptions (fasteners, spiral-planter, faircap-filter, relief,
locking-mechanism-hyperobject) are worked examples. Exempt pairs print as
parity (mode, part): exempt — <reason>, and the summary reads
parity=N/M ok, warn=K, exempt=E, failures=J.
Nearly every manifest on main carries at least one constraint (509/512, measured
2026-10-05; a new cartridge is expected to). A constraint
is a rule the configurator evaluates on the parameter set before anything is
rendered; it never reaches a kernel, which is why a constraints-only manifest
change is skipped by the render lane.
"constraints": [
{ "rule": "pen_diameter > wall_thickness * 2",
"message": { "en": "Pen must fit in the hole", "es": "El bolígrafo debe caber en el orificio" },
"severity": "error",
"applies_to": ["pen_diameter", "wall_thickness"] }
]The dialect is the Studio's own safeFormula evaluator
(apps/studio/src/lib/safeFormula.ts), not a general expression language:
comparisons and arithmetic, && / || / !, the ternary c ? a : b, and
parameter names. No string literals (so select parameters cannot be tested),
no function calls, 256 characters / 128 tokens. Write rules only in that subset:
the evaluator swallows exceptions, so an unevaluable rule silently never fires —
it does not shout, it just stops protecting anyone.
Both message locales are required, severity is error or warning, and
every preset the cartridge ships must satisfy every error constraint — a
preset that violates its own cartridge's rule is a bug in one of the two.
animations are parameter-state interpolation, not per-part keyframes: the
API interpolates from from_state to to_state over frames and re-renders
the cartridge from the kernel at each step.
"animations": [
{ "id": "tray-grow",
"label": { "en": "Tray 120 → 300 mm", "es": "Bandeja 120 → 300 mm" },
"from_state": { "tray_width": 120 }, "to_state": { "tray_width": 300 },
"frames": 6, "duration_ms": 3000, "easing": "ease-in-out", "mode": "unit" }
]Both states must name real parameters of that mode, and every intermediate
point has to be renderable — an interpolation that walks through an infeasible
parameter set is a broken animation. Exploded views are not a manifest feature:
they exist only where the source itself reads an explode variable. 20 cartridges
are animated today.
Forty-one are recorded across the wave reports; these are the ones that catch almost every new cartridge:
- Coincident faces are not a join. Two members that merely touch export as separate bodies (or an invalid shell). Give every join a real overlap — at least 0.01 mm of penetration, or fuse the pair in one union.
- Do not offset twice. A
.transformed(offset)followed by anextrudethat also carries the offset lands the feature at double the distance; it renders fine and is wrong. - A degenerate helix radius (a sweep radius that reaches zero at either end) yields a non-manifold sweep. Clamp it.
- Tangent cylinders do not boolean. Exactly-touching cylinders — and a wall tangent to every tooth root — leave CGAL/OCCT with a zero-thickness contact. Overlap them or step them apart.
- Selects are strings. A select parameter arrives as a string on both
kernels;
int(...)-ing it on one side and comparing strings on the other is how the same preset grew a 5 mm additive prism on one kernel only. - BOSL2
diff()keeps by tag. Children are removed only where their tags say so; akeep-tagged child is never bored. Check both sides of adiff()/difference()pair render the same voids. - Name your font. Neither kernel defaults to the same face, so unnamed
.text()diverges by millimetres. Pin the font (keytag is a worked example), and expect glyph outlines to still differ slightly between OCCT and FreeType. - OpenSCAD silently accepts an unknown
-D. A parameter declared in the manifest but never referenced by the.scadis a dead slider, and nothing errors. Grep your source for every parameter you declare. - BOSL2 is
include-only (see above), and rotation composes: OpenSCAD'stwist=+arotates the top by −a where CadQuery'stwistExtruderotates by +a. A herringbone whose halves point the same way is this bug.
ADR-012 rules that commons contributions certify origin with the Developer
Certificate of Origin — a Signed-off-by: trailer on every commit (git commit -s), no CLA. That ADR's scope is Fashion Cabinet first, with this commons
adopting the same rule when its third-party pipeline opens. The sign-off lane
here is pending the ADR-012 operator steps; until they are done, sign off if
you can and no check will block you if you did not.