Skip to content

Stipulations

The load-bearing rules the app is built on. Without these written down, work drifts. Anything new should be checked against this list. All entries are guesses pending review.

Coverage summary: all fifty-nine rules are now directly covered. Fifty-five are pinned by unit tests in src/lib/ts/tests/; the remaining four — user-interface flows that need real mouse events and a real animation loop — are pinned by browser-driven tests under e2e/tests/. Coverage judgments are guesses pending review.

ALWAYS: Always update the authoritative count testing is done.

Format

Every stipulation carries three lines beneath the prose: a stable id (a short kebab-case slug), a test: pointer to the test that pins it, and a code: pointer to the source file and lines that prove it. The slug is what the test index references back.

The shape:

text
N. Plain English statement of the rule.
    - id: short-slug-name
    - test: [Test_File.test.ts](../../../src/lib/ts/tests/Test_File.test.ts) test 1
    - code: [src/lib/ts/path/to/source.ts:42-57](../../../src/lib/ts/path/to/source.ts)

A real example, fully written:

text
99. The world is made of blocks called smart objects (SO). Every SO has three directions {x, y, z}.
    - id: so-three-directions
    - test: [Data_Layout.test.ts](../../../src/lib/ts/tests/Data_Layout.test.ts) "an SO has exactly three directions"
    - code: [src/lib/ts/runtime/Smart_Object.ts:9-14](../../../src/lib/ts/runtime/Smart_Object.ts)

Blocks

  1. The world is made of blocks called smart objects (SO). Every SO has three dimensions / axes
  2. Each axis has three attributes: start, length and end
  3. Two of these (start and end) are defined to be relative to parent's start (except root)

Each attribute has several flavors

  1. plain number
  2. locked number
  3. a formula computes the number

Invariants

  1. Always and only one of them is called invariant. It uses a built-in formula
  2. If the value of an invariant is directly altered (set) by the user, this causes reverse propagation such that computing the formula results in the new value

Root

  1. has no parent
  2. end is always invariant
  3. start is always zero
  4. length can be locked

Locked attribute

  1. The value of a locked attribute must not be altered by propagation

Formulas

  1. A formula attribute dynamically recomputes the value during propagation
  2. Propagation is initially triggered by some change, somewhere within the scope of its formula

Children

  1. A block sits inside at most one other block. Inside means its position numbers are written in its parent's frame.
  2. Moving a parent moves all of its children. Resizing a parent does not change a child's stored numbers.

Repeaters

  1. A block can be marked as repeating along an axis called run. Its first child becomes the master copy. The rest are duplicates of that master.
  2. Each duplicate has a copy the master's stored numbers. run axis' start attribute is incremented using the run length

Fire blocks

  1. A repeater block can also be told to fill the gaps between duplicates. Each filler is shaped to fit its gap and centered along the z axis.

Formula trees

  1. The text of a formula is first broken into pieces — numbers, names, operators.
  2. The pieces are assembled into a small tree.
  3. Walking the tree computes the number from the formula.
  4. The same tree can be walked backwards: starting from a target number T, the system finds the one editable number E inside the tree whose value can be changed so as to make the formula compute T, and writes it to E.

Camera and projection

  1. The world is shown through one camera. The camera defines how every point in the world corresponds to a spot on the screen.
  2. The same definition runs the other way: any spot on the screen becomes a ray that starts at the camera and points into the world. Thus a given mouse location corresponds to a ray piercing straight into the scene. Whatever objects are encountered along that ray are identified in the hits 3D manager

Dragging

  1. A drag of a face of an SO is confined to that face's flat plane.
  2. The drag's screen motion becomes a ray into the world. The ray's hit point on the face's plane is the new position for the dragged point.
  3. That position is broken down along two directions defined by the face. Each part of the breakdown drives one editable number.

Wider behavior

  1. A change to one cell never quietly changes a cell on another block unless that other block has a formula referring to the first.
  2. Undo brings the world back to exactly the state it was in before the last user action. Nothing is recomputed.
  3. Saving and loading is a round trip: the world after a save-then-load matches the world before the save, after the load-time recompute runs.

Orientation and units

  1. Each axis on a block carries an angle. The block's overall rotation is the composition of those three angles, applied in a recorded order. The order matters — applying the same three angles in different orders produces different visible results.
  2. Stored position numbers are in millimeters. The user can type a value in inches or feet; the parser converts to millimeters before storage. Anything that reads a stored value receives millimeters.

Givens and formula safety

  1. The user can define named values outside any block — call them ALPHA, BETA. Formulas can reference these names the same way they reference a block's cells. The named values are saved with the scene and restored on load before any formula is recomputed.
  2. Formula references must not form a loop. If one cell's formula reads a second cell, and the second cell's formula reads the first, the system refuses the loop rather than recomputing forever.
  3. When a formula tree is walked backwards from a target number, exactly one writable cell inside the tree receives the new value. If there is no writable cell — for example because every candidate is locked — the write is refused. There is never more than one writable target.

Display, history, and repeater details

  1. Every block has a visibility flag. A block also has a flag that hides all of its children. Hidden blocks do not render but still participate in the rule pipeline (formulas, layout, save and load).
  2. When the user drags an edge, corner, or face, the resulting new value is rounded to the current precision grid before it is written to storage.
  3. After undo, redo brings the world forward to the state that was just undone. Like undo, redo restores stored values; nothing is recomputed during the restore.
  4. A repeater carries its own spacing parameter, separate from the master block's run-direction length. Duplicates step along the run direction by the spacing, which need not equal the master's run-direction length.
  5. A fire block's size on the cross direction (the direction perpendicular to the run that is the tallest non-run side of the master) matches the master's size on that direction. The fire block is centered along that direction.

Cell modes and name resolution

  1. Setting a formula on a cell clears any lock that cell carried. Once the cell's value comes from a formula, the lock that previously protected its plain number is cleared.
  2. When a formula uses a bare name to refer to another SO, the resolver starts at the formula's host SO and walks up the parent chain. At each level, it looks among the children of that level for an SO with the matching name. The first match wins.

Save format and named-value locks

  1. A repeater's duplicates are not saved with the scene. The master is saved along with the repeater configuration; the duplicates are rebuilt from the master after load.
  2. A locked named value is protected from reverse propagation, the same way a locked cell is. Reverse propagation that would otherwise change the named value refuses the write.

Geometry, viewing modes, and error state

  1. Every SO is shaped like a box with eight corners, twelve edges between them, and six faces.
  2. The camera has two viewing modes — 3D mode (the normal view where things farther from the camera look smaller) and 2D mode (a flat view where things at different distances keep their real size). The choice is stored as a user preference outside the saved scene file.
  3. An error reported on a cell stays on that cell until it is explicitly cleared.

Identity, persistence, and deletion

  1. Every SO carries a unique identifier that stays the same across save and load. Formulas, parent links, and the saved selection all point at SOs by this identifier; identifier stability is what lets save and load round-trip the world.
  2. Deleting an SO removes every descendant of that SO too. Every formula inside the deleted subtree is cleared, and every formula on a surviving SO that referenced any deleted SO is also cleared.

Precision and grid

  1. Changing the precision setting snaps every plain-number cell in the scene to the new grid. Cells that hold a formula are not touched.

Editing lock and decorations

  1. There is an editing-lock toggle. While the lock is on, clicks on the canvas do nothing; the cursor stays as the open-grab-hand.

View-mode and rotation

  1. Switching from the normal three-dimensional view to the flat view snaps the camera onto the front-most face of the topmost SO and saves the prior orientation. Switching back restores that saved orientation.
  2. When the rotation-snap toggle is on, releasing a tumble drag animates the orientation to the nearest face-aligned orientation. Turning the toggle off restores the orientation that was in place before the snap was last turned on.

Drag

  1. A drag with a current selection edits that selection — moves a corner, an edge, or a face. A drag with nothing selected tumbles the camera around the topmost SO.

Preferences layer

  1. A long list of user preferences persists across reloads through browser storage: chosen unit system, theme colors, the view mode, edge thickness, grid opacity, the precision level, the editing-lock toggle, which decorations are visible, which parts table tab is open, the parts hide list, which detail panels are showing, and several more. Preferences are not part of the saved scene file — they belong to the user, not the design.

Center letter in formulas

  1. A formula may reference the center of any direction using the bare letter c (host direction) or the axis-qualified form <direction>.c for a different direction. The center resolves to start-plus-end-over-two on the named direction, computed fresh on every read. Center references are read-only — reverse propagation refuses to write through a center, and a drag on a cell whose formula reads a center posts the message "cannot drag a center" to the on-screen status strip. A formula on a start, end, or length cell that references the same-direction same-SO center is rejected at the moment it is typed.

Cutting a smart object in half

  1. The user can cut the selected smart object in half along its longest direction (measured by the plain stored length value). The original keeps the lower half; a new sibling appears as the upper half (named with a numeric-suffix bump matching the duplicate routine's naming) and becomes the selected part. The cut is refused — with a red message in the on-screen status strip and no change to the scene — when two directions are tied for longest, and when the selected part is root, a clone of a repeater, the template of a repeater, or any other part with children. Repeaters are an exception to the "has children" refusal: a cut on a repeater produces two repeaters, each carrying its own copy of the template; clones in each half regenerate from each half's own template on the next sync. On the cut direction, the formula on whichever attribute is the invariant is preserved unchanged on both halves; the two non-invariant attributes are rewritten so each half's length value equals half the original's old length on that direction. The exact rewrite depends on which attribute the invariant points at: when the invariant is on length, the original's end and the new sibling's start are altered to the half-way point; when the invariant is on start, the length on each half is divided in half (and the original's end lands at the half-way point); when the invariant is on end, the length on each half is divided in half (and the new sibling's start lands at the half-way point). "Half-way" means: alter the formula so its value evaluates to the half-way point; if the relevant attribute carries no formula, write the half-way value directly as a plain number. Both halves carry the original's formulas on the two directions that are NOT being cut, copied unchanged.

Expand scope

What is the granularity of our stipulations? Are they fractal enough without being crazy?

Strong candidates

these have tests already in the test index but no rule citing them, so the tests are quietly proving behavior the catalogue does not name:

  • Angle math (types/Angle.ts) — there's a test file pinning angle normalisation, conversion, and comparison, but no rule names what those tests prove. Possible rule: "angles normalise to a canonical range; the same angle written two ways compares equal."
  • Coordinate math (types/Coordinates.ts) — point/size/rectangle helpers, with tests, no rule.
  • Hit testing (events/Hits_3D.ts) — there's a test file for 3D hit geometry (point-in-polygon, segment proximity, front-facing detection), no rule. The click-stack drill-down behavior (each click rotates through stacked parts under the cursor) is described in the handoff but not in the catalogue.
  • Topology (render/Topology.ts) — there's a test file ("Topology") for the unified-endpoint pipeline, no rule.
  • Save-format migrations (managers/Versions.ts) — there's a test file ("Versions") for the v1-through-v9 chain, no rule. The catalogue says scenes round-trip but does not say "every old save format has a one-way upgrade to the current shape."

Plausible candidates

load-bearing behavior that no rule pins:

  • Selection-as-list semantics — the "plain click replaces, command-click toggles, multi-select hides the three-tab strip" behavior is described in the handoff but is not a catalogue rule.
  • Parts tree walks (managers/Parts.ts) — the collapsed-rows model, the visible-row count, the hide-list generation — there are invariants (e.g. "collapsing a row hides every descendant; the visible-row count equals total minus hidden") that are not pinned.
  • Orientation math (algebra/Orientation.ts) — sits in algebra (the explicit-yes area in the development process), no rule.

Out of scope per the development process:

  • Visual layout, color, animation tick (utilities/Colors.tsrender/Animation.ts, the R_* decoration files, editors/* for inline editing widgets, common/Constants.ts for sizes and fonts) — the development process explicitly says these resist formalisation.
  • Pure plumbing — type-alias files, prototype extensions, event plumbing without a clear invariant.

How the development process expects this to play out: don't extract them in a sweep. When work next touches one of these modules, write the rules its code assumes and add them at that point.

The validator and dashboard will keep the catalogue honest in the meantime — the strong candidates above are visible as a tell, since their tests sit in the test index without a stipulation: back-pointer.