Skip to content

Single-enumerator refactor: framing lumber has one home - #14

Merged
colbyt merged 3 commits into
mainfrom
refactor/single-enumerator
Aug 9, 2026
Merged

colbyt merged 3 commits into
mainfrom
refactor/single-enumerator

Conversation

@colbyt

@colbyt colbyt commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements Decision 6 in docs/design_decisions.md ("derived artifacts are
generated, not hand-maintained — and kept honest by tests"), specifically its
"still partially built" note that framing math lived in more than one place.

Framing-lumber positions (studs, plates, kings, jacks, headers, cripples,
sills, blocking) now have one home: enumerateMembers() in
web/js/members.js.

  • scripts/export_members.mjs (new) bakes its output to a committed
    web/assets/lib/members.json, with a --verify mode mirroring the
    existing gen_wall_instances.py --verify pattern.
  • seh_lib/wall_builder.py — shared by generate_wall_library.py and
    every library/modules/<id>/compiler.py — now builds FreeCAD box shapes
    from members.json via a new boxes_from_members(), instead of
    independently recomputing stud/cripple positions, king/jack heights,
    header/sill/subheader/blocking placement. boxes_from_members() is pure
    Python (no FreeCAD import) and is unit-tested without FreeCAD.
  • build_lib.py bakes members.json before the freecadcmd geometry step
    (both in-place and in --verify).
  • CI (ci.yml) gets a node scripts/export_members.mjs --verify freshness
    check on every push; geometry-verify.yml (FreeCAD BREP-verify, the
    mandatory gate this refactor can't check locally — see below) gets the new
    files added to its path filter plus a setup-node step, since
    build_lib.py --verify now shells out to node.
  • The parity tests grow a matching pair for the new artifact:
    tests/members_export.mjs (JS: members.json == live
    enumerateMembers() for every module) and tests/test_members_boxes.py
    (Python, FreeCAD-free: the same acceptance numbers
    tests/enumerate_parity.mjs already asserts on the JS side, re-asserted
    against what boxes_from_members() produces from the identical
    members.json). Object naming (king_stud_left/right,
    jack_stud_left/right, etc.) is preserved verbatim since
    library/modules/*/expect.yaml pattern-matches on those names for
    libtools validate-output.

Deliberately NOT touched: OSB sheathing

The JS/3D side models exterior OSB as several rectangular strips split
around a rough opening; the Python/FreeCAD side still cuts a single full
sheet with a rectangular hole. This is a real, pre-existing geometry
difference between the two implementations — already tracked as
CAD-AUD-005 and recorded as a known_issue on every aperture module's
meta.yaml. members.json still carries sheathing role entries (so JS
consumers are unaffected); boxes_from_members() just skips them. This
refactor does not silently pick a winner between the two OSB approaches —
consolidating OSB generation is separate follow-up work, noted in
docs/design_decisions.md.

No dimensional numbers changed. Every acceptance number in
tests/enumerate_parity.mjs (studs at 0/16/32/46.5in, window king/jack/
header/sill/cripple dimensions, etc.) is unchanged and re-verified from the
Python side in tests/test_members_boxes.py against the same
members.json.

Verified locally (Node 26, Python 3.12, no FreeCAD on this machine)

  • All JS tests in ci.yml's "JS test suite" step, including
    tests/parity.mjs and tests/enumerate_parity.mjs (both still pass
    unchanged — 4/4 and 9/9), plus the new tests/members_export.mjs (17/17).
  • node scripts/export_members.mjs --verify passes.
  • python -m pytest tests/test_blocking_math.py tests/test_members_boxes.py
    (26/26). tests/test_library_drift.py and tests/test_generated_entry.py
    could not run locally — they import libtools, which requires a network
    pip install from git+https://github.com/OpenSourceEcology/vcs-library
    not available in this sandbox; unrelated to this change.
  • Static web smoke test: python3 -m http.server over web/, confirmed
    index.html, js/main.js, assets/lib/specs.json, and the new
    assets/lib/members.json all serve 200. web/js/fcstd.js is untouched
    (git diff confirms zero changes to that file).

What CI must still prove (deferred — no FreeCAD on this machine)

geometry-verify.yml's python3 build_lib.py --verify --no-thumbs is the
mandatory gate: it must confirm that regenerating web/assets/lib/*.brp,
volumes.json, and cad_library/*.FCStd from the refactored
seh_lib/wall_builder.py (now reading members.json instead of
recomputing framing positions) produces geometry identical to what's
currently committed, for all 15 modules. Since boxes_from_members()
reproduces the exact same box positions the old inline math computed (same
constants, same coordinate convention, verified above against the JS
acceptance numbers), no .brp/.FCStd changes are expected — but this PR
does not (and, without FreeCAD locally, cannot) regenerate or touch those
binary assets itself. libtools validate-output (also in that job) is the
check that confirms object naming still matches each module's
expect.yaml role patterns.

Not merging

Opening for review — not merging this myself.

colbyt added 3 commits August 9, 2026 06:12
Bake the single JS framing-member enumerator (web/js/members.js) to a
committed JSON artifact via a new node scripts/export_members.mjs, with a
--verify mode (mirrors gen_wall_instances.py --verify) and a matching
tests/members_export.mjs freshness test. This is the export half of the
single-enumerator refactor (Decision 6, docs/design_decisions.md): the
Python geometry builder will consume this file instead of re-deriving
stud/king/jack/header/cripple/sill/blocking positions.
…framing math

seh_lib/wall_builder.py's plain-wall and aperture-panel builders (used by
both generate_wall_library.py and every library/modules/<id>/compiler.py)
now build framing-lumber FreeCAD boxes from web/assets/lib/members.json via
a new boxes_from_members() instead of independently computing stud
positions, cripple positions, king/jack heights, header placement, sill/
subheader/blocking geometry. That math has one home now: enumerateMembers()
in web/js/members.js.

boxes_from_members() is pure Python (no FreeCAD import), so it's covered by
a FreeCAD-free test, tests/test_members_boxes.py, which cross-checks the
same acceptance numbers tests/enumerate_parity.mjs asserts on the JS side
against the identical members.json the Python side reads.

Object naming (king_stud_left/right, jack_stud_left/right, etc.) is
preserved verbatim via _member_object_name(), since library/modules/*/
expect.yaml pattern-matches on those names for libtools validate-output.

Deliberately NOT touched: OSB sheathing. The JS side models it as several
strips split around a rough opening; this module still cuts a single full
sheet with a hole — a real, pre-existing behavior difference between the
two implementations (CAD-AUD-005, already recorded as a known_issue on
every aperture module's meta.yaml). members.json still carries 'sheathing'
role entries for JS consumers; boxes_from_members() just skips them, so this
refactor does not silently pick a winner between the two OSB approaches.

stud_positions()/cripple_x_positions() are left defined (still part of
seh_lib's exported API, still unit-tested transitively via
scripts/gen_library_entries.py's own separate copy) but are no longer
called by the geometry builders.
build_lib.py now bakes web/assets/lib/members.json (via node
scripts/export_members.mjs) before the freecadcmd geometry step, both in
the in-place regenerate path and --verify (which checks freshness the same
way it already does for wall_instances.yaml and specs.json — bake_geometry.py
always reads the real committed members.json, not a temp copy, so if it's
stale the geometry comparison downstream is comparing against stale framing
math too).

CI: ci.yml's fast lane gets a `node scripts/export_members.mjs --verify`
freshness check and runs tests/test_members_boxes.py + tests/members_export.mjs.
geometry-verify.yml (the FreeCAD BREP-verify job — the mandatory gate for
anything this refactor can't check without FreeCAD) gets a setup-node step
and members.js/constants.js/systems.js/export_members.mjs added to its
path filter, since those now affect baked geometry too.

docs/design_decisions.md Decision 6 and docs/adding_modules.md are updated
to describe the new members.json step and explicitly call out what's still
NOT single-sourced (OSB sheathing, CAD-AUD-005).
@colbyt
colbyt merged commit ea037e4 into main Aug 9, 2026
4 checks passed
@colbyt
colbyt deleted the refactor/single-enumerator branch September 2, 2026 18:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant