Skip to content

LayerForge: prioritized backlog for the next sessions

Written 2026-09-24, after PR #88. Tracking issue: #98. Requirements: Alignment requirements (TR-1 to TR-16). Formal target: specs/alignment.allium. Today's behavior: Requirements.

Sizes are my estimates: S under an hour, M one session, L several sessions. Nothing below is started when this page is written. Rank is the order I recommend. Change it if the owner's priorities change.

How the order was chosen

  1. Cheap, independent fixes first. They shrink the backlog and carry little risk.
  2. Do the changelog (#97) before any breaking change (#83, #87 change options and defaults).
  3. Foundations before what uses them. Settings (#87) feed units, sizes, colours and the number. The shape contract (#84) feeds clearance, shape choice and the symmetry test.
  4. The alignment core comes last, because it needs footprints, sizes, settings and adjacency to be right.
  5. Ask the owner early for the two decisions that can change the design: #73 (overlapping shells) and #96 (measured laser numbers).

Ranked list

Rank Issue Title Size Depends on Why here Owner input
1 #71 Negative mark options end in a traceback (done, #100) S none Small, finishes the input checks from #67. #87 can reuse the validation. no
2 #78 Remove or use Model.calculate_height (done, #101) S none Dead code. Quick. no
3 #77 Investigate intermittent shapely warning (done, #102) S to M none Degenerate polygons may hide a real bug in the mark code. Find it before that code is reworked. no
4 #82 + #72 Mark identity: adopt stored coordinates; nearest wins (done, #103) S to M none Same-mark-at-two-places breaks alignment today (TR-10). Small change with a clear test. no
5 #97 Changelog and version policy (done, #104) S none Must exist before #83 and #87 land. no
6 #73 Investigate overlapping shells (decided: document the limit) M none Changes how contours are built, which #89 relies on. Decide before rank 14. decided 2026-09-25
7 #87 Settings model and config file (done, #115: the mechanism and the six keys that exist today; each later issue adds its own keys) M to L #71 helps Everything else reads its numbers from here (TR-16). confirm the file format and key names
8 #96 Calibrate the proposed defaults owner none (use with #87) Turns proposed numbers into measured ones. Do it any time after rank 7, or in parallel. The generated sheet of #141 does the cutting part for any machine. yes: cut the sheet of #141 and record the numbers
9 #74 Units and physical SVG size (done, #147: --units, the units key, width and height with the unit) M #87 Laser software may import at the wrong size. High value. no
10 #84 Shape contract (done, #156: outline() and symmetry_order on each shape; the square and triangle are smaller at the same size) M none Closed outlines, one size, anchor and angle. Needed by 11, 15, 17. confirm angle 0 = +x, size = circumscribed diameter (already chosen)
11 #85 Clearance uses the whole hole (done, #159: the footprint, the web marks.min_web_ratio, and item 2 of #108; the number clause stays with #75) S to M #84 Cheap once outlines exist. no
12 #62 + #76 Mark size and default distance from thickness and kerf (done, #170: size = the larger of 1 x layer height and 1.5 x kerf, --kerf, the defaults of layer height and kerf follow --units) S to M #87 Small once settings exist. Makes small models get marks. no
13 #75 Number placement and fit (done, #184; moved to session E with #83, which shares the fill colour and font-size of TR-14) M #87, #85 The number is now load-bearing (TR-3). It also sits on the first hole today. no
14 #83 SVG for the laser (done, #182) M #87, #74 Colour groups, hairline, number only. Removes --mark-color. Colour does not set the operation in every program (TR-17). no: red cut and black engrave stay as default groups. The probe of #141 shows what your software does.
15 #89 Adjacent pieces and overlap (done, #186: adjacent_pairs, not yet used by the command) M none (#73 decided) Foundation for #63 and #92. no
16 #90 Rotational symmetry test (done, the PR of this row: rotation_symmetry, API only) M #84 The core safety check (TR-2). no
17 #91 Brute-force oracle for #90 (done, the PR of this row: tests only) S to M #90 Guards the safety check. Write it in the same session as #90 if possible. no
18 #61 Shape choice by need (done, the PR of this row: choose_shape, the triangle at the defaults; Breaking) M #90, #84 Removes the rotation-blind first circle. no
19 #63 Marks for pairs of layers, with look-ahead (done, the PR of this row: plan_marks and ReferenceMarkCalculator.choose_mark_for_pair choose and retire marks per pair, reusing before creating; Breaking) L #89, #85, #62 The largest change. The one-pass loop must look at the next layer. decide bores versus pair-local was settled: pair-local
20 #92 Pre-write check and --allow-unaligned (done, the PR of this row: check_alignment, the setting checks.allow_unaligned; a number that does not fit stays a warning, decided 2026-10-09) M #89, #90, #75, #85 The promise of the tool: fail rather than write ambiguous layers. no
21 #60 Close out: end-to-end proof of unique alignment (done, the PR of this row: test_alignment_end_to_end reads the written files of a cube, a cylinder, a tube and an L-shaped part; TR-4 and the whole-stack bore stay open as G-4 says) S 15 to 20 Cube, cylinder, tube, L-shaped part: each layer aligns in exactly one way. no
22 #79 Test gaps (checked 2026-10-10: items 1 and 2 and the TR-10 end-to-end item are settled; items 3 to 5 and the sliver point are open) M 15 to 20 Some items (repair path, many-loop cost) matter more after the rework. no
23 #95 Assembly guide and user docs M 7, 9, 14, 20 The guide needs the final behavior. Each feature PR updates its own docs. review by the owner
24 #80 MkDocs 2 notice S none Low priority. decide whether to pin
25 #94 Cut-through numbers, numbers as outlines M #75, #83, #87 Later. choose an approach
26 #93 Design dowel holes M #96, #87 Later. Design work, no code. A machine-neutral design is proposed in TR-15. yes: review TR-15 (position and count are open)
27 #106 Non-finite option values (nan, inf) pass the input checks (done, #112) S none Found after #71: nan gives a traceback or a silent wrong run. #87 reuses the check. Cheap: do it before rank 7. no
28 #110 Pull request template with the changelog and checks list (done, #113) S #97 The reminders live in this page and the dev notes only. Cheap: do it before rank 7. no
29 #109 Run allium check in CI and clear or accept its warnings (done, #114: CI runs scripts/check_specs.sh; the three warnings on layerforge.allium are accepted) S to M none The check is manual today, and its exit code is 1 on warnings. Investigate the CI install first. no
30 #108 Harden the mark snapping path (done, #159 and #169: one tolerance from the slice's config, and taken built once per contour) S #103 One tolerance source, a containment check in the adjuster. Fits with rank 11 and 12. no
31 #107 Stored marks never retire (acceptance for #63) (closed as completed 2026-10-10; measured on main at 9deb951: the sheared cylinder at the defaults has no slice without a mark and no failing pair, and at the issue's own tolerance=25 slices 8 to 12 have no mark and six pairs fail the check) (done, the PR of rank 19: the sheared-cylinder test lives in test_pair_local_marks_end_to_end.py; measured, 15 of 20 slices get a mark and the other 5 warn, short of the issue's own "every slice" line, a documented and accepted phase-1 gap) part of #63 #63 Not separate work: its sheared-cylinder test now lives in rank 19's PR; rank 21 still needs it. no
32 #120 The test suite reads a layerforge.toml in the working directory (done, #129) S none Found after #87. A plausible file fails 2 to 46 tests. Do it before session C adds more keys. no
33 #117 G-25: The scale and target conflict check tests truthiness (done, #130) S none 0, -1 and nan give different exit codes for the same pair. decided in session A3: the conflict comes before a bad value, and a bad config file comes before the conflict (#131)
34 #118 G-26: A config file is read without a message (done, #132: the file name only) S none A stray layerforge.toml changes a run silently. no
35 #119 G-27: A bad config file is reported after the STL prompt (done, #131: an eager --config callback) S none Investigate an eager --config first. no
36 #121 Harden settings.py: the second-stage error lookup (done, #133) S none Latent KeyError. Do it before #62 adds the first key without an option. no
37 #125 Defaults are written by hand in six places (done, #158: the help text follows Settings(), and one test compares the keys table, the spec block and the help text) S to M none Do it before #62 and #76 change the defaults. no
38 #122 Test scripts/check_specs.sh and find out what findings hold S #114 The CI gate is proven only by hand. no
39 #123 Verify the allium install in CI; make the bump routine (done, #127: upstream signs nothing, so the pin is checked against the tarball its release run built; the bump steps are in docs/development.md) S #114 The pinned hash was trust-on-first-use. There are no attestations. optional: tell upstream about the empty sha256 for x86_64 in its Homebrew formula
40 #124 The spec does not model most option checks S to M none Weed found spec faults in #115 only because it was run by hand. no
41 #135 G-28: Bad option values and the scale and target conflict are reported after the STL prompt (done, #146: --stl-file has no prompt=, cli asks after the checks) S to M none #119 fixed only the config file. Do it before session C, which adds --units and more options that need the same early checks. Investigate option callbacks against moving the prompt. no
42 #136 Tidy the eager --config callback: a double read and three small inconsistencies (done, #146: --config is not eager, the file is read once, --help wins over a bad file) S none Found by /code-review on #131 and by running the command. Low priority. decided 2026-09-25 by best practice: --help wins over everything, as clig.dev says ("you should be able to add -h to the end of anything and it should show help")
43 #137 getting_started.md lists error messages the command does not print (done, #148) S none Two lines are wrong and the two most common messages are missing. Can be folded into #95. no
44 #138 The PR template has no way to say a box does not apply (done, #148) S #110 One line. no
45 #141 Write a calibration sheet SVG so any machine can measure its own kerf, hole, fit and number sizes M #74, #83 Machine-neutral way to get the numbers of #96 and the dowel fit of #93. cut it on your machine
46 #142 Implement dowel holes (TR-15), after the design in #93 is accepted M #93, #84, #83, #74, #89, #141 Later. Do not start before #93 is accepted. no
47 #144 G-29: An output folder that is a file ends in a traceback after slicing (done, #146) S none Found by probing the class of #135. no
48 #145 G-30: A bad --mark-color ends in a traceback while the SVG is drawn (done, #182) S #83 #83 removes --mark-color and adds --cut-color and --engrave-color. Build the colour check there, for all three. no
49 #149 G-31: An output folder that cannot be written ends in a traceback, and an empty one points at the root (done, #160: a temporary file in the nearest folder that exists, no folder made early; a too-long name too) S none Same class as #144. Can go any time. no
50 #151 getting_started.md shows five files for the 20 mm cube, the command writes four (done, the PR of this row) S none Found in the second retrospective pass: the example was wrong since #70. no
51 #152 Documented examples are not checked against a run S to M none Same cause as #137 and #151. Fits session A4 or #95. no
52 #153 Tidy after #146: repeated check sequence, unused load_settings, a test that patches a private name (done, #161: two accepted and written down, one test changed) S none Do it with the next change to cli.py. no
53 #154 (Half done: #182 added plain_number, which fixes the exponent. The rest is open.) SVG size edge cases: no viewBox leaves 100%, a float can print in exponent notation, and a mark at x = 0 prints cx="-0.0" (measured, older than #170) S #74 (done) Investigation. Do it before #83 and #141, which draw on the same root. no
54 #157 Circle outline is a polygon just outside the drawn circle: decide what the footprint check uses (done, #167: keep the polygon, the calculator's radius is the largest reach, mark_reach) S #84, #85 (done) Found in the review of #85. The calculator's disc and the circle outline differ by 0.12%. Decide before #75 and #90 use the outline. decide 0.12% or exact
55 #162 An unregistered shape name makes the adjuster skip every check for the whole slice (done, #168: the error reaches the caller) S #85 (done) Found by probing the class of #85. Not reachable from the command. Can go any time. no
56 #164 The spec config block has no default for the mark angle, so the defaults test cannot see it (done, #170) S #125 (done) Found in the second retrospective of session C2. The test compares 8 settings with the table and 6 with the spec. Do it with #62. no
57 #165 Tidy after #85: a Slice without layer_height has no web, and tests call a private size method (done, #167 and #170) S #85 (done) Found in the same pass. Do it with the next change to slice.py (#62 or #75). no
58 #171 Two broad excepts hide errors: the calculator's candidate test and the label position (done, #179 and #184) S none Same class as #162. The label one goes with #75. no
59 #172 Remove the unused Model.origin and Slice.origin (done, #179) S none Unused since #170. Check #75 first. no
60 #173 Tidy after #62: the calculator's config fallback and where the small-size warning lives (checked 2026-10-10: item 1 is obsolete since #63, item 2 is open) S none Found by /code-review of #170. no
61 #175 The small-mark-size warning names an internal requirement ID (TR-6) (done, #177) S none Found by the second retrospective of session D. I wrote the message. no
62 #178 Four --help strings say "See docs/reference_mark_algorithm.md", which a pip install does not ship (done, the PR of this row: one link to the published page in the help epilog) S none Found while fixing #175. no
63 #180 Warn when the cut and engrave colours are the same (done, after the research in session E) S #83 (done) Found by /code-review of #182. TR-17 and #83 chose no warning; decide. decide
64 #181 SVGStyle() and SliceSVGDrawer default to a millimetre hairline and number height whatever the units S #83, #75 (done) Found by /code-review of #182. The command is not affected. no
65 #183 place_number: reuse the mark footprints per slice and bound the cost on fine outlines S #75 (done) Found by /code-review of #184. 0.45 s for one outline of 8001 vertices. no
66 #185 adjacent_pairs: invalid polygons and the cost on layers with many pieces (item 1 done, the PR of rank 19: an invalid contour is repaired with shapely.make_valid; one it cannot resolve into a polygon with area raises ValueError naming the layer and piece, instead of a raw GEOS exception; items 2 and 3, the O(n²) piece comparison and the min_overlap_area float-noise threshold, are still open) S #89 (done) Found by /code-review of #186. Item 1 done with #63; items 2-3 still wait on #92 or a profile. no
67 #188 Option lists in README and the docs are not checked against --help S none Found by the retrospective of session E: three places were stale after #184. Same cause as #137, #151, #152. no
68 #189 Warnings name a command-line option even when the value came from the file or the API (done for the two warnings: both name the key too; the Python API case stays as it is) S none Found by /code-review of #184; also true of the older --mark-min-distance warning. no
69 #190 Decide what the defaults do for a piece smaller than the number (a 10 mm cube warns in every slice) (decided: keep 5 mm, give the tallest height that fits in the warning; the 10 mm cube needs 4.83 with circle marks, and since #61 gives no warning at the defaults) S #75 (done) Measured in session E. Go with #96 and #141. decide
70 #191 Check the number's width factor (0.6) and baseline shift (0.35) against real fonts and laser software S none Found by the retrospective of session E. Both were checked by eye only. no
71 #198 The calculator's disc counts every listed shape, though only one shape is used since #61 (measured on #61's branch, before #63's rewrite: a 10 mm cube at layer height 5 gets marks in 0 of 2 slices with the default list and in 2 of 2 with triangle alone) (done, the PR of rank 19: the disc now uses only the chosen shape's own reach; measured under #63's pair-driven algorithm, a 6.003 mm square box 9 mm tall at layer height 3 goes from 0 of 3 slices marked, with the pre-fix disc sized to the circle's reach, to 3 of 3 for both the default list and triangle alone; the 10 mm cube and a 6.000 mm box still give 0 either way, for the unrelated reason that their half-width exactly equals min_distance) S to M #61 (done) Found while doing #61, measured on its branch. Breaking, done with #63. no
72 #199 choose_shape rebuilds every listed shape on each call (137 microseconds, once per new mark) (closed as not planned 2026-10-10: 1 to 2 calls per run on five models, measured) S #61 (done) Found by /code-review of #61. Only if a profile shows it. no
73 #201 BaseShape.symmetry_order defaults to 1, so a registered shape with no declared order reads as having a direction (choose_shape picks a lone hexagon first, and rotation_symmetry says it has no symmetry) (done, the PR of this row: register_shape refuses a class that does not set the order in its own body) S #90, #61 (done) Found by the retrospective of session F. Not reachable from the command; only a Python caller who registers a shape. no
74 #203 rotation_symmetry costs O(n³) on symmetric sets (a ring of 400 marks takes 10 s; 200 take 1.2 s; a random set of 400 takes 0.056 s) (done, #217: a pair of pieces has at most one shared mark, so the cubic cost is never met; the bound is in the docstring) S to M #90 (done) Found by the second retrospective of session F. Decide before #92 calls it per pair of layers. no
75 #204 The one-to-one match in rotation_symmetry survives the mutation check: test it or delete it (done, the PR of this row: a three-circle set at TOL that reaches the line, and a docstring note) S #90, #91 (done) Found by the second retrospective of session F. no
76 #205 The --help docs URL is a string in cli.py with no source of truth (mkdocs.yml has no site_url), and no test catches a moved page S #178 (done) Found by the second retrospective of session F. no
77 #210 TR10_SnapToStoredMark still describes the whole-stack registry #63 removed (done, the PR of this row: the rule is TR10_FreshPointClearOfMarksInPlay, matching the avoid_all loop of choose_mark_for_pair; the TR-10 row, reference_mark_algorithm.md and the architecture.md package row said the same stale thing and are fixed with it) S #63 (done) Found while doing #63: the spec rule is aspirational and unbuilt, flagged in the file header. Real code only checks tolerance within one pairing. decided: a fresh point is refused within the tolerance of a mark in play; nothing snaps
78 #211 An unrepairable slice contour ends in a raw traceback, not a controlled exit (done, the PR of this row: UnrepairableContourError, caught in _run, exit 1; measured on 8 damaged meshes, none reaches it, so the test patches the contour source) S #185 item 1 (done) Found while doing #63: adjacent_pairs's ValueError is not caught in cli.py, unlike the mesh-load failure path. decide whether it needs the same treatment
79 #212 docs/requirements.md's G-4 "one mark per polygon" half is stale after #63 S #63 (done) Found while doing #63: superseded by the per-pairing rule (FR-15); the "nothing checks alignment is unique" half is done with #92: the G-4 row is rewritten. no
80 #215 TR-9's "no mark in every layer" clause isn't enforced for non-drifting geometry M #63 (done) Found by the final whole-branch review of #63: a straight, undrifting shape (a 20 mm cube, 7 slices; a straight r30 h30 cylinder, 10 slices) gets one mark spanning every layer, since nothing forces retirement when the geometry never invalidates a mark. Needs a design decision (a forced periodic retirement policy) before it can be built. decided 2026-10-09: a soft cap, marks.max_layers (default 4, proposed); drafted in docs/superpowers/specs/2026-10-09-distinct-marks-and-mark-cap-design.md, not reviewed by the owner, not built
81 #213 Session H: TR-8's two-marks-differ clause (item 1 is drafted in docs/superpowers/specs/2026-10-09-distinct-marks-and-mark-cap-design.md, not built), and #185 items 2-3 M #63 (done) Filed as #63's own Non-goals section during its spec review: choose_shape has no used_on_piece argument, so two marks freshly chosen on the same middle piece can pick the same shape (TR-8). Also carries #185 items 2-3 (an STRtree for the all-pairs comparison, the min_overlap_area default) and a placement spread-bias question from #63's /code-review. no
82 #222 No way to turn off checks.allow_unaligned from the command line (--no-allow-unaligned) (done, #228) S #92 (done) Found by the review of #220. Minor. no
83 #223 Alignment messages name a piece by list index, which the SVG files do not show (done, #229: a point inside both pieces, in model coordinates, rounded to three decimals) S #92 (done) Found by the review of #220. Add the centroid or box. Minor. no
84 #224 TR-4: the minimum baseline between shared marks is not checked S #213, #215 Not built: a pair shares at most one mark. Found while closing #60. no
85 #225 CHANGELOG entry for #72 and #82 describes the whole-run mark registry that #63 removed (done, #227) S #63 (done) Found in the retrospective of the session of 2026-10-09. no
86 #230 Add shapes with a direction: two exist (triangle, arrow), and a piece that meets three or more holes needs more (TR-7, TR-8) M the TR-8 spec Found while designing #213 item 1. Measured: only two shapes have symmetry_order 1. By the rule, not run: three legs into one top need three. Rank it before the TR-8 PR if the stool and the hand must pass at the defaults. decide how many, and check on your laser that they can be told apart (#96, #141)
87 #231 Investigate: pieces of adjacent layers that do not overlap can carry the same slice number and a hole of the same shape (TR-2, TR-8) S the TR-8 spec Found by the design review of #213 item 1. Not measured: build two identical legs and two that differ by a few millimetres. decide the scope of the rule
88 #232 The unrepairable-contour message names a piece by list index, which the files do not show S #211 (done) Found while doing #223. Not reachable today (8 damaged meshes, none). Give the bounds of the piece. no
89 #233 CHANGELOG entries for #83, #62 and #108 name ReferenceMarkManager, which #63 removed S #225 (done) Found by the class probe of #225: three more entries of the same kind. Docs only. no

Ranks 27 to 31 were found while doing ranks 1 to 6, and ranks 32 to 40 while doing ranks 7 and 27 to 29. They are appended, so the numbers in the Depends columns stay valid. Ranks 27 to 29 and 32 to 36 are done (session A3 below). Rank 37 goes before #62 (session D). Ranks 41 to 44 were found in the retrospective of session A3 and are done (session A5, PRs #146 and #148). Ranks 47 to 49 were found by probing the class of #135 in the session of 2026-09-25; rank 47 is done. Ranks 50 to 53 were found by a second retrospective pass the same day; rank 50 is done. Ranks 54 and 55 were found in session C2 (below), and ranks 56 and 57 by its second retrospective pass. Ranks 10, 11, 37, 49 and 52 are done (session C2). Ranks 12, 30 and 54 to 57 are done (session D). Ranks 58 to 60 were found in session D and rank 61 by its second retrospective pass. Ranks 58, 59 and 61 are done (session E); rank 60 is open. Ranks 62 to 70 were found in session E (rank 62 is done, in session F). Ranks 71 to 73 were found in session F, and ranks 74 to 76 by its second retrospective pass. Ranks 38 to 40 are hygiene and can go any time (session A4). Ranks 19, 31, 66 (item 1) and 71 are done (session G, below); rank 66's items 2 and 3 are still open. Ranks 77 to 81 were found in session G and are open. Ranks 82 to 85 were found in the retrospective of the session of 2026-10-09 (ranks 82, 83 and 85 are done, in the second session of that date). Ranks 86 to 89 were found in that second session.

Issue #98 holds the same list as a checklist. Tick it as items merge, and keep the order in both places the same.

Suggested session bundles

Session Items Note
A #71, #78, #77, #82 + #72, #97 Done 2026-09-25 as PRs #100 to #104. #73 was decided: document the limit.
A2 #106, #110, #109 Done 2026-09-25 as PRs #112 to #114.
A3 #120, #117, #118, #119, #121 Done 2026-09-25 as PRs #129 to #133. #121 moved here from D because #119 reshaped load_settings.
A4 #122, #123 (done), #124, #152 Spec and CI hygiene. No product code. Run allium:weed with #124. #152 (check the docs examples) fits here. Can go between any two sessions.
A5 #135, #136, #137, #138 Done 2026-09-25 as PRs #146 (#135, #136 and #144 together, because removing the eager --config callback fixes both) and #148 (#137, #138).
B #87 Done 2026-09-25 as PR #115, narrow: the mechanism plus keys for today's settings. The example file for #96 is not added; docs/configuration.md has an example.
C #74, #84 Units first, then the shape contract. Done 2026-09-25: #74 as PR #147, #84 as PR #156.
D #157, #162, #108 items 1 and 3, #62 + #76, #164, #165 Done 2026-09-25 as PRs #167 to #170, one issue group each, oldest first. #75 moved to E: it needs three keys, an option, unit conversion and a drawer rewrite, and it overlaps TR-14 (#83).
C2 #84, #125, #85, #149, #153 Done 2026-09-25 as PRs #156, #158, #159, #160 and #161, in that order. Chosen because the alignment core had not started after 29 merged PRs (#100 to #155, counted with gh pr list) of input checks, config, CI, units and docs: #84 is the root that blocks #85, #90, #61, #141 and #142. #125 went before #85 because #85 adds a default.
E #83, #75, #89, #141 Done 2026-09-26 for #83, #75 and #89, as PRs #182, #184 and #186, with the tidy work that touches the same lines: #175 as #177, and #172 with the calculator half of #171 as #179. #141 was not started: it needs the design of #93 for its dowel ladder, and a run on the owner's machine.
F #178, #90, #91, #61 Done 2026-09-26 as PRs #195 (#178), #196 (#90), #197 (#91) and the PR of rank 18 (#61), one issue each, oldest first. #178 went first because it and #61 edit the same help line. rotation_symmetry and the oracle are API and tests only; nothing in the command calls the function until #92.
G #201, #204, #63, #198, #107, #185 item 1 Done 2026-09-27. #201 and #204 first (small, unrelated fixes), then #63 with #198, #107 and #185 item 1 together, through the architectural path (spec, plan, then code): see the Status paragraph above for the measured numbers.
H #92, #60, #79, #95 The check, the proof, tests, docs. #92 and #60 are done (the session of 2026-10-09).
I #225, #222, #223, then #213 item 1 and #215 Done 2026-10-09 to 10 for #225, #222 and #223 (PRs #227, #228, #229). #213 item 1 and #215 are one design (marks that look different, then a cap on layers per mark): the spec is drafted in docs/superpowers/specs/2026-10-09-distinct-marks-and-mark-cap-design.md and waits for the owner's review. Then the plan, then two PRs, TR-8 first. #230 decides how many models pass the strict rule at the defaults.

Rules for every session

Start: 1. Read the project memory index (MEMORY.md), then every linked file, including the gotchas. In session B I read four and repeated a zsh slip that the gotchas file lists. 2. git checkout main && git pull --ff-only, then confirm the tree is clean and that no other branches or worktrees are left. 3. Read the issue and its TR rows in Alignment requirements. Follow the acceptance list in the issue. 4. Use plan mode before coding. Use the brainstorming, test-driven-development and verification-before-completion skills by name.

While working: - When a fix names one member of a class of inputs, probe the rest of the class before you close the issue. #119 moved a bad config file before the --stl-file prompt and left bad option values and the option conflict after it (G-28, #135). Test each member of the class, as with nan and inf in #106. - Before a comment, a spec sentence or a doc line says "every" or "before any", run the case. "Before every other check" was false in #131 (an unknown option is refused first), and a code comment in #133 claimed a case that no test reached. - When a check moves or a step is split, compare what the command prints and on which stream, not only the exit code. In #146 the Using settings from <file> line stopped printing on a run that failed a later check. My tests covered the success run, and /code-review found the loss. Now a test covers it. - When a change makes a call raise inside a loop, read the except around the loop. #85 made the adjuster build each footprint with ShapeFactory.get_shape, which raises for an unknown name, and a except ValueError in Slice.adjust_marks (from #53) swallowed it and skipped every check (#162). - When you break code to prove a test, restore it from a copy, not with git checkout -- <file> on a file that holds uncommitted work. In session C2 that removed one of my own edits (I saw it in git diff --stat and redid it). - When a fix covers an option that names a resource (a file, a folder), probe the whole class: missing, a directory where a file is wanted, a file where a directory is wanted, inside a file, a dangling symlink, not writable, empty text. #144 covered a file and a path inside one. A dangling symlink came from the review, and #149 holds the unwritable and empty cases. - When you edit a docs page, run every command and example on that page and copy the numbers from the run. #148 edited the error list of getting_started.md and left a cube example above it that had been wrong since #70 (#151). - After a PR text says "N new tests", "N notes" or "N callers", count them with the tool (--collect-only, git show --stat, grep -c). Four PR texts in three sessions held a wrong count (#115, #146 twice, #150). - One branch and one PR per issue. Commit message ends with Fixes #N, or Refs #N when part of the issue stays open. - Write the failing test first, and watch it fail for the right reason. A property test that passes on the first run proves nothing: shrink the input space until it fails on the bug. Run the checks without pipes: uv run ruff format && uv run ruff check && uv run pyright && uv run pytest -q. - In the same PR: update the FR row in docs/requirements.md, remove the fixed Known gaps row, and update specs/layerforge.allium. Add or update the matching target row if the target changed. Run ./scripts/check_specs.sh and uv run mkdocs build --strict. - Run allium:weed after spec edits, and /code-review before opening the PR. - Run allium:weed for a prose-only spec edit too. It was skipped for #149 because the edit was a guarantee sentence, and the late pass found the FR-5 message wording wrong (<name> is the whole folder text, and other lookup errors use it). - A retrospective that says "no wrong claim found" must list what it ran. The first pass on session C2 re-read five PR texts and missed "the disc holds every outline", which a two-line measure (max(hypot) over exterior.coords, 5.00603 for the circle) disproves. Test every "every", "any" and "never" in a PR text or a docstring with one number. - Before gh pr merge, re-read the PR text against the issues it names and against the run. Session D's #170 text said "the rest of #165 stays open" when item 3 had merged in #167, and said a follow-up was "filed" before I filed it. I edited, marked ready and merged in one command, so the re-read came after. Look at the issue itself for every Refs. Write "filed" only after the issue number exists. - A PR that bundles issues says why in its text. #170 held four issues in 34 files, against the one-PR-per-issue rule, and the text was silent. Split it, or give the reason (here: one change to the defaults that would leave main half-migrated). - A review note is a lead. Read the code behind each one and write what you saw, as for any agent report. The reviewer of #170 did not run the tests or read callers outside src. I judged four notes "documented" and one "real" from the note text and confirmed the real one (config or ReferenceMarkConfig() before, (config or layer.config).resolved(...) after) only in the retrospective. - Run the case the docs promise before the merge, not in the retrospective. docs/configuration.md says --kerf 0 suits a CNC router. I ran it only after #170 merged (it works: size 3 at layer height 3, size 0.2 at layer height 0.2). - Read your new user-facing message as the person who runs the command. grep -rnE "TR-|FR-|G-[0-9]" over the strings in src. The warning of #170 names TR-6 (#175). - In zsh, define lf() { uv run --project <repo> layerforge "$@"; } before the first probe. The L=...; $L slip happened a fifth time in session D, after it was in the gotchas file. - After a merge, check the run on main (gh run list --branch main) and the docs workflow in it. It runs on push only, so the PR checks do not cover it. - Ask before pushing, merging or editing issues unless the owner has said to. The owner allowed push, PR and squash-merge on green for the session of 2026-09-25 only. - Write the PR text after the checks and CI have run, and copy numbers from their output. In session A2 and B three PR texts had a wrong or stale claim (a template that did not show, a CI job "not run yet" that had passed by merge, a test count off by one). Each was caught by re-reading. - A plan step that says "check that X shows" needs a way to check it. Test the way first. - When a change adds or retypes a field, search the spec for the concept (grep size) and read every invariant that mentions it. allium check does not catch a wrong type in an invariant. - State in the docs, the changelog and the PR text only what was measured. "Unchanged at the defaults" was true for one model and false in general (session A, #103). - allium check exits 1 on warnings, even on main. scripts/check_specs.sh fails only on an error diagnostic or a finding, and CI runs it. The accepted warnings are listed in Development (#109). - Merge with squash, oldest PR first. After gh pr merge, mergeable reads UNKNOWN for about 15 s: run git fetch and look again in a separate command. Two PRs that each delete one of two adjacent Known-gaps rows conflict. Drop both rows and continue the rebase.

  • After a feature PR, grep the option, key and old word (grep -rnE "number-height|Slice N|Planned") over README.md, docs/*.md, specs/*.allium and src, and read every hit. #184 updated FR-6, FR-25 and TR-11 and left three other places stale; only the retrospective found them (#188).
  • A retrospective that says "found no wrong claim" must say what it compared with what. Mine compared PR texts with my memory of the diffs, and missed three stale docs. Compare the text with git show --stat and a grep of the names it mentions.
  • Before choosing the format of a computed number in a message, run it on a model whose coordinates are not 0. #223 reached review with {:g}: the cubes I ran print 0, and a cone printed y 1.83697e-16. Round, and make negative zero read 0.
  • Tick #98 and write the backlog paragraph in the PR that does the work, not in the retrospective. In the second session of 2026-10-09 the three PRs left both to the end, the same slip as session F.
  • A review that returns nothing after a few seconds is not a review. /code-review low on #228 returned (none) after 4 s and one tool call. Read the diff yourself and say so in the PR text.
  • Edit scripts: assert that old occurs once, and never write if False inside one. I wrote a ... if False else ... branch three times in session E (once in each of three docs scripts), after the gotchas file and the last retrospective both said not to. A str.replace on a short line also hits the same text at another indent (a repeated keyword argument in cli.py).
  • Test data for a threshold uses exact values. box(9.9, 0, 20, 10) overlaps by 0.99999, not 1.
  • A test that passes on its first run is a guard, not proof. Say so in the PR text, and mutate the code to see which tests can fail. In session E the clearance check of place_number and the polygon filter of adjacent_pairs each survived the first tests, until I added a case for it.

Finish: - Merge on green CI, delete the branch (local and remote), confirm a clean tree. - Update the memory file project-layerforge-backlog.md. Add a memory only for facts a future session cannot get from the repo.

Known traps

See memory layerforge-tooling-gotchas and Development (Working Notes): trimesh section argument order, to_2D re-centring, no triangulation engine, BSD sed -i, qlmanage hangs (use rsvg-convert), | tail hiding failed checks, and zsh not splitting an unquoted $var (three slips in one day: use one explicit call per case). In shell heredocs, do not put backslashes before backticks: build issue and PR bodies in Python or from a file.

Status

Sessions A, A2 and B are done (2026-09-25). A: ranks 1 to 5 merged as #100 to #104, and rank 6 (#73) was decided as a documented limit (#105). A2: #106, #110 and #109 merged as #112 to #114. B: #87 merged as #115, with the mechanism and the keys that exist today (layer_height, marks.size, marks.tolerance, marks.min_distance, marks.shapes, marks.angle). Retrospective 2026-09-25: nine new issues, #117 to #125 (ranks 32 to 40). Session A3 is done (2026-09-25): #120, #117, #119, #118 and #121 merged as #129 to #133. The retrospective found four more issues, #135 to #138 (ranks 41 to 44). The next work is #135 (session A5), then session C (#74, #84), or A4 (#122, #124) first. Each later issue adds its own config keys and its own row in TR-16; the default marks.tolerance of 0.1 x mark size comes with #62. Session A5 and #74 are done (2026-09-25): #135, #136 and #144 merged as #146, #74 as #147, #137 and #138 as #148. The retrospective found #145 (G-30) and #149 (G-31). A second pass found #151 to #154. The next work is #84 (session C), then session D with #125 first.

Session C2 is done (2026-09-25): #84, #125, #85, #149 and #153 merged as #156, #158, #159, #160 and #161. #84 gave each shape outline() and symmetry_order, so the arrow is now a closed polygon. #85 made the adjuster and the calculator check the whole hole and the web (marks.min_web_ratio, config file only, default 0.5), and closed item 2 of #108. The retrospective found #157 (the circle outline) and #162 (an unregistered shape name skips every check), ranks 54 and 55, and I commented on #83 (the arrow's default colour equals the outline's), #124 (a proposed cap on its acceptance list) and #152 (another drifted example). Session D followed (below). Session A4 (#122, #124, #152) can go between any two sessions. A second retrospective pass found #164 and #165 (ranks 56 and 57, both go with #62), and one wrong claim in the text of #159, which it corrected (the calculator's disc does not hold the circle's outline() polygon; #157). It also ran the allium:weed check that was skipped for #149: no code bug, one wording gap in FR-5, fixed here.

Session D is done (2026-09-25): #157, #162, #108 (items 1 and 3), #62, #76, #164 and #165 merged as #167 to #170. The mark size now follows the sheet and the kerf, not the place: a 10 mm cube at the defaults goes from 0 marks to 4 in 4 slices (measured on main before and on the branch after). layer_height and the kerf default in mm and follow --units. Slice needs layer_height. The retrospective found #171 to #173 (ranks 58 to 60) and commented on #63 (the calculator's radius is one number for every mark). The next work is session E (#83, #75, #89, #141) or A4 (#122, #124, #152).

A second retrospective pass on session D (2026-09-26) ran the repo-wide grep for the old rule (no stale text), and the probes --kerf 0, --units cm, --mark-min-distance 0, a 100 mm sheet and 6.003 and 6.004 mm cubes (each matches the docs; 6.003 gets no mark in 3 slices and 6.004 gets one in each). It found #175 (rank 61), a negative zero in the SVG that predates #170 (comment on #154), the spec side of #172 (comment), and three wrong or late statements in the text of #170, which I corrected. Main was green after #174 (tests and docs).

Session E is done (2026-09-26) for #83, #75 and #89, merged as #182, #184 and #186, after #177 and #179. The SVG is laser output (#83, #145): outlines and holes are 0.01 mm red hairlines with a class, the number is black, --mark-color is gone, and a bad colour exits 2 before the prompt. The number is the slice index, 5 mm tall, placed by an exact sweep where its box is clear of every cut (#75, #171); measured at the defaults, a 10 mm cube warns in all 4 slices and 20 mm and 40 mm cubes do not. adjacent_pairs computes the overlaps of adjacent pieces (#89) and nothing calls it yet. #141 was not started. The retrospective found #178 (already open, now ranked), #180, #181, #183 and #185 (ranks 62 to 66). It ran the documented cube example again: four files, and the SVG matches the sketch in getting_started.md. My first pass said it had re-read the merged PR texts and found no wrong claim, but I had read only their text, not the docs they touch. A second pass grepped the option names across README, docs and specs and found three stale places after #184 (configuration.md listed --number-height as planned, the settings table of alignment_requirements.md said New, and the README had no --number-height). They are fixed in the second retrospective PR, and #188 asks for a check. The next work is session F (#90, #91, #61: the symmetry test, its oracle, and shape choice), or A4 (#122, #124, #152).

After the retrospective the owner asked for the best practice on #180 and #190 and to proceed. #180 (a warning when the cut and engrave colours are equal) merged as #193, and #189 and #190 as the next PR, which adds the tallest height that fits to the number warning and names the config keys. Measured: the 10 mm cube at the defaults needs a number of 4.83 mm, 0.17 mm below the default, and warns in all 4 slices until then. The default stays 5 mm. The if False slip in my edit scripts happened a fourth time on this day, in the spec edit of #193.

Session F is done (2026-09-26 to 27) for #178, #90, #91 and #61, merged as #195, #196, #197 and #200. --help now names the docs page by URL and no repo path (#178). rotation_symmetry finds the turns that map a set of marks onto itself, and a brute-force oracle with a hypothesis property guards it (#90, #91); nothing in the command calls it yet (#92). A new mark takes the shape with the least symmetry, so the defaults draw a triangle for every mark (#61, Breaking). Measured, the same command on main and on the branch: the 20 mm cube's slice 0 goes from a circle to a triangle, and the 10 mm box's four number warnings go to none. The retrospective found #198, #199 and #201 (ranks 71 to 73), and it ran every command on the edited pages again: the README command, the example script, and the example config in configuration.md all exit 0 and draw a triangle. It found one stale line by grep only, in configuration.md (already fixed in #200), and added that case to #152 and #188. Comments on #63 and #92 carry what session G needs: the two-marks-on-a-piece clause of TR-8, a warning when no listed shape has a direction (--available-shapes circle prints nothing today), and rotation_symmetry cost (100 marks 0.15 s, 200 marks 1.2 s).

A second retrospective pass on session F (2026-09-27, owner asked) found what the first missed. The Session F paragraph and the #98 ticks were not in the repo: a summary said they were, and no grep had checked (added in #202). The first report said the PR texts of #195 to #197 were checked before merge only; this pass compared their counts with gh pr view (files 4, 9 and 2) and --collect-only (6, 53 and 32 tests), and found them right. It measured rotation_symmetry on a ring and on a random set: 100 marks 0.155 s and 0.004 s, 200 marks 1.218 s and 0.014 s, 400 marks 9.958 s and 0.056 s (#203). It found that the one surviving mutant is not reached by any test (#204), and that nothing ties the --help URL to the docs (#205). My PR texts for #200 and #196 ran to about a page each; the owner's style asks for the answer first and no restating, so later PR texts keep the measured table and cut the narrative.

Session G started 2026-09-26 (owner asked to choose a subset): #201 and #204 first, then #63 with #198, #107 and the first part of #185, through the architectural path (spec, plan, then code). #201 is done: register_shape refuses a shape class that does not set symmetry_order itself. #204 is done: a three-circle set close enough for _maps_onto_itself to need its one-to-one match now guards the line; the docstring says why. #63 is done (2026-09-27): plan_marks and ReferenceMarkCalculator.choose_mark_for_pair choose and retire marks per pair of adjacent layers, replacing the whole-run ReferenceMarkManager and ReferenceMarkService (both removed) and Slice.process_reference_marks (G-7, TR-9, Breaking). #107's sheared-cylinder acceptance test now lives in test_pair_local_marks_end_to_end.py: measured on radius 20, height 60, shear 0.5 per z, layer height 3, tolerance=25, min_distance=10, 15 of its 20 slices get a mark and the other 5 (8 to 12) warn instead of staying silently unmarked, a documented and accepted gap short of the issue's own "every slice" line, because their shrunk region lies entirely within tolerance of a mark retired at an earlier boundary in the same run of unmarked boundaries. #198 is done: the calculator picks a shape before a point, so its disc uses only that shape's own reach; measured on a 6.003 mm square box 9 mm tall at layer height 3, the default shape list and triangle alone both go from 0 of 3 slices marked, with the pre-fix disc sized to the circle's larger reach (reproduced by monkeypatch), to 3 of 3. adjacent_pairs (#89) is used by the command for the first time, through plan_marks, and item 1 of #185 is done with it: an invalid contour is repaired with shapely.make_valid, and one that cannot be repaired raises ValueError naming the layer and piece, instead of a raw GEOS exception; items 2 and 3 (the O(n²) piece comparison, the min_overlap_area float-noise threshold) are still open. The retrospective found #210, #211 and #212 (ranks 77 to 79): TR10_SnapToStoredMark's spec rule still names the whole-stack registry the redesign deliberately removed; an unrepairable slice contour ends in a raw traceback, not a controlled exit; and G-4's "one mark per polygon" half is now stale (superseded by the per-pairing rule), though its "nothing checks alignment is unique" half is still open (#92). The final whole-branch review (Opus, the plan's own required last gate) found ten more findings before merge: a real, measured bug (extending a shared avoid-list with its own current contents on every failed pairing into a merge doubled it each time — 16.7M entries at 24 merging pieces, 914 MB peak — fixed by narrowing the extend source), and that TR-9's "no mark is a hole in every layer" clause is not enforced for geometry that never invalidates a mark (a straight, undrifting shape gets one mark spanning every layer): the owner chose, by AskUserQuestion, to correct every "built" claim to name this honestly rather than delay the merge for a new forced-retirement design, filed as #215 (rank 80). #213 (rank 81) was filed earlier, during the branch's own /code-review task, for TR-8's two-marks-differ clause and the rest of #185. PR #214 squash-merged as a21c37d4; CI green on main after, including the docs workflow. The next work is #92 (pre-write check and --allow-unaligned), session H (#213), or A4 (#122, #124, #152).

The session of 2026-10-09 (owner asked to choose a subset; it is not the "Session H" of #213, which is deferred): #203, #210 and #211 are done and merged (#217 to #219). #203: a pair of pieces has at most one shared mark, so rotation_symmetry never meets the cubic cost; the bound is now in its docstring. #210: the TR-10 rule names the check choose_mark_for_pair makes. #211: UnrepairableContourError ends in a message and exit 1. #92 is the architectural item of the session (spec, plan, then code): check_alignment fails a pair of overlapping pieces that shares no mark or whose shared marks a turn maps onto themselves, before any file is written. The owner decided that a number that does not fit stays a warning, so only TR-2 is an error. Measured with the new command at the defaults: the 20 mm cube passes, the cone (r 20, h 30) fails on the pairs 7-8 and 8-9 and writes ten files with --allow-unaligned, and a 10 mm cube at layer height 5 fails on the pair 0-1. #212 is done with #92. #60 is done in #221: test_alignment_end_to_end reads the written files of four models and finds a shared triangle mark in every adjacent pair; two mutations make it fail. The retrospective filed #222 to #225 and comments on #185 and #152, and left #107 for the owner (below).

The second session of 2026-10-09, continued on 2026-10-10 (owner asked to examine the open issues critically and choose a subset): #225, #222 and #223 are done and merged as #227, #228 and #229. The run on main was green after each, including the docs workflow. A read-only probe set the priority. At the defaults and layer height 3, a 20 mm cube (7 slices), a cylinder r30 h30 (10), an icosphere r20 (14) and a tube r20/r10 h30 (10) each get one mark that is a hole in every layer (#215). The sheared cylinder of #107 (slice 11) and two legs that merge into one top each leave a piece with two triangles, and check_alignment passes both (#213 item 1). The owner chose a strict TR-8 (a model that needs more shapes with a direction than exist fails the check, and --allow-unaligned still writes it) and a soft cap for TR-9 (marks.max_layers, default 4 proposed). The design is drafted in docs/superpowers/specs/2026-10-09-distinct-marks-and-mark-cap-design.md, after a design review by a separate model that found that the neighbours of both pieces, not the two pieces, must set the excluded shapes, and that a reused mark needs the same test as a fresh one. It waits for the owner's review: no plan and no code yet. #107 is closed as completed: at the defaults the sheared cylinder has no slice without a mark and no failing pair, and at the issue's own tolerance=25 slices 8 to 12 have none and six pairs fail, which the closing comment states. #199 is closed as not planned (choose_shape is called 1 to 2 times per run on five models). The retrospective filed #230 to #233 (ranks 86 to 89), commented on #79 and #173, corrected the status of TR-8 (it said the second mark "comes with #63") and TR-9, added the Known-gaps row G-32 and the spec gap lines, and ticked #98 (items 63, 68 and 69 had been closed since 2026-09-26 and never ticked). Next: the owner reviews the spec, then writing-plans, then the TR-8 PR, then the cap; #230 can go first. Or session A4 (#122, #124, #152, #188).

Open decisions and owner inputs

Decision Blocks Needed by
Laser and material numbers (#96): kerf, smallest clean hole, number height, hairline behavior. Cut the sheet of #141 on your machine. Final defaults in #87, #62, #75 Before the proposed defaults are final
Colours and stroke conventions of the laser software (#83): built with the defaults red cut, black engrave and a 0.01 mm hairline (#182). The probe of #141 shows what your software does, and #180 asks about equal colours. Final defaults Before the proposed defaults are final
Review the draft design for #213 item 1 and #215 (docs/superpowers/specs/2026-10-09-distinct-marks-and-mark-cap-design.md): the rule, the cap and its default of 4, the strict failure for models that need more shapes The plan and both PRs Next session
How many shapes with a direction to add (#230), and whether they can be told apart on your laser (#96, #141) #230 Before the TR-8 PR, if the stool and the hand must pass at the defaults
Dowel hole design (#93): review the proposal in TR-15, mainly position and count #93, #142 Later
Config key names in TR-16 Later keys Each issue that adds a key. The six built in #87 follow TR-16 as written, so the owner may still rename them.

What is settled

Overlapping shells in one mesh are a documented limit (#73, decided 2026-09-25): the even-odd rule stays, users merge bodies before export, and a later opt-in manifold3d union is possible. Marks are holes. Every layer aligns to the one below and the one above in exactly one way. The number fixes the flip. Mark shape fixes rotation. Units default to mm. Failing to align is an error before writing, and --allow-unaligned overrides it. Angle 0 points along +x, counter-clockwise. Mark size is the diameter of the smallest circle that holds the outline. Marks are chosen per pair of adjacent layers. Every laser, material and judgment number is a setting. Dowel holes are a later feature.