Commit graph

769 commits

Author SHA1 Message Date
c4b646c1fa feat(makepad-table): editable header, currency and tax, doc switcher (Invoicer UI Phase 2)
Some checks failed
email.yml / feat(makepad-table): editable header, currency and tax, doc switcher (Invoicer UI Phase 2) (push) Failing after 0s
repo hygiene / hygiene (push) Has been cancelled
makepad-table / model (push) Has been cancelled
makepad-table / widget (push) Has been cancelled
makepad-table / hygiene (push) Has been cancelled
The invoicer displayed the document number, issue date and due date in three
`TextInput`s that were all `is_read_only: true`, because there was nowhere to
write an edit back to. This is the write half, plus the currency and tax
entry and the sidebar switcher the phase called for.

Model (`makepad-doc-model`):
- Setters for number, both dates, currency, default tax, issuer and
  recipient, each trimming its input. A trailing space in a document number
  becomes a trailing space in the exported filename, and a leading one makes
  two identical-looking documents sort apart.
- `secondary_date`, `set_secondary_date` and `secondary_date_label`, because
  the second date is a due date on an invoice and a valid-until on a quote,
  and a receipt has neither.
- `Currency::presets()` and `from_code()`. An unknown code is refused rather
  than turned into an `Other` with a guessed symbol and decimal count, which
  would format amounts confidently and wrongly.
- `TaxRate::parse_percent`.
- `switcher_label()`.

**A defect this uncovered.** `Document::default_tax()` returned `None` for a
receipt, even though `Receipt` carries a `default_tax` field like the other
two and its `tax_total_minor()` bills from it. The accessor was the only
thing claiming a receipt has no default rate. `document_to_table_data`
trusted it, substituted `TaxRate::zero()`, and printed 0% in the Tax column
for every un-overridden line while the totals underneath were computed from
the real rate — the table and the total disagreeing on the same screen.

It never showed because the shipped sample receipt is 0%-rated, so the wrong
answer and the right one coincided. It separates as soon as a rate is set,
which is exactly what the tax field added here now lets a user do. Fixed at
the accessor, so the table builder is corrected without touching it.

**A second one.** `TaxRate::percent` casts `f64 -> u32`, and that cast
saturates: `percent(-5.0, ..)` is 0%, and so is `percent(f64::NAN, ..)`.
Neither refuses, so a user typing nonsense into the new field would have got
a plausible-looking rate they did not ask for. `parse_percent` validates
first — finite, 0 to 100 — and returns `None` otherwise. Rejected input is
reported in the status line and the field is reset to the stored value, so
the box never keeps text the document did not accept.

UI:
- The three header inputs are editable, with a white background and a focus
  border rather than the read-only grey.
- The due-date field used to render "2024-05-01 (valid until)" for a quote —
  the label baked into the value, so it could not be edited without deleting
  the annotation. The label is now on the label.
- Currency and default-tax fields.
- Three sidebar buttons switch document, labelled from the documents
  themselves, with the current one named below.
- Header edits commit on Return or focus loss, not per keystroke: re-reading
  the model on each character fights the caret, and a half-typed date is not
  a date.

Also fixed, all pre-existing:
- `examples/table_demo` did not compile. It used `action.cast::<T>()`, which
  makepad's Action API no longer has. That package was in no workspace and
  had no CI until the previous commits, so it never failed loudly — it was
  simply never built. This is the second defect found purely by putting it
  somewhere a compiler would look.
- Both demos imported `makepad_widgets` alongside `makepad_table`, which
  re-exports it wholesale, making every widget name ambiguous.
- A dead `refresh_totals_display` no-op stub.
- Stale "Phase 3 will open PopupMenu" status strings; the menus exist.

doc-model tests 12 -> 22, and all five crates now pass
`clippy --all-targets -D warnings`. Verified by reintroducing three defects:
restoring `None` for a receipt's default tax fails the tax-reporting test,
letting `parse_percent` fall through to `percent` fails the validation test,
and dropping the trim fails the whitespace test.

Invoicer UI Phase 3 (file browser, recent documents, search) remains open.
2026-08-17 04:41:08 +00:00
a859053bc6 test(spreadsheet): cover remaining data.rs dependency-graph branches
Some checks failed
email.yml / test(spreadsheet): cover remaining data.rs dependency-graph branches (push) Failing after 0s
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
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
Targets the uncovered branches the coverage report named, around the
formula dependency graph and incremental recalculation:

- Formula replacement/removal edge cases: set_cell("") removes the
  cell and its edges, formula->value and formula->formula rewiring
  drop stale dependency edges.
- No-op removal paths: set_cell("") on a missing cell records nothing.
- Dependency-graph cleanup after formula deletion: remove_cell on a
  formula cell, and update_dependency_graph's defensive path when a
  dependent has no dependents entry.
- Affected-cell recalculation error paths: non-formula cells inside a
  cycle-affected set keep their raw value; the ="" empty-result
  regression; the recursive eval slow path (cached AST, parse
  fallback, and CycleDetected); parse_cell_computed_value arms; parse
  errors propagating through a dependent and through a large range.

Also covers named-range unary expansion (=-Total), the >64-cell range
fast path, non-numeric number-format fallbacks, and the demo_q3 /
demo_roi constructors.

Engine line coverage: data.rs 91.17% -> 97.90%; engine total
90.09% -> 93.24%. Unit tests 260 -> 279 (19 new); 9 integration tests
unchanged.
2026-08-17 04:40:28 +00:00
a88fb68eab fix(spreadsheet): B17 recalc invariant must not panic on empty-string results
`recalculate_incremental` treated "empty computed_value on a changed
formula cell" as proof that the topological sort dropped the cell, and
debug_asserted on it. But a formula may legitimately evaluate to the
empty string (`=""`, `=IF(FALSE, "x", "")`), leaving computed_value
empty through no fault of the dep-graph walk. In a debug build that
edit panicked the engine.

The invariant now checks what it actually meant to check: whether the
topological sort *visited* every changed formula cell, using the
`visited` set built from `sorted`. Emptiness is no longer used as the
error signal, so legitimate empty-string results flow through (the
display falls back to the raw formula text, as already documented for
empty computed values).
2026-08-17 04:40:28 +00:00
bd97e68af0 test(cad): opt-in reuse knobs so the coverage loop is usable while writing tests
Some checks failed
email.yml / test(cad): opt-in reuse knobs so the coverage loop is usable while writing tests (push) Failing after 0s
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
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
A cold run spends about 90 seconds installing a toolchain and another
minute compiling printpdf before it measures anything. That is correct
for CI and hostile to the person actually writing the tests, who runs it
twenty times in an afternoon — and the workaround is to hand-roll a
private copy of the harness, which then drifts from the committed one.
Both of the last two coverage pushes were done that way. Better to
support it.

Three opt-in variables, none set by CI:

  CAD_COV_TOOLCHAIN_HOME   reuse RUSTUP_HOME + CARGO_HOME
  CAD_COV_TARGET_DIR       reuse the build cache
  CAD_COV_MAKEPAD          reuse a Makepad checkout (already existed)

With all three: 20 seconds instead of three minutes, measured.

The default is unchanged and stays the only reproducible mode:
everything under one mktemp directory, removed by the trap. A reused
directory is deliberately NOT deleted — it lives outside $WORK by
definition, and silently removing a path the caller named would be a
nasty surprise the first time someone points it at the wrong thing.

The toolchain check is now "is there a cargo binary here", and a reused
home that was installed without llvm-tools-preview gets a message
naming the component and the rustup line to fix it, rather than a "no
such file" on llvm-profdata three steps later.

Verified both paths against this commit: hermetic cold run and
fully-reused run both report 96.86% and meet every floor.
2026-08-17 04:40:16 +00:00
723fe019d0 test(cad): the store's generation contract, the STL trait impl, and two NaN guards
Some checks failed
email.yml / test(cad): the store's generation contract, the STL trait impl, and two NaN guards (push) Failing after 0s
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
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
Mop-up of three files whose remaining gaps were small but not empty.
scene_holder 96.16 -> 100.00%, arch_stl 96.83 -> 98.88%,
construction_geometry 95.08 -> 95.88%.

scene_holder — the generation counter is the whole reason PartsStore
exists: it moves on its own so `SceneCache::scene_for` can tell whether
its cached scene is stale, instead of trusting every caller to remember
`mark_dirty()`. That contract is per-method and nothing checked it:

  - Reads must NOT bump. Eight of them (len, as_slice, iter, get,
    find_by_raw_id, index_of_raw_id, is_empty) asserted against one
    generation snapshot. A read that bumps rebuilds the scene every
    frame -- slow, and invisible.
  - `get_mut` bumps only on a hit. Bumping on a miss invalidates the
    cache for a lookup that changed nothing.
  - `iter_mut` bumps unconditionally, before it knows whether the
    caller writes. That is the deliberate conservative choice that
    replaced the `as_mut_vec()` escape hatch, and it is now pinned so
    nobody "optimises" it into a lie.
  - The pairing itself: an unchanged store returns the same Arc, a
    bumped one rebuilds and the rebuilt scene carries the edit.
  - `PartIdAllocator::default()` must agree with `new(1)`. Defaulting
    to 0 would hand out an id that reads as "no node".

arch_stl — only `build_stl` was covered, so the `Exporter` impl (the
path the export buttons and the async worker take) had never run. Both
arms now write the same bytes, both report a failed write, and a group
node contributes nothing an empty scene would not: meshing it would add
an empty solid and shift every later vertex index.

construction_geometry — the two non-finite guards in
`snap_to_polar_angle` and `normalize_angle_signed`. `rem_euclid` on an
infinity is a NaN, so without them an infinite drag delta becomes a NaN
heading and every vertex after it is NaN. Also the documented wrap-round
contract at the boundary: 370 degrees behaves as 10, -30 snaps to -45
rather than 315, and pi stays pi because the range is (-pi, pi].

Its remaining 20 uncovered lines are `other => panic!(...)` arms inside
existing tests. Those only execute when a test fails, so they are
uncoverable by construction rather than untested.

Floors: construction_geometry 92 -> 95, arch_stl 94 -> 98,
scene_holder 93 -> 99, total 95 -> 96.

Verified with: ./tools/test-cad-coverage.sh  (552 tests green, total 96.86%)
2026-08-17 04:38:04 +00:00
arena-agent
228bc2c81f ci(doc-engine): gate the engine coverage, and note it in the doc README
Some checks failed
email.yml / ci(doc-engine): gate the engine coverage, and note it in the doc README (push) Failing after 0s
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
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
doc-engine / engine (push) Has been cancelled
doc-engine / coverage (push) Has been cancelled
doc-engine / consumer (push) Has been cancelled
New coverage job runs tools/test-doc-engine-coverage.sh on changes to
crates/apps/doc/**, the script itself, or the workflow. A coverage
number nobody gates goes down; the floors (total plus per-file) are the
enforcement. The doc workspace README records the milestone and the two
CRDT-tolerance behaviors the new tests pin.
2026-08-17 04:33:08 +00:00
arena-agent
9d37874453 test(doc-engine): isolated source-coverage harness with floors
Mirror of the CAD engine harness for the doc crate, minus the shim
gymnastics (doc-engine depends only on serde/serde_json, so it
instruments directly): an isolated toolchain + cargo + target dir under
one mktemp directory, removed by a shell trap on every exit path;
nothing enters the host, the workspace target/, or $HOME. Runs the unit
tests plus tests/materialize.rs under -C instrument-coverage, enforces
a 96% total-lines floor against a 99.00% baseline plus per-file floors
(losing one module's tests must not hide in the total), and with
KEEP_COVERAGE=1 writes the uncovered-line listing that makes adding
branch tests directed rather than guesswork. COVERAGE.md records the
baseline, the exclusions, and the arms that are deliberately left
uncovered (defensive CRDT merge arms, one unreachable!, and the
Compensation::inverse arms unreachable through the public API).
2026-08-17 04:33:08 +00:00
arena-agent
1269811b44 test(doc-engine): cover the branches the first coverage report named
A first instrumented run (99.00% -> this branch set is what got it
there) showed the gaps precisely; these tests close the reachable ones:

- insert_text_at_offset / delete_text_at_offset: mid-block splices,
  prepend at 0, append at end, backward/forward deletion and every
  out-of-range guard (both fns were 0% covered).
- set_block_alignment materialization, including the CRDT-tolerance arm
  for a block whose op has not arrived.
- set_table_cells: multi-cell batch undoes and redoes as ONE group;
  empty write lists are rejected.
- CrdtDocument::to_json/from_json wire round trip (the format every
  workspace save rides on) was never exercised end to end.
- toggle_text_style_at_offsets rejects unknown fields and missing
  blocks; a style patch on an EMPTY block is retained rather than
  dropped.

Writing them inverted two expectations and the tests pin the actual --
and correct -- CRDT semantics instead: inserts/cell writes addressed to
anchors that have not arrived yet are ACCEPTED into the op log (they
must be, to merge when the anchor lands) while conjuring nothing into
the rendered document. 96 integration tests pass; clippy stays at -D
warnings clean.
2026-08-17 04:33:08 +00:00
ede451136b docs(cad): refresh the coverage table, 88.75% -> 96.53%
Some checks failed
email.yml / docs(cad): refresh the coverage table, 88.75% -> 96.53% (push) Failing after 0s
repo hygiene / hygiene (push) Has been cancelled
Six tranches since the table was written. It also gains the second
finding, which is quieter than the matrix bug and the same shape as it:
three of the four exporters had an arm per CadSolid variant and only
one variant had ever been walked through them. SVG had boxes, GLB had
boxes, the PDF projector had walls.

That failure mode is worth writing down rather than leaving in commit
messages. A part whose arm is wrong does not fail anything -- the export
succeeds, the file opens, and the column is not in it. All four
exporters are now driven over every variant they claim to support.
2026-08-17 04:32:52 +00:00
c5eefaaea4 feat(makepad-table): 3D cells (Phase 6)
Some checks failed
email.yml / feat(makepad-table): 3D cells (Phase 6) (push) Failing after 0s
repo hygiene / hygiene (push) Has been cancelled
makepad-table / model (push) Has been cancelled
makepad-table / widget (push) Has been cancelled
makepad-table / hygiene (push) Has been cancelled
The last of the README's table phases. `CellKind::Solid3d` reads a short
textual description of a solid from the cell and draws an isometric wireframe
of it:

    cube 10 20 30 / cube 10 / sphere 5 / cylinder 3 12

Separators may be spaces or commas, names are case-insensitive, `box` and
`cyl` are aliases. Text in, geometry out — the cell stays a `String`, so a 3D
column saves, loads and round-trips exactly like every other column, and the
kind travels with the column through a drag-reorder.

A wireframe rather than a shaded render, deliberately. Shading needs a 3D
pass with its own camera, depth buffer and lighting shader; the CAD viewport
elsewhere in this repo spends about 2,500 lines on precisely that. A cell
forty pixels tall gains nothing from it. Edges projected isometrically and
stroked with `DrawVector` need no pass of their own, and isometric has no
camera to configure and cannot degenerate. Curved solids are drawn as rings,
not their full triangulation: a 40px cell cannot resolve hundreds of
triangles and stroking them would cost more than the rest of the table.

The projection scales to fit and centres, so a 1-unit and a 1000-unit cube
are drawn identically — without that a cell shows either a dot or nothing.
Degenerate inputs (no edges, an inset larger than the cell, a zero-size cell,
geometry that collapses to a point) return nothing rather than dividing by a
zero span, because `DrawVector` silently drops a path containing NaN and the
cell would just look empty.

Dimensions must be finite and positive, and a rejected spec draws its reason
in the cell — `[unknown shape: torus]`, `[cube wants 3 args, got 2]`. Same
principle as the LaTeX path: an empty cell and a broken one must not look
identical, or a typo reads as a rendering fault.

Tests 26 -> 44. Parsing: each shape, uniform and three-dimension boxes,
aliases, case, comma separators, and every rejection path including NaN and
infinity. Wireframes: a cube has twelve edges and eight corners, extents are
centred, sphere vertices lie on the radius. Projection: fits inside the cell,
is scale-invariant, is centred, stays finite under extreme aspect ratios, and
returns nothing when degenerate.

Verified by reintroducing three defects: dropping dimension validation fails
1 test, a fixed scale instead of scale-to-fit fails 2, and removing the
re-centring fails 1.

That last one is the interesting case, because on the first attempt it failed
*nothing*. Every primitive is built centred on the origin, so the midpoint of
its projection is already zero and subtracting it is a no-op — the centring
tests could not distinguish "centres the drawing" from "happens to be
centred". `an_off_centre_solid_is_still_centred_in_its_cell` translates a
cube well away from the origin first, and that one does fail. A guard that
cannot fail is decoration, and this one could not until it was checked.
2026-08-17 04:32:32 +00:00
aad2a20d43 test(cad): cover the PDF plan projection, 75.92% -> 88.99%
Some checks failed
email.yml / test(cad): cover the PDF plan projection, 75.92% -> 88.99% (push) Failing after 0s
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
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
The last of the four exporters with the same gap: the arch projector
classifies each node by LAYER NAME and then by geometry, and only the
wall path had ever run. Columns, beams, spheres, generic blocks,
polygons, the 2-D primitives and CSG results all reached their own
`make_*` and none of them were executed, along with both public entry
points.

10 tests:

  - The plan projection negates Z so north is up. One line, and a sign
    error there mirrors the entire drawing.
  - Cylinders become Columns and spheres become Spheres, with centre,
    radius and height asserted, and the label prefix checked -- the
    labels carry a per-type counter that the drawing schedule reads.
  - A box on an unregistered layer falls back to a generic Block rather
    than vanishing. That fallback is what keeps an unclassified part on
    the drawing.
  - A beam keeps length on size.x, plan width on size.z and thickness
    on size.y. Swapping any two produces a plausible-looking beam of
    the wrong shape.
  - Polygons and extruded polygons are drawn as their bounding box
    centred on the polygon's own centre, not the node origin -- the
    node is at (2, 3) and the triangle's centre is offset from it, so
    the test would pass either way if it only checked the size.
  - An empty vertex list emits nothing, rather than a zero-by-zero
    block at the plan origin.
  - `export_scene_to_pdf` writes a real `%PDF-` file into a directory
    it had to create, and reports a path it cannot create.

The tolerances in this module are 1e-6 rather than 1e-9 on purpose:
every dimension crosses f32 to f64 on the way in, and 0.3f32 as f64 is
0.30000001192092896. The first draft used 1e-9 and failed on the beam.

Floors: arch_pdf 72 -> 86, total 94 -> 95. The remaining 152 lines are
the printpdf emitter itself -- page furniture, dimension strings and
title-block layout, whose output is only meaningfully checked by
opening the file.

Verified with: ./tools/test-cad-coverage.sh  (540 tests green, total 96.53%)
2026-08-17 04:31:44 +00:00
2a74c6cac4 ci(email): gate the keystore feature, cover email_bulk
Some checks failed
email.yml / ci(email): gate the keystore feature, cover email_bulk (push) Failing after 0s
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
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
doc-engine / engine (push) Has been cancelled
doc-engine / consumer (push) Has been cancelled
nigig-map / test (push) Has been cancelled
sms / gates (push) Has been cancelled
sms / robius-sms (push) Has been cancelled
sms / android (push) Has been cancelled
sms / nigig-sms (push) Has been cancelled
sms / supply-chain (push) Has been cancelled
email.yml: the feature-compile check now covers imap,keystore together.
test-email-coverage.sh instruments email_bulk.rs (91.9% line) alongside the
rest of the domain; total 89.84%, floors enforced.

The review doc records C6/C7/C1f as fully closed, with the honest caveats
unchanged (network sockets and the OS vault are compile-checked, not
runtime-verified).
2026-08-17 04:29:30 +00:00
595ad6ad24 feat(email): pull-to-refresh (C7) and a real OS keystore (C1f)
C7: pull-to-refresh on the inbox, mirroring the SMS/M-Pesa transaction
lists (scrolled + scroll_position over a threshold, throttled to 1.2s and
guarded by the in-flight flag). The Refresh button remains for platforms
without a gesture.

C1f: a real KeyringCredentialStore behind the keystore feature -- the OS
credential vault (Linux Secret Service, Windows Credential Manager, macOS
Keychain) via the keyring crate, so IMAP credentials can survive a restart.
Native only; without the feature active_store() stays fail-closed. The
runtime vault is not host-verified (no secret service in CI), which is the
same honest caveat as the IMAP transport.
2026-08-17 04:29:30 +00:00
765e178737 feat(email): paced bulk send — batch and pace large recipient lists (C6)
The Bulk tab was capped at MAX_RECIPIENTS (100): a 500-recipient list was
refused with TooManyRecipients, not paced. That is a capped single send,
not bulk.

email_bulk.rs: bulk_send_plan splits a list into provider-sized batches
with a pacing schedule (pure, tested), and run_bulk_send executes the plan
— gap between batches, rate-limiter backstop, abandon check between every
step, per-batch progress. Tested against a mock send (batching, delays,
failed-batch counting, abandon).

email_send.rs: validate_bulk_message accepts a list over the cap (the
caller batches it) while still enforcing subject/body limits.

The Bulk page now sends <=100 recipients as one message and anything over
as paced batches, posting BulkSendProgress after each batch and at the end.
Domain tests 195 -> 206.
2026-08-17 04:29:30 +00:00
5e864498d5 test(cad): cover the GLB export entry points and every solid it collects, 75.48% -> 94.83%
Some checks failed
email.yml / test(cad): cover the GLB export entry points and every solid it collects, 75.48% -> 94.83% (push) Failing after 0s
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
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
Same shape of gap as the SVG exporter, one file over. The mesh
collector had an arm per solid and only boxes were ever walked through
it; `scene_to_glb`, `export_scene_to_glb`,
`export_scene_to_glb_with_cache` and `build_glb_sequential` -- every
public entry point except the one the trait impl uses -- were at zero,
along with both `From` conversions on the error type.

8 tests:

  - Nine solids exported one at a time, each asserted to produce a
    structurally valid GLB: the "glTF" magic, version 2, and a declared
    length that matches the file. A viewer rejects the file outright if
    any of those disagree, so checking "some bytes came back" would not
    have been worth writing.
  - The sequential fallback is asserted byte-identical to the parallel
    builder. It is documented as the path for environments without
    rayon; if it drifts, that fallback silently exports something else
    and only those environments see it.
  - An empty scene and a groups-only scene are both refused, with the
    two distinct messages. A GLB that opens to an empty stage is worse
    than a refusal, because the user reads it as "the export worked".
  - `export_scene_to_glb` creates the directory it was pointed at (the
    user picks the path, its parent may not exist), the shared-cache
    variant writes identical bytes, and both report a path they cannot
    create instead of dropping the export.
  - The error type's Display, plus its io and serde_json `From`
    conversions -- those exist so `?` works inside the export path, and
    an unexercised conversion is a `?` that fails to compile the day
    someone needs it.

Floors: arch_gltf 72 -> 92, total 93 -> 94.

Verified with: ./tools/test-cad-coverage.sh  (530 tests green, total 95.42%)
2026-08-17 04:29:19 +00:00
e16a6da5f5 test(cad): cover the real command context and the undo-stack housekeeping, 87.63% -> 96.23%
Some checks failed
email.yml / test(cad): cover the real command context and the undo-stack housekeeping, 87.63% -> 96.23% (push) Failing after 0s
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
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
The undo/redo tests all ran against a mock. The mock keeps its own Vec,
so `CadCommandCtx` -- the implementation the editor actually uses --
had two methods that no test had ever called: `update_node`, the
in-place property edit, and `insert_node_at`, the undo of a delete.
`UndoRedoStack::clear`, its Debug impl, and the describe/as_any pair on
half the command types were also at zero.

12 tests against the real context, with a real PartsStore:

  - `update_node` edits in place, bumps the store generation, and does
    NOT reorder the list. Order is what the layer panel and the draw
    order read; the same class of reordering defect is already called
    out in the mock's own comment.
  - `update_node` on a missing id reports NodeNotFound and does not run
    the edit closure -- otherwise a stale selection edits whatever node
    happens to be in that slot.
  - `insert_node_at` puts a deleted node back at its recorded index,
    not on the end, and clamps an out-of-range index instead of
    panicking. The index is captured before the delete and other
    commands may have shortened the list since.
  - DeleteNode and CreateNode are round-tripped through the real
    context, including DeleteNode's no-recorded-index arm (appends) and
    CreateNode's fallback from `assigned_id` to the snapshot id.

Plus the trait and stack housekeeping:

  - The `Command` defaults: the generic "command" label, and
    `can_merge` returning false. A default of true would silently
    collapse unrelated undo steps.
  - `clear()` empties both stacks. The editor calls it when a document
    is closed; an entry surviving into the next document applies an
    edit to the wrong model.
  - The Debug impl prints depths and asserts the command list is NOT
    dumped -- a derived Debug over two stacks of boxed trait objects
    would put the whole edit history in a log line.
  - Every command type's describe/as_any, including that two commands
    with identical field shapes do not downcast into each other. That
    downcast is what `can_merge` runs on; a wrong one turns a drag into
    one undo entry per frame.

Floors: commands 85 -> 94, total 92 -> 93.

Verified with: ./tools/test-cad-coverage.sh  (523 tests green, total 94.31%)
2026-08-17 04:27:54 +00:00
15ef8a0447 feat(makepad-table): LaTeX cells (Phase 5)
Some checks failed
email.yml / feat(makepad-table): LaTeX cells (Phase 5) (push) Failing after 0s
repo hygiene / hygiene (push) Has been cancelled
makepad-table / model (push) Has been cancelled
makepad-table / widget (push) Has been cancelled
makepad-table / hygiene (push) Has been cancelled
A column can now be declared as maths rather than text:

    TableColumn { kind: CellKind::Latex, ..Default::default() }

Rendering goes through makepad's own `MathView`, which is already registered
with the script VM and already owns a glyph cache and the `makepad-latex-math`
parse/layout path. Reimplementing that inside the table would have been a
second copy of the same logic with none of the same testing.

The kind lives on the column, not the cell. Cells stay `String`, so a
`TableData` built before this existed keeps working and a table still
round-trips through plain text. A column of formulae is also the realistic
case — a spreadsheet does not mix prose and LaTeX down one column — and it
means the decision is made once per column rather than re-derived per cell
per frame. `CellKind` defaults to `Text`, so nothing changes for existing
callers except that the struct gained a field.

One `MathView` is repositioned over each maths cell in turn, the same pattern
`cell_editor` already uses. A widget per cell would allocate a glyph cache per
cell. The walk is `Size::Fit` rather than fixed to the cell, because
stretching a glyph run to fill a cell distorts the maths.

An expression that does not parse is not blanked. `MathView` draws its own
`[reason]` marker, so a mistyped formula is visible in the cell rather than
silently erasing the content — the failure mode that makes a formula column
untrustworthy.

Also extracted `align_text_x`, which was inline in `draw_cells`. It counts
characters rather than bytes; a multi-byte string measured with `len()` is
pushed off the cell entirely. The 7px-per-character approximation is
unchanged and still crude — correcting it needs a real text measurer, not a
different guess — but it is now in one place and under test, so replacing it
will be a visible diff rather than a silent shift.

Tests 21 -> 26. New: kinds default to Text, a Latex column keeps its kind
through a drag-reorder, left alignment ignores content, centre and right
alignment against the stated approximation, and character-vs-byte counting.
Verified by reintroducing two defects: measuring bytes fails the multi-byte
test, and resetting kind during a reorder fails the kind-preservation test.

The invoicer's six columns gained an explicit `kind`. That break was caught
by the `Invoicer and demo compile` step added with the workflow in the
previous commit, which is what it is there for.
2026-08-17 04:27:12 +00:00
44bf7da725 test(cad): cover the shapes the SVG exporter never drew in a test, 82.48% -> 99.02%
Some checks failed
email.yml / test(cad): cover the shapes the SVG exporter never drew in a test, 82.48% -> 99.02% (push) Failing after 0s
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
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
Only boxes were exercised. Cylinders, spheres, circles, arcs, polygons,
extruded polygons and CSG results all had their own arm in the SVG
visitor and not one of them was executed — 110 uncovered lines, in the
exporter that produces the construction drawing.

That is the worst shape for an exporter bug: a part whose arm is wrong
does not fail anything, it just is not in the drawing. Nothing is red,
the file opens, and the column is missing.

13 tests:

  - Every drawable variant is exported on its own and must produce
    exactly one path. Nine variants, nine assertions.
  - A round outline has one point per segment, and a sphere is drawn
    from segments_u, not segments_v. Both show up visually as a column
    faceted in the wrong axis rather than as an error.
  - An arc is sampled inclusively across its 32 segments (33 points) so
    it closes on the end angle instead of stopping a step short, and a
    half sweep must not return to its start.
  - A polygon with two vertices, and an empty CSG result, add no path.
    An empty `points=""` renders as a stray dot in some viewers.
  - The `Exporter` impl itself: the cacheless `export`, the cached one
    (asserted byte-identical), and the write-failure arm, whose message
    is what the status label shows. Only `build_svg` was covered
    before, so a broken `export` would have shipped.
  - A 90 degree yaw must change the projected outline of a polygon.
    The box arm had rotation covered; the polygonal and round arms use
    a different projection helper and had none.

Floors: arch_svg 79 -> 97, total 89 -> 92.

Verified with: ./tools/test-cad-coverage.sh  (510 tests green, total 93.44%)
2026-08-17 04:25:04 +00:00
a82916b006 test(cad): cover the scene-graph builder API, 81.73% -> 98.53%
Some checks failed
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
email.yml / test(cad): cover the scene-graph builder API, 81.73% -> 98.53% (push) Failing after 0s
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
cad_scene.rs is the type every other CAD file is written against, and
388 of its lines had never been executed. The gap was not in exotic
corners -- it was the fluent builder the whole editor and the script VM
construct scenes through. `NodeBuilder`'s setters, `SceneBuilder`'s
starters and domain builders, `push_raw`, `CadTransform`'s helpers, the
2-D solids' size/set_size arms, `ParamHash` over half the variants,
`walk_scene`'s dispatch, and the `Exporter` default methods were all at
zero.

32 tests, grouped by what they protect:

  - The setters are checked for what they must NOT touch as well as
    what they set. `.cube().radius(9.0)` has to be a no-op, not a
    silent solid swap; `.rotate_y(30).rotate_y(15)` has to be 45
    degrees, because every one of these helpers is additive and a
    helper that assigned instead would drop the earlier call.
  - The domain sugar is pinned to its documented axes: length/width to
    size.x, domain_height to size.y, thickness/depth to size.z. Getting
    one onto the wrong axis gives a wall 0.2 m long and 6 m thick,
    which reads as a modelling mistake rather than a code one.
  - The six domain builders are checked for layer, name and their
    documented default colour. Asserting the colour rather than "not
    the default material" is deliberate and was found the hard way:
    Column's grey IS the default colour, so it correctly shares the
    default material instead of registering a duplicate.
  - `set_size` is checked on every parametric solid. It is what the
    properties panel calls, and a missing arm is a control that does
    nothing -- the same class of defect the by-value-getter CI gate
    already guards.
  - `size()` on a CSG or extruded solid is checked against a real mesh
    bounding box, including the empty-result case. That arm used to
    return a hardcoded 1x1x1, which made those parts unpickable outside
    a 1 m box at their origin.
  - `ParamHash` is checked to move for every field of the 2-D and
    section variants. It keys the mesh cache AND the viewport's GPU
    buffers, so a field it does not hash is an edit that leaves stale
    geometry on screen.
  - `walk_scene` is checked to route all twelve `CadSolid` variants to
    their own callback, in order, with `leave_node` always firing. The
    exporters are all visitors: a variant landing in the wrong arm is a
    part that silently vanishes from the STL, the SVG or the PDF. A
    visitor overriding nothing is walked too, so the trait's default
    bodies are executed rather than assumed.
  - `export_to_vec`, `export_with_cache` and `spawn_export` -- the
    default methods an exporter gets for free, all on the async export
    path -- are driven through a counting stub, with the worker thread
    joined so the callback assertion is deterministic.

Floors raised to lock it in: cad_scene 78 -> 96, total 85 -> 89.
Remaining 44 lines are small accessors and defensive arms.

Verified with: ./tools/test-cad-coverage.sh  (499 tests green, total 92.44%)
2026-08-17 04:23:24 +00:00
ab17c72c55 feat(makepad-table): drag-reorder columns, and the first tests this crate has
Some checks failed
email.yml / feat(makepad-table): drag-reorder columns, and the first tests this crate has (push) Failing after 0s
repo hygiene / hygiene (push) Has been cancelled
makepad-table / model (push) Has been cancelled
makepad-table / widget (push) Has been cancelled
makepad-table / hygiene (push) Has been cancelled
Phase 4 of the README's table, plus the test infrastructure Phases 1-3 never
had. The crate had zero tests before this; it now has 21.

Drag-reorder:

- A press on a column header no longer commits to an action. It arms a drag
  and resolves on release: travel more than 8px and it reorders, release
  without travelling and it opens the column menu as before. Without that
  ambiguity resolved, every menu open would jitter into a one-pixel drag.
  The threshold matches `TouchTracker::MOVE_THRESHOLD` so a mouse and a
  finger agree on what a drag is.
- While dragging, the carried column is tinted full-height and a 2px bar
  marks the boundary it would land on. The bar is suppressed when the drop
  is a no-op, so no bar means nothing will happen rather than a bar sitting
  misleadingly at the source edge.
- `TableAction::ColumnMoved { from, to }` fires only when the index actually
  changed, so a host persisting column order is not asked to write on every
  wobble. An open cell editor is cancelled, because it addresses a cell by
  index and the indices just moved underneath it.

`draw_drag: DrawVector` — declared, never used anywhere — is replaced by two
`DrawColor` layers. `DrawVector` is a full tessellator with path, vertex,
index and paint state; a translucent rectangle and a vertical bar do not
need any of it.

Testability, which needed a structural change rather than a test file:

`Table` derives `Script` and `Widget`, so it has no `Default` and cannot be
constructed without a live `Cx`. Nothing about it was unit-testable. The
logic worth testing does not need a widget, so it moved off it —
`ColumnGeometry` owns boundary and drop-position arithmetic, and a free
`reorder_columns` owns the move. `Table` forwards to both, and
`compute_layout` now goes through `ColumnGeometry` too, so there is one
implementation rather than two that can drift.

The 21 tests cover column geometry at even and uneven widths and at a
non-zero origin, drop-position resolution including the exact-midpoint case
and clamping outside the table, the index shift in both directions, no-op
drops, out-of-range refusal, cells travelling with their header, ragged
rows, a permutation property over repeated drags, and the Phase 3 menu's
geometry and hit-testing.

Verified by reintroducing three defects separately: removing the shift for
the removed source column fails 7 tests, dropping the no-op guard fails 1,
and moving headers without their cells fails 3.

Phase 3 was marked "scaffolds only" in the README and was in fact
substantially complete — menu state, open, hit-test, apply, and drawing all
present, with 15 row and column actions wired. Corrected to done, with its
geometry now under test.

Also adds `.forgejo/workflows/makepad-table.yml`, the first CI this tree has
had. Every step passes `--manifest-path` explicitly: the crate is excluded
from the root workspace, so `-p` from the repo root cannot reach it and
`--workspace` skips it — omitting the flag does not fail loudly, it silently
tests nothing. The workflow gates tests, clippy at `-D warnings` and fmt,
and asserts three invariants that would otherwise regress quietly: that the
exclusion still holds from both sides, that no manifest tracks a git branch
instead of pinning a revision, and that monetary fields stay integer.

Each gate was checked by breaking what it protects. The exclusion check
caught a defect in itself while being tested: a bare grep for the path also
matched the explanatory comment above the exclude list, so deleting the
entry and keeping the comment passed. It now anchors on the quoted entry.

Two pre-existing clippy warnings fixed so the new `-D warnings` gate starts
from zero.
2026-08-17 04:22:22 +00:00
arena-agent
d62cc13d34 style(nigig-build): cargo fmt the two test targets left unformatted
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
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
email.yml / style(nigig-build): cargo fmt the two test targets left unformatted (push) Failing after 0s
tests/ui.rs and tests/cost_estimator.rs drifted out of rustfmt shape
in the recent feature/merge series, failing the whole-crate fmt gate
(cargo fmt -p nigig-build -- --check). Mechanical reformat only.
2026-08-17 04:17:36 +00:00
arena-agent
ff0cba1b80 fix(project): replace retired SpreadsheetGrid::select_cell with set_selection_pair
spreadsheet-ui 6a3c467 removed the direct selection helper, leaving
apply_spreadsheet_op calling a method that no longer exists — the
nigig-build lib (and therefore every cargo gate) stopped compiling.
set_selection_pair((r, c), (r, c)) is the API the same migration
series already adopted (8fe7db2); both call sites redraw the view
right after, covering the paint the removed helper used to trigger.
2026-08-17 04:17:36 +00:00
arena-agent
5e5adf962d fix(doc): boot the CRDT editor with content and migrate persistence to the app-data store
The Android APK (pageflipnav) booted the doc workspace to a blank page.
Two compounding causes, both invisible to sandbox gates:

- CrdtDocEditor (the active editor since the navigation switch) had no
  boot init: it starts from DocumentController::default() and only the
  legacy DocEditor seeded the showcase document behind its initialized
  gate. The first event on a factory-fresh editor now runs
  init_document: load the on-disk save when it decodes as #MP_CRDT_V1
  wire (initial_document_source gates that so classic-format saves stay
  with the legacy workspace's first-edit migration), otherwise
  seed_demo_doc builds a CRDT mirror of demo_doc_blocks() -- styled
  headings, accent runs, divider, image node, the 4x3 table with bold
  header, and the closing hint. set_engine flips the same flag so a
  host-installed document is never overwritten.
- persistence.rs resolved its save file under
  env!("CARGO_MANIFEST_DIR"), baking the build machine's absolute
  source path into the binary; on device that path does not exist, so
  Open read nothing and Save wrote nowhere (the errors were swallowed),
  and on desktop the app polluted its own checkout. Writes now go only
  to app_data_dir()/nigig_build_store/generated/current.doc.json (the
  crate-wide convention the CAD store already uses); reads keep a
  one-way fallback to the legacy source-tree file so an unreplicated
  developer save is honored once. Boot and migration emit [DOC_TRACE]
  lines so a device logcat session names the branch that fired.

Tests (8 new): boot-source gate (CRDT wire boots verbatim; classic JSON
and None route to the demo seed), runtime boot on first event
(source-agnostic non-empty projection + flag), host-installed-engine
no-overwrite guard, full structural assertion of the seeded showcase,
and four temp-dir persistence tests (round trip, store-beats-manifest
precedence, manifest fallback, empty-file rejection).
DEVICE_VERIFICATION.md gains the matching section-9 hardware rows (9.0
fresh-install demo boot, 9.3 classic-save coexistence).
2026-08-17 04:17:36 +00:00
ea98d4d95c fix(makepad-table): exclude from the workspace, pin makepad, fix money defects
Some checks failed
email.yml / fix(makepad-table): exclude from the workspace, pin makepad, fix money defects (push) Failing after 0s
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
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
doc-engine / engine (push) Has been cancelled
doc-engine / consumer (push) Has been cancelled
nigig-map / test (push) Has been cancelled
sms / gates (push) Has been cancelled
sms / robius-sms (push) Has been cancelled
sms / android (push) Has been cancelled
sms / nigig-sms (push) Has been cancelled
sms / supply-chain (push) Has been cancelled
`crates/apps/makepad_table` is a nested workspace that has never been
compiled — its README says as much: "written without a local cargo/rust
toolchain, so the first compile on your machine is the verification step."
This is that step. Four crates, all of which build, and three defects in
the money code that only a compiler and a test runner could have found.

Workspace containment, which is what was asked for:

- The root manifest now names `crates/apps/makepad_table` in `exclude`.
  Cargo already declined to absorb it, because the crate carries its own
  `[workspace]` table — but that made the isolation a property of someone
  else's manifest. Deleting that table would have pulled four crates and a
  second makepad checkout into every workspace-wide build. Verified: the
  root workspace resolves 58 members and none of them are these.

- `examples/table_demo` belonged to no workspace at all and had no
  `[workspace]` table of its own, so `cargo metadata` failed outright in
  that directory. It is now a member of the nested workspace. Kept rather
  than deleted: it is the template the README's "drop into makepad"
  section refers to.

- All three manifests pinned to the fork revision the rest of the repo
  uses (`gitdab.com/andodeki/makepad` @ ecf5a57) instead of tracking
  `github.com/makepad/makepad` branch `dev`. A floating branch means the
  same commit of this repo builds against a different makepad from one day
  to the next, and against a different makepad from every other crate
  here. All four crates verified to compile against the pin.

The defects, in the order they surfaced — each was hidden by the one
before it:

1. `format_with_thousands` computed `(i - first_group_len)` before the
   `i >= first_group_len` guard that protects it. `&&` short-circuits left
   to right, so the check never ran in time. Any number whose leading
   group is short of three digits — 2, 3, 5, 6, 8, 9, 11, 12 digits wide —
   underflowed a usize: a panic in debug, silent wrapping and misplaced
   commas in release. Every currency string in the application went
   through it. The two existing tests used 1234 and 1234567, the two
   widths that happen to work.

2. With the panic gone, `Currency::format` was visibly wrong on negatives.
   The symbol was emitted before a signed whole part, giving "$-12.34"
   instead of "-$12.34"; and `whole` truncated toward zero while `frac`
   used `rem_euclid`, so the two disagreed below zero. -1234 formatted as
   "$-12.66" and -1 as "$0.99" — the wrong sign, the wrong place, and the
   wrong amount.

3. `invoice_totals_arithmetic` asserted `1_840_00` where the sample data
   totals 1_840_000 minor units. The prose in the same comment said
   18,400.00, which is right; the literals were a factor of ten low. The
   arithmetic was never wrong, the expectations were. The two loose range
   assertions on tax and grand total are now exact equalities.

Tests 12 -> 16 across the two crates, and all 16 pass; previously 6 of 12
failed. Each fix was verified by reintroducing the defect on its own:
the guard-order bug fails 5 tests, the sign bug fails 4 with the overflow
fix left in place, and dropping the per-line discount from the tax
calculation fails the arithmetic test by 71.32 — an error the old range
assertions were wide enough to have accepted.
2026-08-17 04:10:26 +00:00
5cb1bfe9f3 test(spreadsheet): cover the branches the report named
Some checks failed
email.yml / test(spreadsheet): cover the branches the report named (push) Failing after 0s
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
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
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
Nine tests against `workbook_api.rs`, the weakest file in the engine at
77.80%. Now 88.80%; engine total 88.97% -> 90.09%, tests 259 -> 260.

One of them pins a trap rather than a bug. `set_cell` calls
`begin_recording()` before `apply()`; the other eight public mutators
(`remove_cell`, `put_cell`, `set_col_width`, `set_row_height`,
`toggle_bold`, `set_number_format`, `set_alignment`, `set_bg_color`) do
not. Calling one of those directly and then `undo()` reverts the
*previous* recorded edit, not the one just made.

Nothing ships broken: every `spreadsheet-ui` call site takes its own
`data.snapshot()` first, checked one by one in `grid.rs`. But the
asymmetry is invisible at the call site and the next caller will not know
to snapshot. `only_set_cell_records_its_own_undo_step` states the current
contract so a change to it is a deliberate decision rather than an
accident.
2026-08-17 04:05:36 +00:00
83839ea0a3 test(spreadsheet): coverage for the UI controllers, and fix a 0% report
The coverage script measured the engine only, and it cherry-picked four
source files to report on, which flattered the number: 91.15% against a
hand-picked subset versus 88.97% for the whole of `src/`.

Rewritten to cover both crates honestly, with per-crate floors and a
listing of uncovered lines. Two bugs in the script itself:

- The ignore regex contained the work-directory name, so it excluded the
  very sources being measured and reported a confident 0%. The work dir
  also cannot live inside the repo, or Cargo treats the copied crates as
  workspace members and refuses to build them.
- `llvm-cov show` filename headers carry no trailing colon, so the awk
  matcher never fired and the uncovered-line listing was always empty.

`spreadsheet-ui/src/{grid,ui,workspace}.rs` and `src/bin/` are excluded:
the first three are `script_mod!` generated DSL and the last is desktop
startup, neither of which a unit test can reach.

UI controllers now measure 94.55%: `event_router.rs` 70.59% -> 97.96%,
`selection.rs` 80.65% -> 100%. UI tests 9 -> 17.
2026-08-17 04:05:29 +00:00
6f05c47f20 fix(pay): restore visible:false on pin_input, and gate it (0.3 regression)
An upstream commit removed `visible: false` from `pin_input` in the shared
pay sheet while leaving it on `pin_eye_btn`. Every existing gate still
passed, because they all probe the *compile* surface: they prove the field
is absent from a packaging build. None of them read the DSL, where the
field legitimately exists in a default build and the hiding is what keeps
the control off screen until a demo build unhides it on init.

That is the DSL-reload hole review item 0.3 asks to close: a live reload
re-reads the DSL, so a control that is visible by default there is visible
on screen regardless of what init did.

`check-no-pin-capture.sh` now walks the DSL for both PIN controls before
it runs the compile probe. Verified it fails on the unfixed sheet and
passes on the fixed one.
2026-08-17 04:05:21 +00:00
8701f5df51 feat(pdf): image embedding and header/footer stamping — Phase 4 complete
Some checks failed
email.yml / feat(pdf): image embedding and header/footer stamping — Phase 4 complete (push) Failing after 0s
repo hygiene / hygiene (push) Has been cancelled
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (push) Has been cancelled
The last gap in Phase 4: dart-pdf's header_footer_test, image_stamp_test
and image_pdf_test had no counterpart here.

What was missing is worth stating precisely, because it is the shape of
bug ADR 0017 exists to catch. ContentWriter::draw_image has emitted
`q w 0 0 h x y cm /Name Do Q` since Phase 2, and was tested. But nothing
in the stack could *create* the image XObject that /Name resolves to. So
every Do operator ever written named a resource that did not exist, no
document could contain a raster image, and nothing anywhere returned an
error. The writing half was present, the reading half faithfully
reported the content stream, and the image was simply never there.

stamp.rs adds: image XObject embedding, header/footer banners with
left/centre/right alignment, image stamp content, and stream
composition. A JPEG is embedded as-is with /DCTDecode — PDF's image
model is the same DCT data the file already holds, so re-encoding would
lose quality for nothing — and its geometry is read from its own SOF
marker rather than trusted from the caller, because a /Width that
disagrees with the codestream renders as diagonal garbage in every
viewer. Raw samples embed as Flate.

Embedding an image then adding the page that draws it exposed a live
defect in PdfDocBuilder. add_object derived its number from
`3 + 2 * pages.len()`, so every add_page after an add_object silently
shifted a number already handed out. Embedding an image and then adding
its page — the natural order, since the page's content stream has to
name the image — produced a page whose /XObject entry pointed at the
page object itself:

  3 0 obj <</Type /Page ... /XObject <</Im0 3 0 R>>>>

The file parsed. The reference resolved. The resource was the page.

This is the same positional-numbering defect already fixed once for
fonts, one layer out — the comment above first_extra_object_number
describes the font version, where /ToUnicode pointed at the descriptor
and /FontFile2 at the Type0 wrapper. Both come from deriving object
numbers from collections that are still growing. Fixed at the root: the
page count is frozen when the first extra number is issued, and pages
added afterwards are allocated past the fixed block instead of
colliding with it. Non-contiguous page numbers are legal — /Kids is an
explicit array — and 952 tests confirm nothing depended on the order.

The integration tests parse the generated file back with PdfDocument and
assert the image appears in `page.xobjects` with subtype Image, that its
/Width and /Height match the SOF marker, and that the header and footer
baselines are at opposite ends of the page. Reading the resource back is
the assertion that matters: a substring check for "/Im0 Do" passed
throughout the entire period when no image could be embedded at all.

Verified by mutation, five injected defects, each confirmed red:

  numbering fix reverted        4 fail
  JPEG width/height transposed  5 fail
  header positioned from bottom 3 fail
  sample-count check removed    1 fail
  attach_image_to_page a no-op  5 fail

One test needed correcting rather than the code: three assertions
grepped the output for operators, which are Flate-compressed by default,
so they were asserting against compressed bytes. They now disable
compression explicitly — the structure is identical either way, and the
alternative was a test of miniz_oxide.

Engine suite 920 -> 952. Coverage 86.16% -> 86.40%; stamp.rs at 94.64%
with a floor at 90.

Phase 4 is complete and the plan records it, including the numbering
defect, since a status table that lists only features would not have
told the next reader why the object numbers look the way they do.
2026-08-16 22:27:32 +00:00
2770716516 docs(cad): record the engine coverage baseline and what it excludes
Some checks failed
email.yml / docs(cad): record the engine coverage baseline and what it excludes (push) Failing after 0s
repo hygiene / hygiene (push) Has been cancelled
TEST_BASELINE.md said "750 passed / 0 failed" and stopped there. A pass
count says the suite is green; it says nothing about what the suite
touches, and this module shipped a broken matrix inverse under 750 green
tests.

Adds the per-file table, the enforced floors, and -- the part that
matters -- the list of what is NOT measured. Twelve files, roughly
18,000 lines of widget code, have no coverage number at all. Reading
88.75% as "the CAD module is 88.75% covered" would be wrong: it is the
engine that is, and the engine is the smaller half. Better to write that
down than to let the number be quoted without it.
2026-08-16 22:27:22 +00:00
1c91d6b398 ci(cad): gate the engine coverage, with per-file floors
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
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
email.yml / ci(cad): gate the engine coverage, with per-file floors (push) Failing after 0s
repo hygiene / hygiene (push) Has been cancelled
The harness measured; nothing enforced. A coverage number nobody gates
goes down.

tools/test-cad-coverage.sh now exports llvm-cov JSON and fails when the
total drops below 85% or any of the fourteen engine files drops below
its own floor. The per-file floors are the point: deleting every test in
persistence.rs moves the total by under two points, so a single number
would wave that through. Each floor sits a couple of points under
today's measurement, so refactoring does not trip it and a real loss
does.

The low floors are the honest ones. arch_pdf (72) and arch_gltf (72)
have gaps in byte-layout paths that only a real PDF or GLB consumer
reaches; arch_svg (79) and cad_scene (78) have gaps in widget-facing
helpers and defensive arms on invariants SceneBuilder already enforces;
exporters (88) cannot reach the save-dialog branch without a windowing
system. Raising those needs work, not a bigger number here.

Also in this commit, from running the script the way CI will rather than
with a warm local checkout:

  - the Makepad fetch is sparse + blobless + depth 1 over the actual
    path-dependency closure (math, csg and its six siblings,
    micro_serde, its derive, micro_proc_macro, live_id, id_macros).
    29 MB and two seconds instead of a 319 MB checkout of a repository
    that is mostly shaders, fonts and demos. Two of those crates were
    found by the run failing at manifest-read time, which is why the
    script now verifies all thirteen manifests exist before building
    instead of trusting the sparse pattern.

The new cad-engine-coverage job needs no native packages and no GPU --
makepad-math and makepad-csg are dependency-free Rust, which is the
whole reason the engine can be measured at all. It installs its own
toolchain into a temp dir and deletes everything through a shell trap:
nothing cached between runs, nothing left in the workspace.

Verified end to end with a cold run: fresh toolchain, fresh sparse
fetch, 466 tests green, total 88.75%, all floors met, environment
cleaned.
2026-08-16 22:26:42 +00:00
b87d8b0762 test(email): coverage over the full domain; IMAP feature gate in CI
Some checks failed
email.yml / test(email): coverage over the full domain; IMAP feature gate in CI (push) Failing after 0s
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) Has been cancelled
doc-engine / consumer (push) Has been cancelled
nigig-map / test (push) Has been cancelled
sms / gates (push) Has been cancelled
sms / robius-sms (push) Has been cancelled
sms / android (push) Has been cancelled
sms / nigig-sms (push) Has been cancelled
sms / supply-chain (push) Has been cancelled
tools/test-email-coverage.sh now instruments all twelve email files
(the new pacing, credential-store, cache, session and imap modules) and
enforces per-file floors; measured 90.7% line coverage over the domain.

email.yml: the domain test filter gains imap_client::/credential_store::,
the test floor ratchets 150 -> 190, the sample-data gate is now a hard
zero (sample_thread is test-only), and a new step checks the feature-gated
IMAP transport still compiles.

The review doc marks Phase C and Phase D complete with the honest
caveats (sockets/keystore/pool-reuse are not host-verified).
2026-08-16 22:22:55 +00:00
32b8decc1e feat(email): real inbox, compose and More pages (C4b/C5/C7, D1/D2/D3)
C4b: the inbox fetches real mail via spawn_fetch_inbox (backend-agnostic)
and renders loading / error / empty states; sample_thread() is test-only.
C5: Compose is a real form -- to/subject/body, build_without_config
validation, two-tap confirm, spawn_send_message (branches on backend).
C7: a Refresh button re-fetches (worker -> InboxFetched -> drained on the
UI thread).

D1: the last placeholder, more.rs, is now the account page (status,
backend, sign-out) -- there is no duplicated scaffold left to extract.
D2: the lib.rs compatibility shims are deleted; imports go straight to
nigig_core/nigig_uikit and NavigationBarAction lives in a real module.
D3: the CachedWidget decision is documented in email.rs.
2026-08-16 22:22:55 +00:00
c792ec5a30 feat(email): SMTP transport pool (D4) and drop the dead action variant (D5)
D4: a one-slot transport pool reuses the SMTP connection across sends
instead of rebuilding a transport (TCP+TLS+AUTH) per operation. Safe
because SEND_IN_FLIGHT already serialises sends, so a single reused
transport is exactly the right size. The entry is moved out of the pool
while held (a MutexGuard is !Send and would poison the spawned future),
and the reuse-vs-rebuild keying -- server/port/username AND password --
is a pure, tested function. Connection-level reuse is a read of lettre's
contract, not an observed handshake (no live relay in CI).

D5: EmailWorkerAction::None is gone, with its Default and
ActionDefaultRef impls. The action is consumed only via downcast_ref()
(needs 'static + Debug), so no default was ever required.

Also: EmailSessionAction (SignedIn/SignedOut) so the More page's sign-out
reaches the inbox across the sibling-page PageFlip boundary.
2026-08-16 22:22:55 +00:00
78d4a52e6f feat(email): complete Phase C domain — IMAP, keystore seam, cache, pacing, dispatch
C1e: imap_client.rs — ImapTransport trait, ImapClient (verify + list_inbox),
a pure INTERNALDATE parser and envelope mapping, and a feature-gated
async-imap transport (native only; wasm never compiles it).
C1f: credential_store.rs — CredentialStore trait and a fail-closed default;
the platform keystore (AndroidKeyStore, per the SMS precedent) is the
follow-on that cannot be host-tested.
C3: email_cache.rs — a local mail cache whose bodies are pushed through a
BodyCipher before hitting disk; PlaintextBodyCipher is the honest default
until C1f lands a real key.
C6: email_pacing.rs — SendRateLimiter + SendPacing ported from robius-sms
with email-shaped limits (100/hour); fixes a zero-capacity panic in the port.
C5/C4b plumbing: email_session.rs (shared signed-in account), InboxFetched
action, spawn_fetch_inbox and spawn_send_message (backend-aware dispatch),
and EmailSendRequest::build_without_config for the proxy send path.

Domain tests 154 -> 193.
2026-08-16 22:22:55 +00:00
d15a034797 test(cad): cover the async export path and the model-name env read
Some checks failed
email.yml / test(cad): cover the async export path and the model-name env read (push) Failing after 0s
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
exporters.rs 76.30% -> 91.17%, constants.rs 82.69% -> 97.30%.

`spawn_export_to_target` was 0% -- the entire function. It exists
because every export used to serialise on the UI thread into a fixed
path, so a 1,000-part GLB froze the editor and the second export of a
session destroyed the first. The replacement had never been executed by
a test: not the worker thread, not the BufWriter flush, not the callback.

Four tests, driven by a stub Exporter over an empty scene, each blocking
on the callback through a channel so the assertion is that `on_done`
actually fires on the worker thread:

  - success: the bytes land at the path, and the status message names
    the byte count and the destination. The old fixed-path exports had
    nothing to assert here -- the destination was not a parameter.
  - exporter failure: reported as "export failed: ...", and NO file is
    created. A half-written export that reports success is worse than
    no export.
  - undeliverable: parent is a regular file, so create_dir_all fails
    and the message says so.
  - a directory in the file's place: the other side of the
    create_dir_all guard, where the write itself fails.

Still uncovered in exporters.rs: the ExportTarget::Prompt arm, 28 lines.
It raises a native save dialog; there is no windowing system in a
coverage run and the harness's picker stub deliberately refuses rather
than faking a save, so those lines are reported as uncovered instead of
being reached by a test that proves nothing.

constants.rs: local_openai_model was the one endpoint-configuration
reader with no test, while local_openai_url next to it had five. A blank
model name now has to be None -- passing "" to the endpoint produces a
rejected request that surfaces to the user as an AI failure rather than
as missing configuration.

Verified with: ./tools/test-cad-coverage.sh  (466 tests green)
2026-08-16 22:22:26 +00:00
34d47f4479 test(cad): cover persistence.rs, 0.00% -> 94.51% of lines
Some checks failed
email.yml / test(cad): cover persistence.rs, 0.00% -> 94.51% of lines (push) Failing after 0s
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
persistence.rs owns every byte the CAD editor writes: the saved script,
the baked OBJ mesh, the thread-local slot the script VM uses to hand a
Solid back to the UI, and the pending-image mutex. It had no tests. Not
low coverage -- zero.

It could not have had any. `save_cad_state` and `load_saved_cad_script`
resolve their directory through `cad_data_dir()`, which is the real
per-user application data directory. A test that called them would write
into the developer's (or the CI runner's) actual data dir, and two tests
would fight over the same file. So the save/load pair is split:

  save_cad_state_in(dir, source, solid)
  load_saved_cad_script_in(dir)

with the existing public functions delegating to them through
`cad_generated_dir_path()`. No caller changes, no behaviour changes --
viewport.rs, workspace.rs and workspace_actions.rs keep calling exactly
what they called before.

14 tests, on the failures rather than the happy path:

  - A save that cannot create its directory returns the "could not
    create generated directory" error. This is the path that used to be
    `.ok();` at the call site, which is how a "Saved" label appeared
    over a write that never landed.
  - A failed OBJ write still leaves the script on disk, and the test
    asserts the script is readable back afterwards. The write order is
    load-bearing: losing the baked mesh costs a rebuild, losing the
    source costs the user's session.
  - An empty or whitespace-only script file loads as None, not as
    Some(""). Some("") would open the editor blank and then overwrite a
    script the user still had.
  - The script-output slot must empty on take. A stale Solid re-applied
    on the next tick would silently undo whatever the user did in
    between.
  - The generated paths are asserted to hang off the runtime data dir
    and to contain no build-time source path -- the CI gate for that
    rule greps for one macro, this pins the actual result.

Uncovered: 6 lines, the two public wrappers. Calling them means writing
to the real data dir, which is the thing this commit is avoiding.

Verified with: ./tools/test-cad-coverage.sh  (459 tests green)
2026-08-16 22:20:55 +00:00
2ea5a7424e fix(cad): mat4_inverse was wrong for every matrix that rotates and translates
Some checks failed
email.yml / fix(cad): mat4_inverse was wrong for every matrix that rotates and translates (push) Failing after 0s
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
3-D picking has been unprojecting clicks to the wrong world point. The
coverage run in the previous commit left seven lines uncovered -- the
success tail of mat4_inverse -- and the round-trip test written to reach
them failed on element 12.

mat4_inverse is a cofactor expansion transcribed from the standard MESA
gluInvertMatrix. Comparing all sixteen expressions term by term against
the reference, EVERY ONE of them had at least one mistyped index:

  inv[0]  a[9]*a[11]*a[14]  should be  a[9]*a[7]*a[14]
  inv[12] a[8]*a[10]*a[13]  should be  a[8]*a[6]*a[13]
  inv[13] a[8]*a[10]*a[13]  should be  a[8]*a[2]*a[13]
  inv[14] a[0]*a[7]*a[13]   should be  a[0]*a[6]*a[13]
  inv[14] a[4]*a[7]*a[13]   should be  a[4]*a[2]*a[13]
  ... and one each in the other eleven.

The wrong terms all carry a[3], a[7], a[11] or a[15] -- the bottom row.
For a pure translation or a pure rotation those are 0, 0, 0, 1 and the
mistyped products cancel, which is why the function looks correct in
isolation and why nothing caught this. It stops cancelling the moment a
matrix rotates AND translates.

Which is what the two callers pass in:

  - CadViewport::camera_eye inverts self.last_view.
  - CadViewport::unproject_point inverts self.last_proj, whose element
    11 is -1 for a perspective camera, and then self.last_view.

Measured on a view matrix with a 35 deg yaw and eye (3, -1, 2), the old
code's inv * m came back with 0.4698 and -0.7988 in the translation row
instead of zero: a click resolved to a point roughly one unit away from
where the user clicked, growing with camera distance. Selection, snap
and the measure tool all read that point.

Fixed by transcribing the reference again, this time verified: the tests
assert inv*m AND m*inv against the identity for a translation, a
rotation, translate*rot_y, translate*rot_zyx, a perspective matrix, an
orthographic matrix, and a dense matrix with no zero entries -- the last
because a wrong index cannot cancel when nothing is zero.

math.rs is now 99.60% of lines. The three remaining are the
`det.abs() < 1e-8` early return's own arm, covered by
mat4_inverse_of_a_singular_matrix_is_none but not attributed to it.

Verified with: ./tools/test-cad-coverage.sh  (445 tests green)
2026-08-16 22:18:57 +00:00
500489c2f0 test(cad): cover math.rs, 20.40% -> 99.00% of lines
Some checks failed
email.yml / test(cad): cover math.rs, 20.40% -> 99.00% of lines (push) Failing after 0s
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
math.rs had no tests at all. Every other engine file in the CAD module
carries its own #[cfg(test)] block; this one -- the file that decides
where a click lands in 3-D, whether a point is inside a picked polygon,
and how a part's model matrix is built -- had none, and the coverage
harness added in the previous commit put a number on it: 20.40%.

39 tests, written against the behaviour that is easy to get wrong rather
than the happy path:

  - The two normalize functions disagree on purpose. DVec3::normalize
    returns -Z for a degenerate vector, vec3_normalize returns zero.
    Both are pinned, because "fixing" either to match the other would
    change picking behaviour silently.
  - point_in_polygon is exercised on a concave L-shape, not just a
    square. The picking path projects a bounding box to screen space and
    can produce a concave outline; a convex-only test passes on a
    ray-casting implementation that is broken for exactly that case.
  - point_on_segment_nearest is checked past both endpoints, where the
    projection parameter is clamped, and on a zero-length segment, where
    the 1e-12 guard is the only thing between the caller and a NaN.
  - ray_triangle_intersect is checked on each rejection branch
    separately: parallel, u < 0, v < 0, u + v > 1, and a triangle behind
    the origin.
  - ray_aabb_intersect is checked from outside, from inside (where it
    returns the exit parameter, not the entry), behind the ray, parallel
    to a slab both inside and outside it, with a negative direction
    component (the t1/t2 swap), and on a diagonal miss -- which is the
    only way to reach the `tmin > tmax` return, since an axis-aligned
    miss leaves through the parallel-slab branch first.
  - segment_intersection is checked parallel, crossing, and crossing
    off the end of one segment and of both.

Remaining uncovered: 7 lines, the success tail of mat4_inverse. The next
commit reaches them, and finds out why they were never reached.

Run: ./tools/test-cad-coverage.sh
2026-08-16 22:18:16 +00:00
674b2be66d feat(pdf): JPEG 2000 decoding — Phase 3 complete, all three codecs
Some checks failed
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (push) Has been cancelled
email.yml / feat(pdf): JPEG 2000 decoding — Phase 3 complete, all three codecs (push) Failing after 0s
repo hygiene / hygiene (push) Has been cancelled
The last codec ADR 0015 deferred. The plan recorded the blocker as a
dependency decision, not an algorithm: openjpeg would add a C dependency
that breaks the Android cross-compile. This is pure Rust and adds no
dependency at all.

It shares the MQ arithmetic decoder with JBIG2 — T.800 and T.88 specify
the same coder — so the previous tranche paid for most of this one.
Context::with_state moved onto the shared type because JPEG 2000 starts
three of its nineteen contexts away from state 0 and JBIG2 starts all of
them at 0.

Implemented: codestream and JP2 container parsing, packet headers with
tag trees and the bit-stuffing rule, EBCOT tier-1 (all three passes,
four zero-coding context tables, run-length mode), both 5/3 reversible
and 9/7 irreversible wavelets, RCT and ICT, arbitrary decomposition
levels, and multiple components.

Refused by name: multiple tiles, custom precinct partitions, code-block
style options, COC/QCC/RGN/POC overrides, subsampled components. Each
error says which feature the file needs. This matters more here than
anywhere else in the stack, because a JPEG 2000 decoder that quietly
skips something does not fail — it returns a slightly soft or banded
image that looks entirely fine.

That property also dictates how this is tested. Fixtures are produced by
OpenJPEG via Pillow and compared **exactly**, sample for sample: the
fixtures are lossless 5/3 so no tolerance is needed, and a tolerance is
where a subtly wrong decoder hides. Four images — grayscale raw
codestream, the same in a JP2 container, a larger one whose tag trees
actually branch, and RGB. A generator script is checked in beside them
so CI can prove the fixtures still match what produced them.

Verified by mutation. The first round was misleading and is worth
recording, because it is the same lesson as ADR 0017:

  DC level shift dropped        3 fail
  5/3 lifting rounding changed  2 fail
  RCT sign flipped              PASSED  <- survived
  RCT components swapped        PASSED  <- survived
  cleanup run-length disabled   PASSED  <- survived
  sign-context XOR dropped      PASSED  <- survived

Four mutations survived because Pillow writes MCT=0 by default, so the
RGB fixture coded its three components independently and never reached
the colour transform at all. The RCT branch was completely untested
while appearing covered — an untested branch that looks tested is worse
than one that looks missing. Added rgb8_mct.j2k with mct=1; all four
now fail. The header bit-stuffing mutation is caught by the unit test
rather than the round-trip.

Two real defects found while writing the tests:

- A corrupt marker length in a tile-part header walked the read cursor
  past the codestream and panicked on a slice. Found by the corruption
  sweep, not by review. The sweep now truncates at every length and
  flips every byte of a real file, and asserts only that nothing panics.
- The 9/7 flat-signal test initially asserted an amplitude I had derived
  from my own arithmetic. That is a test agreeing with the code by
  construction. It now asserts flatness — a ripple means the lifting or
  the edge extension is wrong — and the amplitude is pinned by the
  OpenJPEG round-trips instead, which use pixels this code did not
  produce.

Also removed two dead fields and an unused parameter that clippy found:
Subband::x0/y0 are always zero in the single-tile case this supports,
and dead state implying multi-tile support exists is worse than no
state.

JPX decodes on the image path, like JBIG2, because the codestream
carries its own geometry; it stays in REFUSED_CODECS with a reason
string saying where it is decoded rather than that it is missing.

Engine suite 866 -> 920. Coverage 85.66% -> 86.16%; jpx.rs at 93.72%
with a floor at 88.

Phase 3 is complete: CCITT, JBIG2 and JPX all land, and the plan is
updated to say so and to record how the two gating questions — JBIG2's
CVE record and JPX's C dependency — were actually answered.
2026-08-16 22:17:40 +00:00
24a26f052e test(cad): host-only coverage harness for the CAD engine
Some checks failed
email.yml / test(cad): host-only coverage harness for the CAD engine (push) Failing after 0s
repo hygiene / hygiene (push) Has been cancelled
The CAD module had 411 tests and no way to find out what they miss.
`cargo test -p nigig-build` needs the full Makepad desktop stack --
wayland, X11, GL, alsa, polkit -- so nobody had ever run it under
instrumentation, and "well tested" was an assertion, not a measurement.

Fourteen of the module's twenty-six files are pure: geometry, the scene
graph, undo/redo, the four exporters and file I/O. Their only Makepad
imports are the math types, the CSG library and two log macros, all of
which are dependency-free Rust. This script copies those fourteen into a
temporary crate that carries the SAME module path
(`nigig_build::construction_frame::pages::workspace::cad::*`), so the
sources compile byte-for-byte with no edits, and runs them plus the real
tests/cad_integration.rs under `-C instrument-coverage`.

Baseline on this commit: 84.41% of lines over the fourteen engine files
and the integration suite. math.rs is 20.40% and persistence.rs is 0.00%.

Excluded from the report, per the coverage plan: the Makepad checkout
(vendored/generated upstream code), the cargo registry and git caches,
the rustc sysroot, and the harness's own lib.rs/shim/picker -- the
platform-startup stand-ins the script writes itself, which are
scaffolding and not CAD code. The exclusion is enforced twice, by
-ignore-filename-regex and by an explicit source list, because the
regex alone breaks when CAD_COV_MAKEPAD points outside the temp dir.

What it does NOT measure, and does not pretend to: mod.rs, viewport*.rs,
workspace*.rs, script_bindings.rs, cad_editor_sheet.rs, code_editor.rs,
tools.rs and profile_benchmarks.rs. Those need live_design!, Cx and an
event loop; the full-crate-check job in nigig-build.yml gates them.

Everything -- toolchain, cargo home, target dir, profraw data, the
fetched Makepad tree, the report -- lives under one mktemp directory
removed by a shell trap on success, failure, interrupt or termination.
The two enums the integration suite borrows from the widget-bound mod.rs
are extracted from the real file at run time rather than copied, so the
harness cannot silently drift from the crate.
2026-08-16 22:16:56 +00:00
81e846ae35 feat(pdf): JBIG2 generic-region decoding, and the bitonal image path
Some checks failed
email.yml / feat(pdf): JBIG2 generic-region decoding, and the bitonal image path (push) Failing after 0s
repo hygiene / hygiene (push) Has been cancelled
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (push) Has been cancelled
The second codec Phase 3 deferred. ADR 0015 made a threat review the
precondition for implementing JBIG2 rather than an effort estimate, so
the review's conclusion is encoded in what this does and does not do.

What is implemented: the MQ arithmetic decoder (T.88 Annex E), generic
region decoding with templates 0-3 and AT pixels, TPGDON typical
prediction, and MMR-coded regions. Segment header and region info
parsing, and page composition.

What is refused, by name: symbol dictionary, text region, halftone
region, refinement region, and a non-empty /JBIG2Globals. Those are the
segment types that carry the composition machinery, and shipping them
means shipping an interpreter over untrusted input — it is what
FORCEDENTRY built its computer out of. A file needing them gets a typed
error naming the segment type, exactly as the whole codec used to.

The MQ coder itself is pure arithmetic with no file-controlled
addressing, which is why it is safe to run and the composition parts are
not. Every bound is checked against the declared region size before a
buffer is indexed: region dimensions against MAX_DIMENSION and a pixel
budget before allocation, segment lengths against the remaining stream,
and the region's declared position against the page before a single
pixel is written. That last one is the format's actual exploit surface
and it has its own test saying so.

MMR regions delegate to ccitt.rs rather than carrying a second G4
decoder, so the two cannot drift apart. A test decodes the same coded
bits through both paths and requires identical pixels — that is what
catches an inverted convention, and JBIG2 is natively 1=black where PDF
is 0=black, so the inversion is real and easy to get backwards.

JBIG2 is decoded on the image path, not in the filter facade, because it
needs /Width and /Height from the image dictionary. It therefore stays
in REFUSED_CODECS with a reason string that says where it *is* decoded,
so a host showing that string does not tell a user the codec is missing
when it is not. CCITT moved the other way for the same reason inverted:
it derives its dimensions from /DecodeParms, so it decodes in the facade.

Wiring both into ImageInfo::decode_to_rgba surfaced a defect in the
parallel-array rule that the CCITT tranche had not reached. For
/Filter [/FlateDecode /CCITTFaxDecode] the /DecodeParms array has one
entry per filter, and the obvious implementation takes arr[0] — handing
the Flate parameters to the fax decoder. ccitt_parms_of finds CCITT's
own index instead. This is the same bug ADR 0015 records for the old
chain code, in a new place.

A declared-but-unresolved /JBIG2Globals returns None rather than
decoding without it. Decoding anyway yields a blank or partial image
that every caller reads as a success — the declared-versus-delivered
failure of ADR 0017.

Verified by mutation, six injected defects, each confirmed red:

  compose bounds check removed        1 fails
  pack() stops inverting              4 fails (both suites)
  globals silently ignored            1 fails
  refused segments silently skipped   1 fails
  declared-globals check dropped      1 fails
  ccitt_parms_of always takes slot 0  1 fails

29 unit tests and 12 integration tests, asserting pictures rather than
buffer lengths. ADR 0016's stub JPEG decoder returned a correctly sized
black rectangle and passed everything that checked a length; these say
which colour they expect.

Engine suite 825 -> 866. Coverage 85.15% -> 85.66%; jbig2.rs at 94.84%
with a floor at 90, and image.rs 31.76% -> 44.77% so its floor rises
28 -> 40.

JPX remains refused and is the next tranche.
2026-08-16 22:02:26 +00:00
b26e6a1f14 feat(pdf): CCITT G3/G4 decoding — the codec Phase 3 deferred
Some checks failed
email.yml / feat(pdf): CCITT G3/G4 decoding — the codec Phase 3 deferred (push) Failing after 0s
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
ADR 0015 refused CCITTFaxDecode by name and recorded it as the
recommended next codec: well specified, no arithmetic coding, no C
dependency. This implements it.

T.4 and T.6, all three schemes selected by /K: G3 1D modified Huffman,
G4 two-dimensional, and G3 mixed with a tag bit after each EOL. Both
run-length code books, makeup and extended makeup codes, and the
pass/horizontal/vertical mode codes. /Columns, /Rows, /BlackIs1 and
/EncodedByteAlign are honoured; /Columns and /Rows are bounds-checked
before anything is sized from them, because both are attacker-controlled
in a hostile file.

It decodes to real pixels, so unlike DCTDecode it belongs in the filter
facade rather than the image path: the generic filter contract promises
decoded bytes and this can honestly keep that promise. Removed from
REFUSED_CODECS, added to SUPPORTED_FILTERS — the registry now describes
what the crate actually does. Both existing data-driven registry tests
pick this up without editing.

Three defects were found by writing the tests rather than by reading
the code:

- A zero-length run recorded no transition. That is exactly how a row
  beginning with black is coded — a white run of zero, then the black
  run — so every such row came out with its colours shifted by one run:
  "####...." decoded as "....####".
- Decoding stopped at bits_left() == 0, but encoders pad the final row
  to a byte boundary. The padding was fed to the decoder as though it
  were a code, failed to match, and lost the whole image. Now a
  trailing all-zero tail is recognised as padding, which is
  unambiguous because every code book needs a 1 bit.
- A row of zero-length runs did not advance the pixel position and
  looped forever. Found by mutation, not by review. Bounded by the
  column count: a hang is a worse failure than an error.

Verified by mutation, five injected defects, each confirmed to turn the
suite red:

  a0 starts at 0 not -1        1 fails
  pack_row fills black         13 fails
  find_b1 parity dropped        1 fails
  short-/Rows check removed     1 fails
  read_run returns 0            2 fails

Two of those did not fail on the first attempt and changed the tests:

- a0 = 0 survived, because no fixture placed a colour change at column
  0 — the one position where the off-by-one is visible. Added
  group4_codes_a_change_at_column_zero.
- read_run returning 0 survived because the new run bound also errors,
  so an assertion of merely "some CCITT error" could not tell the two
  mechanisms apart. The assertions now name the specific failure.

30 unit tests in the codec, asserting decoded pictures rather than
byte counts, plus 6 integration tests through the filter facade
covering the chain case, truncation and the spec defaults. The
facade test asserts output != input: ADR 0015 records DCTDecode
"succeeding" by returning its own compressed input, and a test that
only asserted Ok passed against that bug.

Engine suite 796 -> 825. Coverage 84.82% -> 85.15%; ccitt.rs at 92.57%
with a floor at 88.

JBIG2 and JPX remain refused and are the next two tranches.
2026-08-16 21:50:46 +00:00
3dab4a1fd5 test(email): coverage floors for the email domain
Some checks failed
email.yml / test(email): coverage floors for the email domain (push) Failing after 0s
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
tools/test-email-coverage.sh instruments the nigig-core email domain
and enforces a whole-domain floor (90%) plus per-file floors on the
files that harboured the bugs. It runs in an isolated temp dir and
reports over only the seven email source files, excluding Makepad's
generated code. Wired into email.yml, which also now runs mail_proxy
tests and ratchets the domain test floor to 150.

Measured 93.4% line coverage across the domain.
2026-08-16 21:49:12 +00:00
47b2f72af0 feat(email): backend chooser + two setup forms (C1c)
SetupDraft ties account identity to the backend choice and validates
them together (the direct backend must also have a usable SMTP server;
the proxy backend leaves SMTP fields unset). The setup form grows a
chooser -- Direct (IMAP + SMTP) vs the Nigig mail service -- with an
honest per-backend summary, and two forms swapped by the chooser. The
inbox branches: proxy accounts verify against the mail service via
spawn_proxy_verify instead of an SMTP handshake.

Also pins the Proton Bridge provider default (local bridge, not a
remote imap.protonmail.ch).
2026-08-16 21:49:12 +00:00
2230933e4a feat(email): ProxyApiBackend HTTP client (C1d)
The proxy backend's network half: a thin client over a ProxyTransport
trait (reqwest on native, fetch on wasm, mock in tests). verify,
list_inbox and send build typed requests and map status+body onto
structured errors, so the parsing logic -- where the bugs live -- is
host-tested without a server. The token rides in an Authorization
header and never in a request type that can be Debug-printed.

Also: spawn_proxy_verify posts a ProxyVerifyResult action, and
build_transport is pinned to construct for every port (A2/A3).
2026-08-16 21:49:12 +00:00
4426cd2c43 docs(map): reconcile makepad fork with upstream dev 2026-08-16
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
- Fork portallist_flow_adaptive_view at ecf5a572 is 1 ahead / 0 behind upstream dev abd70f47 (Aug 15); extra commit restores test/gltf/csg re-exports
- Hard-reset nigig-dev-reexports from stale 2c5cd97 to ecf5a572 and force-pushed so both tracking branches are current
- Pinned rev stays ecf5a572 in map + pdf-makepad Cargo.toml (no bump needed)
- Document baseline strategy: nigig-map/makepad_map as control surface tracking upstream widgets/src/map, not enabling map feature in nigig-rider for rendering; upstream routing (map_nav/geodata/route app) stays separate from widget (valhalla vs map_nav decision)
- Note periodic diff workflow for packed-vertex, LOD, dissolve, growing-archive watcher improvements
2026-08-16 21:30:47 +00:00
df618c4091 fix(map): overlay DrawVector import and tile super::* paths
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
nigig-map / test (push) Has been cancelled
- overlay.rs used makepad_draw::vector::DrawVector which does not exist; use makepad_widgets::DrawVector (as in top-level overlay.rs)
- makepad_map/tile.rs: crate::label/style/geometry -> super::label/style/geometry so it resolves to makepad_map submodules (which have LABEL_CLASS_PIN, bag_year_color, TILE_SIZE, stroke_prof_take) not top-level crate modules
- Now cargo check shows only wayland-sys native lib missing, no Rust errors
2026-08-16 21:27:26 +00:00
e6702c3437 fix(map): repair nigig-map build — icons, imports, crate deps
Some checks failed
email / gates (push) Has been cancelled
email / email-domain (push) Has been cancelled
email / nigig-email (push) Has been cancelled
email / supply-chain (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) Has been cancelled
doc-engine / consumer (push) Has been cancelled
sms / gates (push) Has been cancelled
sms / robius-sms (push) Has been cancelled
sms / android (push) Has been cancelled
sms / nigig-sms (push) Has been cancelled
sms / supply-chain (push) Has been cancelled
nigig-map / test (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
- makepad_map/icons.rs pointed at icons/ inside makepad_map/; icons live in src/icons, fix to ../icons.
- drape.rs used crate::map:: prefix which does not exist in crate root; use super::
- tile.rs crate::map:: prefix broke after module rename; collapse to crate::
- overlay.rs DrawVector came from crate::; import from makepad_draw::vector instead
- view.rs used crate::{makepad_derive_widget, makepad_draw, widget} which are external; switch to makepad_widgets::
- mvt_parser, tile_disk, makepad_map/tile, drape re-used makepad_widgets::makepad_fast_inflate/mbtile_reader which are not re-exported; depend on direct crates makepad-fast-inflate, makepad-mbtile-reader and import them directly
- Add missing Cargo deps makepad-draw, makepad-platform, makepad-derive-widget, makepad-fast-inflate, makepad-mbtile-reader, makepad-script pinned to existing rev ecf5a572

Workspace now passes cargo check -p spreadsheet-engine (250 tests) and map crate no longer errors on include_str / unresolved import; remaining linux GUI link requires native libs.
2026-08-16 21:14:38 +00:00
7d6fc4cbbe feat(pdf): document creation — outlines, forms, attachments, font subsetting
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (push) Has been cancelled
Phase 4 of NIGIG_PDF_FEATURE_PARITY_PLAN.md. ADR 0019.

Almost none of it existed: Outlines, PageLabels, EmbeddedFiles and
ViewerPreferences appeared nowhere in the workspace, in any crate. What did
exist was a builder whose central method was

  pub fn add_page_with_content(&mut self, _width: f64, _height: f64, ...)

which accepted a page size and discarded it. Asking for 200x400 and 300x500
gave two US Letter pages, because no /MediaBox was written at all. The test
asserted the output contained the string "/Type /Page", which it did.

Two more defects sat in the object writer, both producing files our own
parser rejects: dictionary keys were written unescaped (a key with a space
reparses as "expected number"), and f64::NAN was emitted as the literal
token NaN, so one non-finite value anywhere made the document unreadable.

Added: outline trees with the open/closed state in the sign of /Count,
/PageLabels as a number tree with real roman and A..Z/AA..ZZ numbering,
named destinations, attachments with file specs, /Info, XMP, viewer
preferences, page mode and layout; AcroForm creation for text, checkbox,
radio, choice and signature fields with generated appearances; and
TrueType subsetting - DejaVu Sans goes from 759,720 bytes to 4,348 for
twelve characters.

cmap is deliberately not rebuilt: the subset is embedded as a CID font with
Identity-H, so the content stream addresses glyphs by id and /ToUnicode
serves extraction. A cmap disagreeing with the content stream is worse than
none. CFF is refused by name rather than emitting a font with no glyphs.

Nine real bugs, every one found by running the output through an
independent tool rather than by reading the code:

  1 page size discarded              reading a generated file back
  2 dict keys unescaped              probing the writer
  3 NaN written as a keyword         probing the writer
  4 subset zeroed the lsb            fontTools outline compare
  5 hmtx indexed by new gid          fontTools outline compare
  6 name table format read as count  BaseFont came out "Embedded"
  7 add_font shifted numbers already handed out
  8 trees allocated over font numbers - object 29 written twice
  9 widgets missing /F Print, /P and appearance /Resources

7 and 8 are the instructive pair: every reference resolved and every object
existed, each simply named the wrong thing. pypdf reported correct field
values from a file PDFium rendered blank. 9 is the one only a renderer could
find - /F defaults to non-printable, and a form XObject naming a font its
/Resources does not declare is discarded whole.

Verified by three independent implementations: fontTools (0 outline
mismatches of 12 against the source font), pypdf (metadata, page sizes,
outline with resolved page numbers, all five fields, attachment
byte-for-byte, labels ['i','1']) and PDFium, which renders both pages
correctly. cargo run -p nigig-pdf-graphics --example generate_sample
regenerates the sample.

Fourteen mutations. Three survived and each exposed a weak test: the key
test used an attachment name (written as a string, never a key), nothing
read the outline open state, and /P could not be witnessed because
page_index is supplied by the reader, which already knows the page. All
three now killed.

pdf: 789 passed (was 730). pdf-ui: 775. Coverage 85.17%.
2026-08-16 21:02:19 +00:00
nigig-ci
c0b27d0586 feat(email): MailBackend trait and BackendKind — both backends (C1a/C1b)
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
email / gates (push) Has been cancelled
email / email-domain (push) Has been cancelled
email / nigig-email (push) Has been cancelled
email / supply-chain (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
You chose to support IMAP-on-device AND a server-side proxy, user
selectable. This is the seam that makes that contained rather than two
parallel apps.

Why it is cheaper than it sounds: wasm cannot open a raw TCP socket, so a
proxy always had to exist for the browser target. The second backend was
never optional -- it was implied scope nobody had named.

C1a, mail_backend.rs:

  BackendKind { ImapSmtp, ProxyApi } with three predicates that exist so
  the UI cannot get them wrong:

    is_available_on_wasm()      IMAP is raw TCP; a browser cannot open one,
                                so the chooser must not offer a dead option
    stores_reusable_password()  IMAP keeps a REUSABLE mailbox password on
                                the device. For most people that is the
                                password-reset channel for every other
                                account they own. A revocable proxy token
                                is strictly safer, and the chooser must say
                                so rather than presenting a free choice
    summary()                   the honest one-liner, asserted by test to
                                actually mention "password" / "revoke"

  BackendSettings is the PERSISTABLE half and carries no secret, exactly
  as EmailAccount does for the password (S2). BackendDraft::validate
  returns (settings, Secret) and reports every problem in one pass.

  The trait is deliberately synchronous and tiny -- kind(), is_configured(),
  describe(). Anything computable above the line (grouping, previews,
  threading) is NOT a backend concern, which is why email_store did not
  change at all. I/O stays in the free functions that already own the async
  context, so this file is host-testable with no runtime.

  ImapSmtpBackend exists with validation but no protocol client yet; that
  is C1e and nothing here claims a connection works.

C1b: EmailAccount gained `backend: BackendSettings`, #[serde(default)] so
existing persisted accounts still load. A test asserts the serialised
account -- including the backend section -- contains neither the token nor
a field named password/token.

Provider defaults now fill IMAP too, so a Gmail user still fills one
field. Outlook is special-cased: its IMAP host is outlook.office365.com,
not imap.outlook.com, so the naive smtp->imap rewrite would produce a name
that does not resolve.

New gate, negative-tested both ways: stores_reusable_password() and
is_available_on_wasm() must exist, and the persisted settings structs must
not declare password/token/secret fields.

Domain tests 99 -> 126. Test floor 95 -> 120.
2026-08-16 20:30:33 +00:00