Commit graph

791 commits

Author SHA1 Message Date
nigig-ci
de698b1a64 ci(map): make the workflow runnable, and cover the code it now guards
Some checks failed
nigig-map / test (push) Failing after 2m37s
repo hygiene / hygiene (push) Successful in 4s
nigig-map.yml has never executed a single step. It used
actions/setup-rust@v1, which does not exist on data.forgejo.org, so
every run died in "Set up job" with "repository not found" and
cancelled all seven steps -- the same class of defect as
android-actions/setup-android in sms.yml. Replaced with the inline
rustup install already used by pay-domain.yml.

That action also requested `toolchain: stable`, contradicting the
1.97.1 pin in rust-toolchain.toml. The replacement reads the channel
out of rust-toolchain.toml, so CI and developers use one compiler.

Added the native GL/wayland dependencies; Makepad does not build
without them.

Gates, scoped to what is honestly true today now that the crate
compiles:

  - Build is a hard gate. This is the regression that matters: until
    the previous commit the crate did not compile at all.

  - Unit tests are a RATCHET at 9, not a hard gate. 535 unit tests
    existed and had never run; 526 pass and 9 fail on real logic
    (4 mvt_parser, 1 overpass_parser, 4 sprite classification). Failing
    the build on those would mean a permanently red job that everyone
    learns to ignore. The ratchet fails the moment a tenth appears.

  - `cargo test` with no filter is NOT used: two of the four test
    targets and the criterion bench do not compile (tests/ui.rs imports
    makepad_widgets::makepad_test; tests/makepad_visual_tests.rs and
    benches/tile_decode_bench.rs import pub(crate) modules, and
    criterion is not a declared dev-dependency). Separate defects.

  - fmt and clippy report without gating, matching doc-engine.yml and
    sms.yml. rustfmt could not parse view.rs while the crate was broken
    so it skipped all of src/; there are now 392 visible pre-existing
    diffs and 132 clippy warnings. A step that always fails is worse
    than no step.

Also added four unit tests for center_lat() and meters_per_pixel().
Both were introduced in the compile fix and had zero coverage: I
verified that by regressing center_lat() by +1.0 degree and watching
the ratchet stay green at 9. It now fails at 12. The tests round-trip
the projection across eight latitudes, pin the equator to zero, check
hemisphere sign, and assert the ground scale ratio between 0 and 60
degrees is cos(60) = 0.5 -- the position puck's accuracy circle is
sized from that, so an inversion would be wrong by 2x at Nordic
latitudes.

Ratchet negative-tested both ways: perturbing lon_lat_to_normalized
takes it 9 -> 12 and fails; at HEAD it reports 530 passed, 9 failed
and passes.
2026-08-04 05:06:48 +00:00
nigig-ci
b549b069e9 fix(map): repair the crate so it compiles, five errors from one bad merge
nigig-map has not compiled on main. `cargo build` failed with 12 errors,
which blocked nigig-map.yml and, transitively, pageflipnav. All five
distinct causes trace to 0718743, whose message claims "view.rs (widget
integration, 15 lines added)" while the diff is 34 insertions and 166
deletions: a block of struct fields was pasted over the tail of
`impl NigigMapView`, replacing two methods.

1. Struct fields inside the impl block. Lines 1060-1070 were a verbatim
   duplicate of the fields already at 292-302, sitting after a method
   body, so the parser hit `style_json_light:` where it wanted `!` or
   `::`. Removed the duplicates.

   This one error also silently disabled rustfmt for the whole crate:
   it cannot resolve `mod view` if view.rs does not parse, so it skipped
   src/ entirely and only ever checked tests/. 392 formatting diffs in
   src/ were invisible for that reason. They are pre-existing and left
   for a separate commit.

2. `overlay_state: super::overlay::MapOverlayState`. The Script and
   Widget derives parse fields with micro_proc_macro's eat_type(), which
   reads one ident plus optional generics and has no case for `::`. Both
   derives aborted with "Unexpected field form" pointing at the derive
   attribute, not the field. Imported the type and used a bare ident, as
   every other field in the struct does. Comment added, because the
   error names the wrong line.

3. `source_mode_label()` and `theme_label()` were the two methods the
   pasted fields overwrote. Both are still called from update_status().
   Restored verbatim from 0718743^.

4. `Vec4f::new` does not exist in this makepad rev. It was in
   `hex_to_vec4`, a helper with zero callers that duplicated
   `vec4_from_hex` ten lines above it. Deleted rather than repaired.

5. `meters_per_pixel()` read `self.center_lat`, but ViewportState stores
   only `center_norm`. Added `geometry::normalized_y_to_lat()` (inverse
   of the y half of lon_lat_to_normalized, same formula as
   tile_corner_lon_lat_f64) and a `center_lat()` accessor.

Also fixed an f32/f64 mismatch: map_offset() returns Vec2f, OverlayCamera
wants Vec2d.

Verified: `cargo build --manifest-path crates/apps/map/Cargo.toml`
succeeds. `cargo test --lib` now runs 535 unit tests that had never
executed -- 526 pass, 9 fail on real logic (4 mvt_parser, 1
overpass_parser, 4 sprite classification). Those failures and the
still-broken tests/ and benches/ targets are pre-existing and out of
scope here; this commit is the compile fix.

Negative-tested: restoring the `super::` path on overlay_state brings
back 6 errors.
2026-08-04 05:02:24 +00:00
1655ee1348 perf(spreadsheet-ui): reuse cached render styles
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
2026-08-02 18:21:38 +00:00
arena-agent
c854624838 docs(doc): device verification runbook for the hardware-only batch
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
One roadmap box legitimately cannot execute in the sandbox — ScrollYView
parent handoff verification on Android/iOS — and with it the class of
platform-owned behaviors deferred across the touch milestones (IME
opening, native clipboard-menu placement, touch arbitration on real
event streams, the GPU-bound painting/clipping sweep scoped here by the
legacy perf-box retirement). This writes DEVICE_VERIFICATION.md so a
hardware session becomes checklist execution:

- prereqs: cargo_makepad build/run commands for Android (adb) and iOS
  (run-device with provisioning), per the fork's tool help;
- nine sections covering interaction mode (View/Edit), IME input
  including autocorrect commits into cells, long-press selection with
  handles and the clipboard menu, table gestures (touch-only cell-range
  spanning, merge/split), the scroll-handoff box on BOTH workspaces
  (crdt_body and the legacy body_scroll), system-clipboard round trips
  of raw vs RFC-4180-quoted tabular payloads, multi-line cell rendering,
  the visual painting/clipping sweep with the layout-cache perf smoke
  check, and persistence;
- every row names the code mechanism under test (10 px / 24-frame
  arbitration, show_text_ime + the NextFrame reassert,
  show_clipboard_actions keyboard_shift passthrough, the start/extend
  cell-range path, quoting round trips, grown-row layout) with expected
  outcomes and explicit fail criteria — including which failures must
  be filed rather than waved through;
- a sign-off table that gates closing the roadmap box on both editor
  columns passing.

Documentation only; no code changes. The roadmap box gains a pointer to
the runbook for the hardware session.
2026-08-02 14:41:14 +00:00
98ee382791 perf(spreadsheet-ui): reuse cached display text
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
2026-08-02 14:35:04 +00:00
arena-agent
2055b6dfb4 perf(doc): cache the projection layout by document state; retire legacy perf boxes
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
The legacy roadmap carried four open boxes whose foundations had
landed long ago: incremental page/block reflow execution, draw-time
fragment-payload reuse, command-to-block-revision wiring, and the
renderer draw-pass integration test. It also carried an unmeasured
cost on the ACTIVE path: CrdtDocEditor recomputed the whole
ProjectionLayoutTree in every event handler (~20 sites) and on
every draw — several full O(blocks + glyphs) passes per keystroke.

Decision per box (DocWorkspace/DocEditor is the fallback path; the
CRDT-native editor ships):

- Command->revision wiring: retired. Change detection keys on the
  engine's op version-vector sum, bumped exactly once per mutating
  op (edit, undo, redo, peer import) — no per-command revision
  plumbing needed on the active path.
- Incremental reflow execution: retired for the legacy pipeline;
  answered on the CRDT path by a document-keyed cache in
  CrdtDocEditor::layout_tree — an unchanged document serves an Rc
  clone of the previous tree for every consumer, and the first
  consumer after any op recomputes once. Whole-tree granularity by
  design: per-block re-layout buys nothing until a profile asks.
- Draw-time fragment reuse: retired for the legacy renderer; the
  CRDT draw walk reuses the same cached tree — the glyph/rect
  payloads are the cache, not a second draw-only structure.
- Renderer draw-pass integration test: resolved by scoping. All
  non-GPU draw logic (geometry, rects, hit tests, event flows) is
  covered by the real-Cx runtime harness with Area::Rect stubs;
  painting/clipping visual verification stays GPU/Studio-bound and
  lands with the device-verification batch.

set_engine drops the cache slot outright so a swapped engine can
never inherit another document's tree under a colliding key; the
RefCell slot never escapes a call (several consumers hold &self).
Tests pin pointer-identity reuse, edit/undo invalidation with fresh
geometry, and no stale-tree inheritance across engine replacement.
README roadmap boxes annotated and the decision section documents
the rationale and residuals.
2026-08-02 14:29:22 +00:00
20a5e53dc9 perf(spreadsheet-ui): clear render cache on full invalidation
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
2026-08-02 14:28:07 +00:00
4bfd83897a perf(spreadsheet-ui): reuse cached visible text widths
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
2026-08-02 14:23:35 +00:00
80b182e959 feat(spreadsheet-ui): add render cache get-or-build path
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
2026-08-02 14:20:05 +00:00
65c643f047 perf(spreadsheet-ui): populate retained render cache
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
2026-08-02 14:14:31 +00:00
nigig-ci
ea7788673d docs(ci): record the repo-hygiene scheduling bug and the current board
All checks were successful
repo hygiene / hygiene (push) Successful in 4s
Adds the fourth defect the first real runs exposed -- the mapping form
of `on:` not being scheduled on this instance -- and a table of the
latest result for all 17 jobs, so "is CI green" has an answer that is
not someone's memory.

14 pass. The two failures, cad-module formatting and nigig-map, are
pre-existing source problems rather than CI plumbing.
2026-08-02 10:38:06 +00:00
arena-agent
7d1a7316d2 feat(doc): render multi-line cell text on grown rows
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Successful in 2m39s
nigig-build (CAD) / cad-module (push) Failing after 8s
nigig-build (CAD) / full-crate-check (push) Successful in 4m46s
Cell values holding newlines (legacy strings, or fresh ones the
RFC-4180 quoting round-trip now produces) rendered collapsed inline;
the text round-tripped but every display line squeezed onto one
band. One shared line model now threads layout, renderer, caret,
highlight, hit test, and the keyboard surface:

- layout_projected_table grows a row by one 18px text line height
  per extra display line of its tallest visible cell over the 28px
  baseline; the table rect and the block flow below follow. Column
  widths stay fixed and single-line tables lay out byte-identical
  (control assertions pin both). Merge composition: a covered
  cell's hidden text never inflates its row, and a vertical merge
  anchor sums the grown heights of the rows it spans.
- The renderer draws styled runs segment by segment: an embedded
  newline in a run resets x to the inset and advances one line,
  keeping the whole text block vertically centered so single-line
  cells draw exactly where they did.
- table_cell_caret, cell_text_span_rects (one band per covered
  display line, replacing the single-rect helper), and the
  point-based cell_char_offset_at (y picks the band, x midpoint-
  splits within it) all resolve through one cell_text_line_col /
  cell_text_offset_at pair whose round-trip is unit-tested at every
  boundary, including empty lines and the newline's own offset.
- ArrowUp/ArrowDown, previously dead in cell mode, step between
  display lines keeping the visual column (clamped per line), Shift
  extending the in-cell selection; they stay inert at the first and
  last line and on single-line cells, so no implicit row exit and
  no half-moved cell ranges.

Defect fixed in-phase: an in-cell character span covering a newline
copied as a raw slice, so a paste re-distributed it across cells.
The in-cell copy branch now quotes through the same
quote_tabular_field as every other tabular payload; the
Shift+ArrowDown runtime test pins the quoted payload end to end.

Tests: line-math boundaries, row growth with block flow and merge
composition, multi-line caret rects, per-line selection bands,
point hit-testing clamps, vertical-arrow step/inertness/collapse,
a real tap parking on the tapped display line, and the quoted span
copy via copyable_selection_text and the TextCopy hit.
2026-08-02 10:36:28 +00:00
Arena Agent
e4fbd71d78 fix(cad): finish 1.9 and 4.6, found by auditing the whole plan
Some checks failed
nigig-build (CAD) / supply-chain (push) Successful in 2m39s
nigig-build (CAD) / cad-module (push) Failing after 7s
nigig-build (CAD) / full-crate-check (push) Successful in 4m51s
repo hygiene / hygiene (push) Successful in 4s
Asked whether every phase was complete, I checked each row against the
code instead of against my own record. Phases 0-5 were done except two
leftovers that had been reported as finished and were not.

1.9 -- the dead binding was still there:

    let rzyx = makepad_widgets::Mat4f::identity(); // Simplified — use transform directly
    let rzyx = mat4_mul(...);   // immediately shadows it

Harmless to execution, but it reads as though the rotation is being
skipped, in the one function that builds the model matrix -- in a module
where a rotation bug has already shipped four times. Deleted, with the
real computation formatted so the Z*Y*X order is legible and the degrees
contract stated. (The other half of 1.9, add_part's placement, was
genuinely done: the slot comes from the monotonic id, not parts.len().)

4.6 -- 10 `v18b rev2:` prefixes survived the archaeology sweep, in
arch_gltf, arch_pdf, viewport and workspace. Same treatment as the other
73: keep what the code does, drop which internal revision introduced it.
Now zero.

Also marked the 29 Phase 0/1/2/4 rows that were complete but never
recorded as such, with the specifics rather than a bare "DONE" -- 0.2
notes the lockfile is at the workspace root (a per-crate one would be
ignored, since nigig-build is a member); 1.1 notes the rotation contract
settled on DEGREES, not the radians the plan proposed; 4.3 notes it was
superseded by Phase 5.4 rather than done as written.

Every numbered row in the plan is now DONE, or REJECTED with the
measurement or counter-example that closed it.

783 lib + 154 integration tests pass. All 13 CI gates pass.
2026-08-02 10:27:45 +00:00
24d90b6855 feat(spreadsheet-ui): add retained cell render cache
Some checks failed
nigig-build (CAD) / supply-chain (push) Successful in 4m34s
nigig-build (CAD) / cad-module (push) Failing after 8s
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
2026-08-02 10:20:49 +00:00
Arena Agent
583fbbd092 docs(cad): record Phase 2 and 3 outcomes, including three rejections
Some checks failed
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
Phase 2 is complete (2.6 was the last open item). Phase 3 is complete in
the sense that every item has been either done or measured and closed
with a reason.

Done:  3.2 world-AABB cache (5.9x), 3.4 redraw_all removal (53 calls),
       3.6 script regeneration on commit (425 us/frame at 500 parts),
       3.7 async exports, 3.9 grid batching (5,400 -> 1 tessellation
       per frame at 1080p), 2.6 save dialogs.
       3.1, 3.5 were already done in earlier phases.

Measured and rejected, with the numbers in the table:
  3.3  ParamHash memoisation. 50 ns/node for a Box. The polygon case is
       real (987 ns) but it is the vertex data, and bulk-hashing
       measured no faster; the fix would be a data-model change.
  3.8  Cost-estimate parallel threshold. 1.01x on a warm cache, which is
       the common case.

Two of the completed items were not what the plan described, and the
table now says so rather than quietly claiming the original wording:
  3.9  the plan blamed the "nice number" step computation. That is
       already a cheap if-else chain. The cost was stroke()-per-dash.
  3.2  the plan said to key the AABB cache on ParamHash. Doing that
       would have served a stale box after every drag, because
       ParamHash deliberately excludes the transform.

Also documents the export architecture in ARCHITECTURE.md 2d, including
why the 3D viewer and Bake stay directory-based -- both write companion
file pairs that reference each other by name.
2026-08-02 10:14:24 +00:00
Arena Agent
d61e215e9e perf(cad): batch the 2D grid into one stroke (3.9); measure 3.3 and 3.8
Some checks failed
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
Phase 3.9 -- the real cost was not the "nice number" step computation
the plan named. That is already a cheap if-else chain, no log10/pow. It
was draw_dashed_line calling stroke() after every 6px dash, and stroke()
tessellates the entire accumulated path each time. A full-screen grid is
~50 lines of ~108 dashes: 5,400 tessellations per frame at 1080p, 14,688
at 4K.

Split into queue_dashed_line (appends to the path) and draw_dashed_line
(queues, then strokes) so single-line callers are unchanged. The grid
queues every dash and strokes once. Bubble markers are collected and
drawn after, not inside the loop -- they use draw_text and their own
fills, which would otherwise land in the middle of the grid's path. Also
one String allocation per grid line instead of two.

the_2d_grid_strokes_once_not_once_per_dash pins it. My first version
asserted exactly one stroke in the whole function and failed with 3: the
work-plane cross below the grid is a separate feature with its own
colour and correctly gets its own strokes. Scoped the assertion to the
grid rather than weakening it. Negative test: swapping one
queue_dashed_line back to draw_dashed_line fails it.

---

Phase 3.3 (memoise ParamHash on CadNode): MEASURED, NOT DONE.
  Box:              50 ns/node  -> 25 us/frame at 500 parts
  Extruded 64-gon: 987 ns/node  -> 493 us/frame

The Box case does not justify a cached field that every mutation would
have to invalidate -- the exact hazard Phase 5.4 removed from
part_geoms. The polygon case is the vertex data itself: I tried
bulk-hashing the slice as raw bytes and measured 125 us vs 128 us for
500 x 64 verts, i.e. nothing. The only real fix is to give polygons an
Arc identity the way Csg already has, which is a data-model change, not
a cache. Benchmark kept so the next person starts from numbers.

Phase 3.8 (cost estimate instead of node count): MEASURED, NOT DONE.
  200 nodes, cold cache: 8.80ms seq / 6.21ms par -> 1.42x
  200 nodes, warm cache: 5.81ms seq / 5.73ms par -> 1.01x

On a warm cache -- the common case, since the preview renderer has
already built every mesh -- parallel neither helps nor hurts. A cost
estimate would have to hash every node to count cache misses, in order
to choose between two paths that differ by 1% in the case it would most
often face. The threshold comment now carries these numbers instead of
"can be tuned based on real-world profiling".

783 lib + 154 integration tests pass. All 13 CI gates pass.
2026-08-02 10:13:13 +00:00
nigig-ci
4d627496d2 ci: use the list form of on: so repo-hygiene actually runs
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
repo-hygiene.yml is the one workflow with no path filter. Its whole
purpose is to run on every commit, because the other six are scoped
with `paths:` and a commit touching only unfiltered files otherwise
gets no checks at all. The file's own header comment explains this,
citing commit 8c9ccb9, which pushed 30 conflict-marker lines into two
workflow files and silently disabled the CAD gates.

It has never run. Not once. Of the first 47 task records after a runner
was registered, every other workflow appears and this one does not,
across pushes that touched .forgejo/, tools/, crates/ and Cargo.lock.

The cause is the mapping-with-null-values form:

    on:
      push:
      pull_request:

Valid YAML, both keys parse as None, and it is the spelling GitHub
documents for "all branches". This instance does not schedule it. The
list form does.

So the workflow that exists to catch silently-disabled checks was
itself a silently-disabled check.
2026-08-02 10:09:28 +00:00
Arena Agent
bdf882b817 perf(cad): regenerate the parts script on drag commit, not per frame (3.6)
Some checks failed
nigig-build (CAD) / supply-chain (push) Successful in 2m39s
nigig-build (CAD) / cad-module (push) Failing after 7s
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
`script_dirty` was set on every MouseMove and FingerMove of a part drag.
That makes `sync_parts_from_any_dirty_viewport` call
`generate_parts_script()` -- formatting every part into a String -- and
the workspace then replaces the entire editor document via
`set_editor_text_all`. Per motion event.

Measured before changing it: 42 us at 50 parts, 170 us at 200, 425 us at
500, and that is the string formatting alone, before the code editor's
own work. See bench_parts_script_regeneration_per_drag_frame.

The reason it was set mid-drag no longer holds. The comment said it kept
the split 2D/3D viewports in sync while dragging -- true when each
viewport owned its own parts list, but since Phase 5.2 all three share
one CadDocument. A move IS their state the moment it happens; they need
a repaint, not a resync, and they get one.

Both commit paths already set the flag: the MouseUp arm for mouse
drags, and finish_part_drag for touch (reached from three places). So
the script still regenerates exactly when it needs to -- once, when the
edit is final.

a_drag_regenerates_the_script_on_commit_not_per_frame pins it. It walks
every arm that calls move_selected and asserts none of them set
script_dirty, then asserts the commit paths still exist -- because the
failure mode of this change is not "slow", it is "the script never
updates at all", and a test that only checked the first half would miss
it. Negative test: putting the assignment back fails it with the line
number.

782 lib + 154 integration tests pass. All 13 CI gates pass.
2026-08-02 10:05:45 +00:00
93faa96920 perf(spreadsheet-ui): invalidate grid after sheet switches
Some checks failed
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
2026-08-02 10:03:16 +00:00
Arena Agent
84ed7d2e43 perf(cad): drop 53 redundant cx.redraw_all() calls (Phase 3.4)
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
cx.redraw_all() sets a flag that repaints every widget in the
application. Every one of the 52 calls in viewport.rs, and the 1 in
viewport_2d.rs, sat DIRECTLY after `self.area.redraw(cx)` -- the
targeted redraw was already there and the full-app repaint added
nothing. Verified mechanically before deleting: a scan for
`cx.redraw_all()` not preceded by `area.redraw(cx)` returns zero hits in
both files.

On a drag this ran per motion event: a whole-application relayout to
move one part.

What I did NOT touch, and why:
  workspace.rs (15)      cross-widget coordination. Both viewport sync
                         paths end in a redraw of the OTHER viewports,
                         and that is what makes removing the viewport's
                         own calls safe. Removing these would be a
                         different change with a different argument.
  viewport_input.rs (28) event paths; 13 are not paired with an
                         area.redraw at all, so each needs reading on
                         its own terms rather than a bulk edit.
  cad_editor_sheet.rs (2) not the viewport.

The risk here is a missed repaint, which no test can see, so I checked
the mechanism rather than relying on the suite staying green: cross-
viewport repaint runs through sync_parts_from_any_dirty_viewport (which
calls vp.redraw on each destination) and
sync_view_from_any_dirty_viewport (which ends in its own redraw_all).
Both live in workspace.rs and are untouched.

the_viewport_does_not_ask_the_whole_app_to_repaint pins it as a source
check, because asserting on repaints needs a live Cx the suite does not
have. Negative test: reintroducing one pairing in viewport_2d.rs fails
it with the file and line named.

781 lib + 154 integration tests pass. All 13 CI gates pass.
2026-08-02 10:02:20 +00:00
Arena Agent
73c4c49cb9 perf(cad): cache the world AABB per placement -- 230us -> 39us (Phase 3.2)
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
Measured before building, because I had previously dismissed this item
as "smaller" without checking. It is 5.9x at 500 parts, and hover
picking runs on mouse-move, so it is a per-frame cost.

pick_part's broad phase transformed 8 local corners by the model matrix
for every part on every pick. Phase 5.3 had already removed the
expensive half (it no longer re-meshes to read bounds), leaving 8 matrix
multiplies per part -- cheap individually, 230 us/frame at 500 parts.
SceneCache::world_aabb_for now memoises the result.

The key is a NEW type, PlacedHash, not the existing ParamHash. This is
the whole subtlety of the change: ParamHash deliberately excludes the
transform, because a local-space mesh cannot change when a part moves
(Phase 5.4). A world-space AABB is exactly the opposite -- moving the
part is the entire point. Reusing ParamHash here would serve a stale box
after every drag and make parts unpickable at their new position, which
is the picking equivalent of the stale part_geoms bug.

Making it a distinct type rather than "ParamHash plus a flag" means the
two cannot be confused at a call site.

a_move_invalidates_the_world_aabb_even_though_it_keeps_the_mesh pins the
asymmetry directly: the same move that rebuilds the AABB must still hit
the mesh cache. Negative test: making PlacedHash ignore the transform --
i.e. reverting it to ParamHash -- fails that test. Restored and green.

retain_world_aabbs is paired with every retain_meshes call site, for the
same reason that one exists: the map is keyed by NodeId and nothing
drops an entry when its node is deleted, so without it the map grows for
the session.

775 lib + 154 integration tests pass. All 13 CI gates pass.
2026-08-02 09:58:23 +00:00
nigig-ci
89f7c1df2a docs(ci): how to register a runner, and what the first runs found
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
Not in the repo root -- this lives next to the workflows it describes.

Covers registering a forgejo-runner against gitdab (Gitea 1.22),
the ubuntu-latest label every job depends on, the docker-vs-host
tradeoff, and how to read job logs given that this instance's REST
API 404s on the logs endpoint.

The important section is action resolution: Forgejo resolves `uses:`
against data.forgejo.org with no github.com fallback, and a missing
action fails the job in "Set up job" before any step runs -- which
reads like an infrastructure blip rather than a config error. Records
which actions currently resolve and which do not.

Also records the four defects the first real runs exposed, three now
fixed, so the next person understands why these workflows look the way
they do.
2026-08-02 09:58:08 +00:00
arena-agent
54e1148b39 feat(doc): round-trip tabular clipboard payloads with RFC-4180 quoting
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
doc-engine / engine (push) Successful in 16s
doc-engine / consumer (push) Successful in 3m58s
Cells holding tabs, newlines, CRs, or quotes re-distributed across the
grid on a copy/paste cycle — the documented caveat of every tabular
clipboard milestone. This closes it on both sides:

- table_grid_tsv quotes such fields on the way out (wrapped in double
  quotes, inner quotes doubled) via a new quote_tabular_field helper;
  plain and empty fields stay raw, so payloads stay byte-compatible
  with spreadsheets and plain text editors. The cell-range payload and
  the block-span document payload share the one builder, so both
  inherit the quoting at once.
- paste_table_payload replaces its split('\n')/split('\t') walk with
  split_tabular_payload, an RFC-4180-style tokenizer: quotes open only
  at field start (mid-field quotes are literal), doubled quotes read
  as one, tabs/newlines/CRs inside quotes are literal field text,
  CRLF rows outside quotes keep their tolerance, an unterminated
  quote reads to the end as best effort, and a single trailing
  newline adds no phantom row (a deliberate empty row survives).
- Caret parking, no-op skipping, empty-field clears, and the one-undo
  grouped write semantics of the raw paste milestone are unchanged;
  the caret offset in a multi-line value counts the newline too.

Tests: unit coverage for the writer and the tokenizer (every quoting
rule plus the quote/split round-trip property), runtime coverage of
copy quoting, paste restoring embedded tab/newline values verbatim
with one-undo and redo, and an end-to-end copy-cut-paste cycle; a
doc-engine materialize test pins special-character cell text
surviving peer sync and undo/redo verbatim. README caveats updated
and the milestone documented.
2026-08-02 09:54:40 +00:00
Arena Agent
07cef07dfb feat(cad): async exports with a save dialog (Phase 2.6 + 3.7)
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
Both plan items are the same call, so doing them separately would mean
writing the plumbing twice: the file picker's `save_data` takes owned
bytes, and serialising off the UI thread is what produces owned bytes.

Before: every export serialised on the UI thread straight into a File at
a hardcoded path -- generated/stl/model.stl, generated/3d/model.glb,
generated/floor_plan.pdf. A large scene froze the editor for the whole
serialise, and the second export of a session silently destroyed the
first.

Now:
  PDF, STL   spawn_export_to_target -> worker thread -> save dialog
  SVG        stays sync (the preview widget needs the bytes on the UI
             thread, so there is nothing to move) but gains the dialog
  CLI        already a String in memory; gains the dialog
  3D viewer  async, but stays directory-based ON PURPOSE: it writes TWO
             files and viewer.html references model.glb by relative
             name, so renaming the GLB through a picker would break it
  Bake       stays directory-based: writes the parts.obj/parts.cad pair,
             a workspace artefact rather than a document, and
             Solid::write_obj takes a path not a writer

Results come back through an mpsc channel drained on NextFrame, the same
mechanism the rebuild worker already uses -- not a second bespoke one.
The status says "exporting…" while in flight, and the completion message
for a dialog export says "ready — choose where to save" rather than
claiming the file is written, because at that point it is not. Reporting
success before the write is the exact bug fixed in the Save buttons
earlier.

The 3D companion HTML is now only written if the GLB actually landed.
Previously a truncated GLB still got a viewer.html beside it, which is
how "export succeeded" turned into a blank page.

ExportTarget::suggest gives each export a distinct default name
(<stem>-<project>-<counter>.<ext>) so successive exports do not propose
to overwrite each other.

sanitize_file_stem exists because a project name is user text that ends
up in a save dialog. It can contain a path separator, "..", a NUL, a
leading dash, or 300 characters of emoji. The test found a real bug in
my first version: replacing "/" with "_" turns "../../etc/passwd" into
"_.._.._etc_passwd", so trimming leading dots BEFORE the replacement
leaves "_.." behind. Trim after, and include "_".

Deleted in the same commit as their cause: write_floor_plan_pdf and
export_to_file, both now unreachable. Verified no callers remain
anywhere in crates/.

New tests: bytes land at the requested path; a failed write is reported
rather than swallowed (parent is a regular file, so create_dir_all
cannot succeed); an empty export still produces a file; the hostile
project-name corpus; successive suggestions do not collide.

772 lib + 154 integration tests pass. All 13 CI gates pass.
2026-08-02 09:52:27 +00:00
52eff6167d perf(spreadsheet-ui): invalidate full grid after scrolling
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
2026-08-02 09:51:13 +00:00
7fdf436510 test(pay): Phase 5 lifecycle matrix as domain tests (R2.3)
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
Payment domain, storage, platform and UI / isolated-payment-tests (push) Successful in 2m36s
Payment domain, storage, platform and UI / payment-ui-tests (push) Successful in 3m17s
PDF engine / engine (push) Successful in 46s
PDF engine / makepad-integration (push) Successful in 3m23s
PDF engine / fuzz (push) Has been skipped
The exit criterion names five scenarios: permission denial, cancellation,
backgrounding, app restart, out-of-order callbacks.

Four of the five are state questions, not hardware questions. A device adds
confidence that Android really emits a given callback sequence; it cannot
tell you how the domain reacts, because the state machine decides that. So
the matrix runs against the real coordinator on every commit instead of when
a phone is free, and a regression names the invariant it broke.

13 tests in crates/nigig-pay-domain/tests/lifecycle_matrix.rs, including the
cases that only exist as races: a success arriving after a cancellation;
backgrounding before a grant (must refuse) versus after one (must be
preserved — the user did authorise); restart before dispatch versus after; a
foreign grant; a replayed grant. Plus a clean-path test so the matrix cannot
pass by refusing everything.

## A coverage hole the matrix found

a_restart_after_dispatch_cannot_redispatch passed with the duplicate-dispatch
budget removed. The state machine refuses Submitted -> Dispatching first, so
the budget was never reached. That is good defence in depth and bad
coverage — nothing proved the budget still worked.

the_dispatch_budget_survives_a_state_machine_walk_back forces the intent back
to Dispatching, exactly as a faulty recovery path would, leaving the budget
as the only guard. It fails when the budget is removed.

The forcing hook is behind a `test-hooks` feature, not #[cfg(test)]: an
integration test is a separate crate and does not see cfg(test), so the
method was simply missing. The isolated runner enables it explicitly,
otherwise that test is silently filtered out and proves nothing.

## Verified by injection

  authorization gate removed  -> 7 of 13 fail
  dispatch budget removed     -> 1 fails (the new one)

## What still needs hardware

That Android actually produces these sequences: permission dialogs,
process-death timing, callback ordering under memory pressure. This file
asserts the response is correct for each sequence; a device confirms the
sequences are the real ones. Different claims, both needed. Tracked as R2.3b.

## Validation

  domain 148 unit + 13 matrix, fmt, clippy -D warnings, bench    pass
  storage 46 / platform 64 / mpesa 29 / pay-ui 78                pass
  clippy -p nigig-pay-ui --no-deps -D warnings                   0 errors
2026-08-02 09:47:52 +00:00
nigig-ci
bda0124992 ci(pay): pass --config to cargo-deny's check subcommand, not the binary
Some checks failed
Payment domain, storage, platform and UI / isolated-payment-tests (push) Has been cancelled
Payment domain, storage, platform and UI / payment-ui-tests (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
The dependency audit step has never audited anything. cargo-deny 0.18.6
exits 2 immediately:

    error: unexpected argument '--config' found
    tip: 'check --config' exists

--config belongs to the `check` subcommand, and its path resolves
relative to the manifest rather than the working directory, so it also
has to be absolute. nigig-build.yml already gets both right; this
invocation predates that fix.

This step is the last one that runs in isolated-payment-tests, so its
failure also skipped the three gates behind it:
  - Payment crates must not depend on Makepad
  - Domain and storage must not reach the platform SDK
  - Mock gateway must not compile into a release build

Verified with cargo-deny 0.18.6 against all three crates:
  nigig-pay-domain    advisories ok, bans ok, licenses ok, sources ok
  nigig-pay-storage   advisories ok, bans ok, licenses ok, sources ok
  nigig-pay-platform  advisories ok, bans ok, licenses ok, sources ok

Found by running the workflow on a real runner for the first time.
2026-08-02 09:42:46 +00:00
703bba3652 perf(spreadsheet-ui): distinguish dirty cells and full invalidation
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Successful in 2m38s
nigig-build (CAD) / cad-module (push) Failing after 8s
nigig-build (CAD) / full-crate-check (push) Successful in 4m53s
2026-08-02 09:38:43 +00:00
d567978119 feat(pay): key rotation for the encrypted ledger (R2.2)
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
Payment domain, storage, platform and UI / isolated-payment-tests (push) Failing after 2m19s
Payment domain, storage, platform and UI / payment-ui-tests (push) Has been cancelled
Examined R2.2 the way R2.1 turned out to need, rather than assuming the
whole item was device-blocked.

Most of 3.1 was already present: DatabaseKeyProvider, open_encrypted,
wrong-key rejection distinct from corruption, keystore-unavailable failing
closed, and a test asserting no PII appears in the raw file.

## The real gap was rotation, and it is pure logic

A StaticTestKeyProvider key lives forever. An Android Keystore key does not:
KeyPermanentlyInvalidatedException is thrown after fingerprint re-enrolment,
adding or removing a screen lock, or a device restore. That is ordinary, not
exceptional. With no rotation path the only responses were "lose the ledger"
or "keep using a key that no longer exists" — and the second is not
available, because the key is gone.

Deleting the ledger is not an option either. It destroys the record of money
that may have left the account, which is the same reasoning that makes
retention.rs refuse to sweep unreconciled rows.

rotate_key uses PRAGMA rekey, which re-encrypts every page inside SQLCipher's
own transaction, then proves the new key reads the data before returning — a
rekey that reported success but left the file unreadable would otherwise only
surface on the next launch, by which time the old key may be gone.

5 tests: records survive rotation, the superseded key stops working, an empty
key is refused without damaging the file, rotation is repeatable, and the
schema version is untouched. Verified by neutering rotate_key: 3 fail.

Storage tests 41 -> 46.

## Ordering, documented at the trait

The caller persists the new key only after rotate_key returns Ok. The reverse
order leaves a stored key that does not open the file. This order leaves, at
worst, a re-keyed file whose new key was not saved — recoverable by rotating
again from the old key still in the keystore.

## What remains

The JNI call itself: KeyGenParameterSpec with user authentication required,
the AndroidKeyStore provider, and catching KeyPermanentlyInvalidatedException.
The full contract is written on DatabaseKeyProvider so it is not rediscovered
from scratch. Tracked as R2.2b. Everything except the platform call is
already exercised by the sqlcipher suite.

## Validation

  storage 46 (was 41) with --features sqlcipher                  pass
  domain 148 / platform 64 / mpesa 29 / pay-ui 78                pass
  clippy -p nigig-pay-ui --no-deps -D warnings                   0 errors
  builds: pay, mpesa, core                                       pass
  rotation injection: 3 tests fail when rotate_key is neutered   pass
2026-08-02 09:37:00 +00:00
arena-agent
a66d7d2198 feat(doc): carry table grids in document-level clipboard payloads
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
A block-span selection — Shift+Arrow across a table, select-all,
any multi-block drag — used to copy blank lines where tables sat,
so copying a document lost every table's content. Table blocks now
contribute their whole grid at their block position as tab/newline
lines, built by the same table_grid_tsv helper the cell-range
payload uses (extracted from cell_range_clipboard_text): one
builder, one convention, no drift between "copy a range" and
"copy across a table". Stored cell text exports verbatim — a
merge's covered cells keep their hidden values — and empty tables
still contribute nothing.

Cutting such a span was already structurally correct through
replace_block_range / CancelBlockRange, so the milestone is
payload-only: the payload now matches what actually disappears —
verified by a cut over [paragraph, 2x2 table, paragraph] draining
the document with "lead\na\tbc\nd\te\ntail" in the payload and one
undo restoring blocks AND every cell value. Also covered:
select-all through the TextCopy hit, and a partial mid-paragraph
span splicing the grid between its text fragments in order. The
stale "tables are skipped" select-all bullet is retired.
2026-08-02 09:35:13 +00:00
85f21a6f35 perf(spreadsheet-ui): consume dirty cells during intent dispatch
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
2026-08-02 09:34:16 +00:00
Arena Agent
c6e7635c5a test(cad): fuzz the coordinate parser; audit all 84 indexing sites
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
I dismissed indexing_slicing as "mechanical churn" without checking.
That was an unchecked claim about 105 reported panic sites, which is
exactly what I have been objecting to elsewhere in this codebase. Audited
all of them.

105 clippy hits are 84 unique lines. Every one is bounded:

  43  fixed-size arrays indexed by a literal or a 0..N loop whose bound
      matches the array -- mat4_mul, mat4_inverse, ray_aabb_intersect,
      the 8-corner projected box. Unindexable by construction.
  ~30 behind an explicit length check in the same function:
      verts.len() >= 12 (I-beam web), >= 8 (HSS inner wall),
      pts.len() == 4, chamfer_rect's `< 4` early return, polygon_area's
      `n < 3`, pick_part's per-triangle bounds test.
   ~11 `% len()` on a non-empty slice, or selection[i] over
      0..selection.len().min(3).

Converting these to .get() adds ~84 `else { continue }` arms guarding
conditions the compiler or an adjacent check already rules out, each one
a place to get the fallback subtly wrong. Not gated; the audit is
recorded in ARCHITECTURE.md 2c so the next reader gets the evidence
rather than the dismissal.

The one genuinely input-facing site is fuzzed instead.
parse_coord_input takes raw text from the coordinate box on every
keystroke and indexes parts[0..2] after a split. Two new tests: an
adversarial corpus (multi-byte leading characters, lone separators, RTL
and combining marks, 500-char inputs, inf/NaN/1e400) and every
char-boundary prefix of a valid input, because the box parses as you
type.

Building the corpus is where the actual work was. My first version --
garbage strings -- passed even with the length guards deleted, because a
first component that fails to parse makes `?` return before the second
index is evaluated. It looked like a strong test and tested nothing. The
cases that reach the guards are the ones whose FIRST component is valid:
"@5", "5<", "@5<45".

Negative test, per guard:
  spherical  parts.len() == 3 removed -> FAILS, index out of bounds
  polar      parts.len() == 2 removed -> FAILS, index out of bounds
  relative   parts.len() >= 2 removed -> still passes, and that is
             correct: the branch is gated on contains(','), and a string
             containing a comma always splits into at least two parts,
             so the check is redundant. Verified rather than assumed;
             left in place because it states intent locally.

763 lib + 154 integration tests pass. All 13 CI gates pass.
2026-08-02 09:33:31 +00:00
nigig-ci
892471c73f ci: commit tools/*.sh executable, and gate the mode
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
Payment domain, storage, platform and UI / isolated-payment-tests (push) Failing after 2m19s
Payment domain, storage, platform and UI / payment-ui-tests (push) Has been cancelled
Five of the six scripts under tools/ were committed mode 100644. Every
one of them is invoked with a leading ./ from pay-domain.yml or
pdf.yml, so those steps could only ever fail:

    ./tools/makepad-native-libs.sh: Permission denied
    ./tools/test-mpesa-store-clean.sh: Permission denied

Both are real failures from run 349, the first time a runner existed to
execute pay-domain.yml at all. They fail at the job's first substantive
step, so payment-ui-tests did no work whatsoever and
isolated-payment-tests skipped its last seven gates -- including the
dependency audit, the "payment crates must not depend on Makepad"
check, and the mock-gateway-in-release guard.

The mode is a property of the index, so a local chmod that is never
staged does not fix it. Marked all five executable with
`git update-index --chmod=+x` and added a hygiene gate that fails if any
tracked tools/*.sh is not 100755.

repo-hygiene.yml is the right home: it has no path filter, needs no
toolchain, and already exists to validate CI configuration itself.

Gate negative-tested: reverting one script to 100644 fails it with
"tools/makepad-native-libs.sh is mode 100644, expected 100755";
restoring the bit passes.
2026-08-02 09:28:52 +00:00
874a8481fa perf(spreadsheet-ui): track dirty grid cells
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
2026-08-02 09:23:13 +00:00
1d3e6ab72a fix(pay): correlate USSD callbacks to the payment that asked for them
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
doc-engine / consumer (push) Successful in 3m56s
Payment domain, storage, platform and UI / payment-ui-tests (push) Failing after 4s
doc-engine / engine (push) Successful in 15s
nigig-map / test (push) Failing after 2s
Payment domain, storage, platform and UI / isolated-payment-tests (push) Failing after 1m58s
sms / gates (push) Successful in 3s
sms / robius-sms (push) Successful in 22s
sms / android (push) Successful in 21s
sms / nigig-sms (push) Successful in 4m6s
sms / supply-chain (push) Successful in 4s
R2.1. Review items 2.7 and 5.3.

## SessionRegistry was built in Phase 5 and never wired

The pump still read:

    while let Some(ev) = robius_ussd::next_event() {
        ... if let Some(id) = h.current.take() { ... }
    }

next_event() drains a process-wide queue and its entries carry no session
id, so every event was applied to whatever `current` happened to be.

Reproduced before changing anything: payment A is dispatched then abandoned
with events still queued; payment B starts; the pump drains A's ResultText
and SessionEnded and applies both to B. An abandoned payment settles the one
that replaced it.

## Now

- Dispatch claims the single in-flight slot. The USSD backend returns no
  session handle, so the intent id is the correlation id — enough, because
  the registry only has to tell this payment from the previous one.
- Every event is admitted against the live operation before it can touch an
  intent. Foreign and stale events are logged and dropped.
- Terminal events are de-duplicated; progress chatter still repeats freely.
  ussd_duplicate_key mirrors ProviderSignal::duplicate_key in the platform
  crate, and a test pins the two together.
- All six terminal and teardown paths retire the session id, so a late
  duplicate cannot revive a closed operation.

6 tests, including the abandoned-payment scenario by name. Verified by
removing the close call: that test goes red. CI gate asserts the pump still
admits, dispatch still claims, and at least six paths still close — matching
the method rather than a receiver literal, because rustfmt wraps the call.

## What this does not do

The pump still lives in PayFlowHandler, which still owns the pending-store
writes and the bulk queue. Moving *ownership* to PaymentCoordinator changes
who cancels on teardown and who observes an out-of-order callback, which is
what ADR 0007's device matrix exists to check. That is now tracked as R2.1b.

The correlation defect — the one that could settle the wrong payment — is
closed, and it did not need a device. I had previously filed the whole of
R2.1 as device-blocked; that was too coarse.

## Validation

  nigig-pay-ui 78 (was 72) / nigig-mpesa 20                        pass
  domain 148 / storage 41 / platform 64 / mpesa 29                 pass
  clippy -p nigig-pay-ui --no-deps -D warnings                     0 errors
  builds: pay-ui, pay, mpesa, core; default and --no-default       pass
  correlation injection: abandoned-session test fails without it   pass
  pin-capture guard                                                pass
2026-08-02 09:21:45 +00:00
Arena Agent
072dcde979 docs(cad): record Phase 5/6 outcomes, including the two rejections
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
Updates the file table in ARCHITECTURE.md for the three new files and
corrects the viewport.rs/workspace.rs descriptions, which still claimed
to hold the rendering and input code that moved out. A doc that lies is
worse than no doc.

Marks the plan items honestly:

  5.5 DONE   viewport.rs 8,023 -> 4,796. The original six-file target was
             not met and is not being pursued: the remaining 4,796 lines
             have no seam comparable to the two that were taken.
  5.6 DONE   workspace.rs 3,991 -> 3,273.
  5.7 REJECTED after counting. "Branched on in 40 methods" was 14, and
             21 of the 37 branches were in one function.
  5.8 REJECTED. The premise is false -- kind is not derivable from the
             solid, because six PartKinds share CadSolid::Box.

Phase 6 marked partially done, with what is left stated plainly: the
indexing_slicing ratchet is 105 sites in CAD, mechanical churn rather
than defect-finding, and the panic family it was really aimed at is now
at zero and gated.

Recording the rejections in the plan matters as much as the completions:
both items would have destroyed something real, and the next reader
should find the counter-evidence rather than the instruction.
2026-08-02 09:21:22 +00:00
Arena Agent
015cf44422 fix(cad): remove the reachable panics; gate unwrap/expect at 5
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
Phase 6, scoped to what is provable rather than a blanket -D warnings.

Measured the CAD module first: ~200 clippy warnings, but the panic
family -- the part the plan actually cared about, copying the pay
crates' ratchet -- was only 8: 2 unwrap, 6 expect, 0 panic!. Three were
real, five are genuine constructor invariants.

Fixed:

- code_editor.rs x2. `lazy_init_session(); self.session.as_mut().unwrap()`
  in both draw_walk and handle_event. Correct today, but the guarantee
  lived across a function boundary the compiler cannot see, so an
  unwrap sat on a widget draw path waiting for a third caller to forget
  the prologue -- and a panic there kills the editor with unsaved work
  in it. Added `editor_and_session()`, which splits the borrow and
  returns Option, so both sites take an early return instead.

  I first tried folding init into `get_or_insert_with`. That silently
  dropped the `keep_cursor_in_view = Once` side effect, which only
  happens on the create path. Caught it by grepping for the field rather
  than trusting the refactor; reverted.

- cad_scene.rs x1. MeshCache::get_or_build did
  `.write().expect("mesh cache poisoned")` while every other method on
  the type already degraded with `if let Ok(..)`. Reachable: the export
  path calls get_or_build on a spawned thread, so one panicking worker
  poisoned the lock and the next draw took the UI thread down with it.
  The cache is pure derived data -- every entry rebuilds from its node
  -- so a poisoned lock now costs memoisation, not correctness. The mesh
  is built before the lock is taken, and the double-check still prefers
  a racing thread's entry so Arc::ptr_eq comparisons stay consistent.

Left alone, with reasons: 4 x cad_scene "default material/layer always
exists" (SceneBuilder::new inserts both; verified) and 1 x arch_gltf
serde_json::to_vec over a Value built in that file.

New gate: "No new unwrap/expect in CAD production code", allowlist of 5.
A bare count drifts upward quietly and a blanket ban just gets
#[allow]-ed, so the count is pinned and each exemption is named in the
comment.

The gate skips #[cfg(test)] by BRACE DEPTH rather than stopping at the
first one. That matters: arch_gltf.rs has production code after two test
modules, so the existing panicking-macro gate's "stop at first
#[cfg(test)]" awk cannot see line 850 at all. My first attempt used the
same awk idiom and reported 4 of 5 -- I only noticed because the number
disagreed with clippy. Verified the older macro gate is not currently
hiding anything, but it is hiding it by luck.

Both negative tests pass: an unwrap added to viewport.rs is caught, and
one added to arch_gltf.rs *after* its test modules -- the exact blind
spot -- is also caught, named with file and line.

13 gates now, all green. 761 lib + 154 integration tests pass.
2026-08-02 09:20:12 +00:00
arena-agent
4df2193859 docs(doc): retire stale in-cell paste and migration-status claims
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
doc-engine / engine (push) Successful in 15s
doc-engine / consumer (push) Has been cancelled
Tabular paste superseded the clipboard section's "in-cell pastes
always stay a single whole-cell write, newlines included", and the
doc-engine README still framed the controller migration as "next"
even though the workspace navigation already runs on CrdtDocEditor
(the classic editor stays registered as a fallback). Found during
the phase audit.
2026-08-02 09:17:57 +00:00
Arena Agent
b26f4c8b91 refactor(cad): split handle_event into pointer and touch dispatch
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
Phase 5.7. handle_event was 952 lines holding two independent dispatch
loops -- one over raw Event (mouse buttons, motion, wheel), one over
hit-tested Hit (finger down/move/up/scroll). They share no locals, so
the seam was already there.

The plan item asked to "consider making 2D/3D two widgets" because the
view mode was "branched on in 40 methods". I counted before acting: 14
methods, 37 branches, and 21 of those branches are inside handle_event
alone. The remainder are mostly one-liners picking a pan axis or a zoom
helper.

Two widgets would have duplicated the entire input layer to delete a
handful of matches! guards, and doubled the surface the shared
CadDocument must keep coherent -- which is precisely what Phase 5.2 was
spent removing. The size problem was the event handler, not the enum.
Split the handler; keep the enum. Reasoning recorded in the module doc
so the plan item is not re-attempted from its original framing.

Verified as a move rather than a rewrite: the sequence of Event::/Hit::
match arms across viewport.rs + viewport_input.rs is identical to HEAD's
-- 41 arms, same order -- and the 3D-only desktop camera fall-through
appears exactly once, as before. Order between the two loops is
preserved and load-bearing: pointer arms set drag state the hit arms
read.

viewport.rs: 5,702 -> 4,796 lines (8,023 at the start of Phase 5.5).

761 lib + 154 integration tests pass. All 12 CI gates pass.
2026-08-02 09:14:22 +00:00
Arena Agent
f02de5645f refactor(cad): split handle_actions into three per-panel handlers
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
Phase 5.6. `CadWorkspace::handle_actions` was a single 1,133-line
method: one flat sequence of `if button.clicked(actions)` arms covering
every control in the editor. Adding a control meant reading all of it to
find where the related ones lived.

Split along the seams the comments already marked -- the panel groupings
existed, they just were not expressed in the code:

  handle_view_and_plane_actions    work/inclined planes, per-axis grids,
                                   reference and clip planes,
                                   construction toggles, coordinate input
  handle_export_and_file_actions   CLI/OBJ/PDF/3D/STL/SVG export, the AI
                                   attach-image and model controls,
                                   open/save/save-as
  handle_properties_panel_actions  colour swatches, layer/material
                                   dropdowns, per-axis position/rotation/
                                   size inputs, DOF toggles

Order is the risk in splitting a sequential handler: several arms depend
on running after an earlier one has updated state. Verified mechanically
rather than by reading -- the full sequence of `ids!(..)` widget
references through handle_actions plus the three extracted bodies is
byte-identical to HEAD's, 147 references in the same order.

19 helpers became pub(super). That is not a widening: they were already
reachable from anywhere in the cad module, and pub(super) keeps them
exactly there. Nothing was made `pub`.

workspace.rs: 3,991 -> 3,273 lines.

761 lib + 154 integration tests pass. All 12 CI gates pass.
2026-08-02 09:11:20 +00:00
648c662a1e perf(spreadsheet-ui): cache visible text measurements
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
2026-08-02 09:10:46 +00:00
nigig-ci
015462b386 ci(sms): install the Android SDK inline instead of a nonexistent action
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
sms / gates (push) Successful in 4s
sms / robius-sms (push) Successful in 23s
sms / android (push) Successful in 34s
sms / nigig-sms (push) Successful in 4m10s
sms / supply-chain (push) Successful in 4s
A runner was registered against this repo for the first time, so the
workflows in .forgejo/ finally executed instead of only ever being run
by hand. The android job failed immediately:

    Unable to clone https://data.forgejo.org/android-actions/setup-android
    refs/heads/v3: repository not found: Not found.

android-actions/setup-android does not exist on data.forgejo.org, and
Forgejo does not fall back to github.com for action resolution. The
failure happens in "Set up job", before any step runs, which cancels
all seven remaining steps. The job reported failure without compiling a
single line -- so the Android gate, the only job in this file that sees
the ~600 lines of JNI under #[cfg(target_os = "android")], has never
checked anything.

Replaced with an inline cmdline-tools install, which is the same
sequence used to verify these crates by hand and depends only on
actions/checkout and actions/setup-java -- both of which do resolve.

Verified on the same runner in this run: gates, robius-sms (48 tests),
nigig-sms (46 tests, floor gate, clippy ratchet) and supply-chain all
pass. nigig-map.yml has the identical defect with actions/setup-rust@v1
and is left alone here.
2026-08-02 09:09:57 +00:00
Arena Agent
1bc5d792a8 refactor(cad): extract the 25 draw_* methods into viewport_render.rs
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
Phase 5.5, first cut. viewport.rs was 8,023 lines with a single
5,930-line `impl CadViewport`. The drawing methods are the largest
coherent seam in it: 25 methods, 2,321 lines, that only read editor
state and emit draw calls.

Rust merges inherent impl blocks for the same type across files, so this
is a pure move -- no signature changed and no caller was touched.
Verified mechanically rather than by eye: the set of method names across
viewport.rs + viewport_render.rs is byte-identical to the set in
viewport.rs at HEAD, 187 before and 187 after, none lost, none gained.

Two items needed pub(super) -- `part_model_matrix_cadnode` and
`DrawCadMesh::draw`. That is not a widening: both were already reachable
from anywhere in the cad module, and pub(super) keeps them there. No
item was made `pub`.

The module doc states the rule for what belongs in the file: a method
lives here if it takes &mut Cx2d/&mut Cx3d and emits drawing commands.
Anything that DECIDES what to draw -- picking, snapping, hit-testing,
tool state -- stays put. Deliberately mechanical, because the previous
attempts to split this file were judgement calls and did not hold.

viewport.rs: 8,023 -> 5,702 lines.

---

Phase 5.8 (remove kind_hint): REJECTED, with the counter-example
recorded as a test rather than a comment.

The plan item says to "derive kind from the solid variant". That is not
possible. Six PartKinds -- Cube, Wall, Slab, Door, Window, Beam -- all
build CadSolid::Box, and Cylinder/Column both build CadSolid::Cylinder.
kind_hint is not duplicated state; it is the only record of which one
the user asked for. Removing it would silently downgrade every Wall,
Slab, Door, Window and Beam to a Cube, taking the properties panel's
thickness controls and the Extrude tool's kind filter with it.

several_part_kinds_share_one_solid_variant_so_kind_is_not_derivable
asserts the collision directly, so the next person to read that plan
item finds the evidence instead of executing it.

I also built a script_tag/from_script_tag round-trip so the kind could
survive a save, then deleted it before committing: nothing reads
parts.cad back, so it would have been a write-only annotation -- the
exact "add an abstraction, declare the migration done" pattern this
phase exists to stop. The real gap is that the parts list has no
serialisation at all, which is a feature, not a cleanup.

756 lib + 154 integration tests pass. All 12 CI gates pass.
2026-08-02 09:08:27 +00:00
arena-agent
c5c1e0a3e9 feat(doc): paste tabular payloads across table cells
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
doc-engine / engine (push) Successful in 15s
doc-engine / consumer (push) Successful in 3m55s
A TextInput carrying tabs or newlines while the caret lives in a
table now pastes the spreadsheet way — one cell per tab stop, one
row per line — starting at the caret cell, or at the armed range's
normalized top-left (consuming the range like any paste-over-
selection). The distribution rides the grouped set_table_cells from
the previous milestone so the rectangle un-pastes in one undo step,
and the caret parks at the last cell the payload touched. Rows or
columns past the table edge clip, CRLF strips per line like
text-block pastes, empty fields clear their targets, and unchanged
cells are skipped so a paste lands neither redundant LWW ops nor
dead group members. Plain payloads keep the whole-cell char splice,
and cells still never spawn blocks.

The pre-existing in-cell paste test asserting the old "newline stays
embedded in the cell" behavior is rewritten to the distribution
semantics; new runtime tests cover the 2x2 distribution with caret
parking and the one-undo/redo round trip, edge clipping without
wrap-around, backward-spanned ranges pasting from their top-left,
CRLF with empty-field clears, and the mobile menu's Paste action
distributing from the pressed cell. Engine integration coverage
replays a grouped multi-cell write into a peer and asserts the
projections converge.
2026-08-02 09:02:05 +00:00
Arena Agent
5ffc515f91 perf(cad): build the command scene snapshot lazily -- 254us -> 0.8us
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
CadCommandCtx::new eagerly called scene_cache.scene_for(parts), which is
O(nodes): every node cloned, every material re-registered. No production
command reads that scene -- MoveNode, ResizeNode, RotateNode, YawNode,
ModifyNode, CreateNode and DeleteNode all address a node by id. Only
tests and the benchmark call ctx.scene().

Every command mutation bumps the PartsStore generation, so the next
context construction was a guaranteed cache miss. move_selected issues
one command per selected part per frame, making a drag O(commands x
nodes) for work that is O(1) per command. This arrived with Phase 5.1,
when the generation counter turned a previously accidental cache hit
into a guaranteed miss; the 0.4us baseline predated it.

The snapshot is now built on the first scene() call and memoised.
CommandContext::scene() returns Arc<CadScene> rather than &CadScene so
an implementation can build on demand instead of keeping one alive for
the context's lifetime.

This was also a latent CORRECTNESS bug, not only waste. The eager
snapshot was captured BEFORE the command ran, so a command that mutated
and then read scene() saw its own edit missing. Every mutating method
now drops the memo; those invalidate_snapshot() calls are placed
directly beneath the existing scene_cache.mark_dirty() calls so a new
mutator cannot silently miss one.

Two tests, both verified to fail against the old code:
- a_command_that_never_reads_the_scene_does_not_build_one asserts on
  SceneCache::rebuild_count (a new #[cfg(test)] counter) rather than
  wall-clock, so it is deterministic rather than machine-dependent.
  Fails 20-vs-0 when construction is made eager again.
- a_lazily_built_scene_reflects_edits_made_earlier_in_the_same_context
  reads the scene BEFORE mutating, then again after. Reading only after
  the mutation passes even with invalidate_snapshot() gutted -- the lazy
  build simply happens later -- so the first read is what gives the test
  teeth. I checked: the obvious version of this test was vacuous.

The benchmark was also measuring the wrong thing. It called ctx.scene()
each iteration to read the start position, which no production path
does: move_selected and finish_part_drag both read p.pos() off the parts
list. Fixing the lazy build alone moved undo 228us -> 0.5us but left
execute at 231us, because the benchmark was timing its own scene read.
It now mirrors the production callers.

Measured: execute 254us -> 0.8us, undo 246us -> 0.5us (1000 commands
over a 1000-node scene), ~300x. No other benchmark regressed.

755 lib + 154 integration tests pass. Test-name list diffed: +2, nothing
dropped. All 12 CI gates pass.
2026-08-02 08:57:10 +00:00
f33e999991 perf(spreadsheet-ui): improve unicode text width estimate
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
2026-08-02 08:53:26 +00:00
5800beb552 fix(pay): close the R1 gaps against the completion standard
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
Payment domain, storage, platform and UI / isolated-payment-tests (push) Failing after 2m2s
Payment domain, storage, platform and UI / payment-ui-tests (push) Failing after 3s
You are right that my first pass at R1 fell short. It deferred an item on a
judgement call, and it fixed three defects without regression tests naming
them. Four gaps, all closed here.

## 1. S8 was deferred; it is now done as far as the platform allows

I skipped certificate pinning as "wasted work if the endpoints get dropped".
That was my call to make about effort, not an external blocker.

Investigated properly: Makepad's HttpRequest exposes no pinning API. Its
only TLS control is set_ignore_ssl_cert, which weakens verification. Pinning
is not implementable at this layer without patching the platform crate.

What *is* enforceable is the property pinning mostly buys — that a mistyped,
injected or attacker-supplied URL cannot be dialled. check_transport gates
every request on HTTPS plus a four-host allowlist, at all three dial sites
in both copies of the client.

7 tests: lookalike hosts (api.coingecko.com.evil.example), embedded
credentials (https://evil@real/), explicit ports, plain HTTP, malformed
URLs, and an assertion that TLS is never disabled. Verified by disabling the
allowlist: 3 tests fail.

## 2. The 13-digit phone defect had no test naming it

I fixed it and moved on. It now has a regression test quoting the original
duplicated branches, plus a property test that normalisation output is
either empty or exactly a valid 10-digit 07/01 number — no third outcome.

## 3. The fee-policy UI wiring was untested

The domain guard had 11 tests; the wiring that connects it to the pay sheet
had none, so nothing proved the sheet actually consults it. Four tests now
cover the shipped policy: it identifies the bundled tariff, refuses once
stale, still quotes while current, and keeps "unknown band" distinct from
"stale table".

## 4. The exchange client had no tests at all

It does now, via the transport module above.

## A test that failed against itself

tls_verification_is_never_disabled_in_this_module asserts the module never
calls set_ignore_ssl_cert — and the literal in the assertion put the string
in the file, so it failed on first run. The needle is now assembled at
runtime. Recorded because it is exactly the kind of thing that gets
"fixed" by deleting the test.

## Completion standard, now written into the plan

A phase is done when: no item is deferred on a judgement call; no capability
is removed to satisfy a review item; defects found while implementing are
fixed in the same phase even if absent from the review; every fix carries a
test that fails without it; and CI enforces it.

## Validation

  domain 148 / storage 41 / platform 64 / mpesa 29                pass
  nigig-pay-ui 72 (was 66) / nigig-mpesa 20                       pass
  clippy -p nigig-pay-ui --no-deps -D warnings                    0 errors
  builds: pay-ui, pay, mpesa, core                                pass
  allowlist injection: 3 tests fail when disabled                 pass
  pin-capture guard                                               pass

Pre-existing and untouched: `cargo test -p nigig-pay --lib` fails to build
on clean HEAD (ClassifiedTransaction not in scope in transact.rs). Verified
by stashing. The transport tests are exercised through the nigig-mpesa copy.
2026-08-02 08:52:32 +00:00
3fda46b1eb fix(spreadsheet-ui): flush toolbar mutation intents per event
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
2026-08-02 08:46:05 +00:00
Arena Agent
11ef0fbf67 fix(cad): GPU buffers self-invalidate; stop re-meshing on every drag frame
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
Phase 5.4. Two defects with one root cause: part_geoms was keyed on the
raw node id, so staleness was invisible to the type and correctness rested
on seventeen scattered `part_geoms.remove(&id)` calls at the edit sites.
All seventeen are deleted; the map is now keyed on (id, ParamHash), the
same key MeshCache uses, and an entry whose hash no longer matches its
node is simply never read.

1. subdivide_selected drew the wrong geometry. It replaced each selected
   part's solid with a fresh Csg and invalidated the MESH cache, but never
   removed the part's part_geoms entry -- so the stale uploaded buffer
   stayed a hit and the viewport kept drawing the un-subdivided shape.
   An audit of every mutation path found this was the only edit site
   missing its manual eviction, which is exactly the failure mode a manual
   protocol produces.

2. ParamHash covered the transform and the material, so a pure move
   invalidated the mesh. It should not: build_mesh reads neither. The
   cached TriMesh is in the node's LOCAL space, and all four consumers --
   the draw loop, pick_part, the STL and glTF exporters -- apply the model
   matrix themselves. Dragging a part therefore re-triangulated it on
   every frame to produce byte-identical triangles, at up to 886 ns per
   extruded part per frame, per selected part. The hash now covers the
   solid parameters and nothing else.

Two existing tests asserted the old behaviour and were INVERTED, not
deleted -- they described what the code did rather than what it needed to
do:
  cache_detects_transform_edits_via_param_hash
    -> a_transform_edit_reuses_the_cached_local_space_mesh
  mesh_cache_self_invalidates_on_parameter_and_transform_edits
    -> mesh_cache_self_invalidates_on_solid_parameter_edits

New: build_mesh_output_does_not_depend_on_the_transform guards the
assumption the key now rests on -- if anyone makes build_mesh bake the
transform in, it fails and the key must grow it back. Plus
mesh_cache_hits_on_a_pure_transform_edit, mesh_cache_hits_on_a_material_edit
and an_uploaded_buffer_is_not_reused_after_the_solid_changes. Negative
test: restoring the transform to the hash makes the first two fail;
restored and green.

Deletion is still explicit (`retain` on the live id set) because it is the
one case a content hash cannot express -- there is no node left to hash.

ParamHash is pub(crate), not pub: it is how the caches agree on staleness,
not a consumer contract.

Also recorded in BENCH_BASELINE.md, not fixed here: bench_command_execute
_overhead is 227 us/cmd against a stale recorded 0.4 us. Verified
pre-existing -- 254 us on the commit before this branch, so this work
slightly improves it. CadCommandCtx::new eagerly builds a scene snapshot
that most commands never read, and every command bumps the generation, so
it is O(commands x nodes). The fix is to make that snapshot lazy; it is a
separate change and gets its own commit.

753 lib + 154 integration tests pass. Test-name list diffed, not just the
count. All 12 CI gates pass.
2026-08-02 08:44:42 +00:00
9a14c0ccad refactor(spreadsheet-ui): dispatch formula edits through intents
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
2026-08-02 08:43:50 +00:00