Commit graph

113 commits

Author SHA1 Message Date
88d46600e1 wip(daily-reports): preserve local site/report/worker, ocr and lite-common updates for Phase 0 freeze 2026-09-27 16:25:50 +03:00
andodeki
c92e67fbd8 test(nigig-traffic): TRAFFIC-14 enable GUI runtime tests (4/4 green under Xvfb)
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
traffic / gates (push) Has been cancelled
traffic / nigig-traffic (push) Has been cancelled
traffic / supply-chain (push) Has been cancelled
- Remove all ignores in tests/ui.rs; harness failure was the broken pin
  plus no display. CI runs them via xvfb-run (xvfb, libgl1-mesa-dri).
- HUD test: HUD is raw draw_text, not widgets; assert view survives
  Intro->Driving and capture screenshot instead of invisible selectors.
- Docs: release gate ticked, blocker 2 closed, 430px HUD clipping noted.

Verified: 121 pass / 0 fail / 0 ignored; clippy 0 owned; fmt; diff --check.
2026-09-26 13:20:51 +00:00
3cfcc2cc60 fix(cad-core): CORE-04 remove last runtime fan triangulators; plan status to complete
- math::triangulate_polygon now ear-clips via polygon::triangulate_indices
  (index/orientation preserving; rejects degenerate, non-finite, bow-tie)
- extrude_polygon_mesh routes through triangulate_profile; new
  try_extrude_polygon_mesh returns structured errors instead of
  underflowing on undersized profiles
- tests: concave area equality, closed-manifold edge twins, bad corpus
- cad.yml gate forbids reintroducing the vertex-zero fan
- EXECUTION_PLAN.md: tranche ledger, verification, release gates
2026-09-26 12:05:51 +00:00
0819e97074 feat(nigig-site): compile and test the hardening core; add SITE-20..28 domain
Some checks failed
email / gates (push) Has been cancelled
nigig-site / Security and supply-chain baseline (push) Has been cancelled
nigig-site / Release capability gate (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
cad / cad-truth-gates (push) Has been cancelled
cad / cad-core-checks (push) Has been cancelled
cad / cad-consumers (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
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
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (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
spreadsheet / engine-coverage (push) Has been cancelled
spreadsheet / ui-controller-coverage (push) Has been cancelled
traffic / gates (push) Has been cancelled
traffic / nigig-traffic (push) Has been cancelled
traffic / supply-chain (push) Has been cancelled
Thirteen hardening modules (3,079 lines) sat in nigig-site/src/ declared in no
mod statement. They had never been compiled or tested: cargo check never saw
them, and the plan's "implemented in the worktree and pending verification"
status was unverifiable by construction.

Extract the UI-free core into crates/apps/nigig-site-core (scope 5's "Rust core
crate ... testable; safe"; plan 6's target architecture). nigig-site depends on
it and re-exports it, so there is one copy of each module source: the app and
the core's unit tests compile the same files. The core has no Makepad
dependency, so its contracts run on a memory-limited runner.

Compiling that code for the first time found four real defects, all fixed:
- Role/Capability lacked Ord, so every BTreeSet of them failed to compile
  (auth.rs)
- CaptureResult::empty never initialised site_id from its site parameter
  (ocr_policy.rs)
- negotiate_version(2, 5) agreed on a protocol the peer never offered, which is
  the silent downgrade the function exists to prevent (sync_protocol.rs)
- an absurd frame size returned Overflow instead of the actionable budget
  breach (media_bounds.rs)

New feature-tranche domain modules, each with unit tests:
- organisation.rs   SITE-20 invites, per-site roles, the 4.2 matrix as testable
                    data, site registry with geofence, settings
- report_pack.rs    SITE-21 report numbering, entry status, signatures binding
                    actor/device/timestamp/document hash, lock and versioning,
                    multi-site compilation, monthly packs
- site_diary.rs     SITE-22 weather with provenance, plant, deliveries, delay
                    log where a weather delay needs supporting rainfall,
                    visitors, manpower by trade
- workforce.rs      SITE-23 consent-gated registration, tag-only blocklist,
                    attendance with overtime, QR badges, payroll CSV that never
                    emits identity, offboarding tombstones
- programme.rs      SITE-25 dependencies with cycle detection and rollback,
                    topological order, critical path, frozen baselines with
                    slippage, checklists gating approval, snags, RFIs,
                    variations needing two distinct approvers
- hse.rs            SITE-28 append-only incidents, closure requires corrective
                    action, toolbox talks, inspections, monthly statistics

workflows.rs gains the FR-1.14 Locked state; commands.rs gains a bounded
non-empty text validator shared by the new modules.

CI: the SITE-02 crypto/repository/store lanes pointed at -p nigig-site, where
those suites no longer live; left alone they would have compiled nothing and
reported a vacuous green. Repointed at the core, and added core-contracts and
core-clippy lanes with a ">=100 tests collected" check so a lane cannot pass
vacuously. All six existing Python contract gates still pass.

Auto-purge of worker ID data refuses to run until the scope 18 retention
question is answered rather than inventing a window.

Verified 2026-09-26: cargo test -p nigig-site-core --locked = 175 passed,
0 failed, 1 ignored (needs a live Secret Service session); cargo clippy -p
nigig-site-core --all-targets --no-deps -- -D warnings clean; cargo check -p
nigig-site clean. cargo test -p nigig-site is still killed by SIGKILL compiling
makepad-widgets on a 2 GB host, as recorded in plan 2.1.

EXECUTION_PLAN.md gains a per-tranche status ledger (1a) that states plainly
which tranches are done, domain-only, externally blocked, or not started, and
records that this branch and main diverged at b3a9005 with SITE-03 published
only on main.
2026-09-26 10:50:33 +00:00
1516aa747b Initial project: app scope + Makepad skeleton 2026-09-26 09:11:09 +03:00
38feca50a1 ci(cad): UI-15 runtime matrix plus CORE/UI truth gates
Some checks failed
cad / cad-truth-gates (push) Has been cancelled
cad / cad-core-checks (push) Has been cancelled
cad / cad-consumers (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
UI-15: project_lifecycle (create/open/edit/save/restart/switch/undo
with A/B isolation and fail-closed corrupt/future/legacy cases),
script_limits (adversarial corpus), bvh_differential (randomized
brute-force comparison with tie discipline), export_interop
(queue discipline, STL/DXF/GLB semantics, grid termination),
runtime_ui (full lifecycle plus desktop/mobile budgets and soak).
cad.yml: CORE canonical-module gates, UI session-module gates
(switch/rebuild/journal/sandbox/ai/cache/BVH/coords/capture/exports),
integration-lane ownership notes; lanes otherwise unchanged.
2026-09-26 05:04:20 +03:00
64ae8d75ab feat(cad-ui): UI-03a project repository with injected roots and atomic saves 2026-09-25 18:57:49 +03:00
081121957e fix(cad-core): UI-02 domain builders record kind_hint (demo goes green) 2026-09-25 17:03:23 +03:00
d4ec1edd34 feat(cad-ui): UI-02 single session authority plus truth gate 2026-09-25 15:01:12 +03:00
a00a7b6790 ci(cad): UI-00 ignored-test inventory plus truth gates and lane split 2026-09-25 12:44:48 +03:00
a8167ac36f ci(cad): CORE-00 CAD-owned workflow plus fail-closed stale scans
Adds .forgejo/workflows/cad.yml owning cad-core, cad-ui and shared
tooling: non-empty source-root gates, exact ignored-test budget (20),
empty-fixture proof, locked cargo check/test/clippy/fmt.

Guards every stale nigig-build/.../workspace/cad scan and both
coverage harnesses to fail closed (exit 1 with move pointer) until
BUILD-00 removes/retargets them; an empty grep scan is never green.

Verified: sun 14/14 + measure 19/19 standalone (DVec3 shim);
coverage harnesses exit 1; stale-guard loop fail=0; git diff --check clean.
2026-09-14 11:25:09 +03:00
Arena Agent
75ec2f9f20 fix(nigig-site): harden macOS native evidence
Some checks failed
nigig-site / Owned paths and honest test contracts (push) Has been cancelled
nigig-site / Cargo check-all-targets (push) Has been cancelled
nigig-site / Cargo clippy-site-owned (push) Has been cancelled
nigig-site / Cargo contained-media-export-fixtures (push) Has been cancelled
nigig-site / Cargo containment-storage-crypto (push) Has been cancelled
nigig-site / Cargo integration-non-live (push) Has been cancelled
nigig-site / Cargo production-dependency-containment (push) Has been cancelled
nigig-site / Cargo site02-crypto (push) Has been cancelled
nigig-site / Cargo site02-repository (push) Has been cancelled
nigig-site / Cargo site02-store (push) Has been cancelled
nigig-site / Cargo unit (push) Has been cancelled
nigig-site / SITE-02 native provider/filesystem (ubuntu-latest) (push) Has been cancelled
nigig-site / SITE-02 native provider/filesystem (windows-latest) (push) Has been cancelled
nigig-site / SITE-02 desktop runtime and normal shutdown (push) Has been cancelled
nigig-site / SITE-02 migration, recovery, and fault corpus (push) Has been cancelled
nigig-site / Media limits (explicitly skipped until enabled) (push) Has been cancelled
nigig-site / Real server interoperability (explicitly skipped until enabled) (push) Has been cancelled
nigig-site / Security and supply-chain baseline (push) Has been cancelled
nigig-site / Release capability gate (push) Has been cancelled
nigig-site / SITE-02 native provider/filesystem (macos-latest) (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
2026-09-14 06:31:57 +00:00
Arena Agent
41259b096a ci(nigig-site): budget clean native builds honestly
Some checks failed
nigig-site / SITE-02 native provider/filesystem (macos-latest) (push) Failing after 2h0m16s
nigig-site / Owned paths and honest test contracts (push) Has been cancelled
nigig-site / Cargo check-all-targets (push) Has been cancelled
nigig-site / Cargo clippy-site-owned (push) Has been cancelled
nigig-site / Cargo contained-media-export-fixtures (push) Has been cancelled
nigig-site / Cargo containment-storage-crypto (push) Has been cancelled
nigig-site / Cargo integration-non-live (push) Has been cancelled
nigig-site / Cargo production-dependency-containment (push) Has been cancelled
nigig-site / Cargo site02-crypto (push) Has been cancelled
nigig-site / Cargo site02-repository (push) Has been cancelled
nigig-site / Cargo site02-store (push) Has been cancelled
nigig-site / Cargo unit (push) Has been cancelled
nigig-site / SITE-02 native provider/filesystem (ubuntu-latest) (push) Has been cancelled
nigig-site / SITE-02 native provider/filesystem (windows-latest) (push) Has been cancelled
nigig-site / SITE-02 desktop runtime and normal shutdown (push) Has been cancelled
nigig-site / SITE-02 migration, recovery, and fault corpus (push) Has been cancelled
nigig-site / Media limits (explicitly skipped until enabled) (push) Has been cancelled
nigig-site / Real server interoperability (explicitly skipped until enabled) (push) Has been cancelled
nigig-site / Security and supply-chain baseline (push) Has been cancelled
nigig-site / Release capability gate (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
2026-09-13 16:26:31 +00:00
Arena Agent
3c9dc7943a classify Site schema versions before decoding
Some checks failed
nigig-site / SITE-02 migration, recovery, and fault corpus (push) Has been cancelled
nigig-site / Media limits (explicitly skipped until enabled) (push) Has been cancelled
nigig-site / Real server interoperability (explicitly skipped until enabled) (push) Has been cancelled
nigig-site / Security and supply-chain baseline (push) Has been cancelled
nigig-site / Release capability gate (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
email / gates (push) Has been cancelled
nigig-map / test (push) Has been cancelled
sms / gates (push) Has been cancelled
spreadsheet / engine-coverage (push) Has been cancelled
traffic / gates (push) Has been cancelled
traffic / nigig-traffic (push) Has been cancelled
traffic / supply-chain (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
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
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
sms / robius-sms (push) Has been cancelled
sms / supply-chain (push) Has been cancelled
spreadsheet / ui-controller-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (push) Has been cancelled
sms / android (push) Has been cancelled
sms / nigig-sms (push) Has been cancelled
2026-09-13 07:22:38 +00:00
Arena Agent
bbce8fb015 harden SITE-02 repository and native vault contracts 2026-09-13 06:51:39 +00:00
Arena Agent
92f1508325 site: verify native vault and production dependency trust 2026-09-12 23:03:56 +00:00
Arena Agent
c568a99948 site: exercise abrupt SITE-02 publication exits 2026-09-12 22:21:22 +00:00
Arena Agent
8c786ae163 site: define blocked SITE-02 key lifecycle review 2026-09-12 22:18:22 +00:00
Arena Agent
6d6f887ba6 site: harden SITE-02 process and scope boundaries 2026-09-12 22:11:59 +00:00
Arena Agent
a7a057f44a site: gate SITE-02 runtime and security review 2026-09-12 21:54:30 +00:00
Arena Agent
5d2d890f70 feat(nigig-site): enforce SITE-01 fail-closed containment
Some checks failed
nigig-site / Migration and recovery (explicitly skipped until enabled) (push) Has been cancelled
nigig-site / Media limits (explicitly skipped until enabled) (push) Has been cancelled
nigig-site / Real server interoperability (explicitly skipped until enabled) (push) Has been cancelled
nigig-site / Security and supply-chain baseline (push) Has been cancelled
nigig-site / Release capability gate (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
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
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (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
spreadsheet / engine-coverage (push) Has been cancelled
spreadsheet / ui-controller-coverage (push) Has been cancelled
traffic / gates (push) Has been cancelled
traffic / nigig-traffic (push) Has been cancelled
traffic / supply-chain (push) Has been cancelled
Contain production capabilities, remove the production sync surface, and keep legacy media/export/transport implementations test-only.

Require authenticated existing-key storage with preservation-first recovery and sticky write disablement, backed by deterministic fault and concurrency tests plus dependency and workflow contracts.
2026-09-12 15:46:30 +00:00
Arena Agent
f49d8b16ac ci(nigig-site): establish truthful SITE-00 gates
Some checks failed
nigig-site / Owned paths and honest test contracts (push) Has been cancelled
nigig-site / Cargo check-all-targets (push) Has been cancelled
nigig-site / Cargo clippy-site-owned (push) Has been cancelled
nigig-site / Cargo integration-non-live (push) Has been cancelled
nigig-site / Cargo media-export-baseline (push) Has been cancelled
nigig-site / Cargo storage-crypto-baseline (push) Has been cancelled
nigig-site / Cargo unit (push) Has been cancelled
nigig-site / Runtime UI (explicitly skipped until enabled) (push) Has been cancelled
nigig-site / Migration and recovery (explicitly skipped until enabled) (push) Has been cancelled
nigig-site / Media limits (explicitly skipped until enabled) (push) Has been cancelled
nigig-site / Real server interoperability (explicitly skipped until enabled) (push) Has been cancelled
nigig-site / Security and supply-chain baseline (push) Has been cancelled
nigig-site / Release capability gate (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
2026-09-12 09:08:22 +00:00
ac8f8aa002 chore: sync full working tree to gitdab
Some checks failed
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (push) Has been cancelled
traffic / gates (push) Has been cancelled
traffic / nigig-traffic (push) Has been cancelled
traffic / supply-chain (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
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-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
spreadsheet / engine-coverage (push) Has been cancelled
spreadsheet / ui-controller-coverage (push) Has been cancelled
p2p-intel / engine (push) Has been cancelled
p2p-intel / notifications (push) Has been cancelled
p2p-intel / coverage (push) Has been cancelled
p2p-intel / makepad-app (push) Has been cancelled
p2p-intel / exchange-tab (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
Whole-tree sync: cad-core/cad-ui split sources, nigig-build
construction_frame migration, pdf port progress, mpesa/pay/uikit/doc
updates, workspace members/profiles/lock, CI workflows and reviews.
See individual file history for details.
2026-09-12 07:15:24 +03:00
3ef182414e feat(robius-notification): real macOS and Windows backends, type-checked against the actual frameworks
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
p2p-intel / engine (push) Has been cancelled
p2p-intel / notifications (push) Has been cancelled
p2p-intel / coverage (push) Has been cancelled
p2p-intel / makepad-app (push) Has been cancelled
p2p-intel / exchange-tab (push) Has been cancelled
The previous commit refused to write these two, on the grounds that code no
compiler has ever seen is not an implementation -- it is plausible-looking
text that would sit in the same crate as tested code and be read as equally
finished. That reasoning holds. The premise behind it did not.

`cargo check` needs the *target's standard library*, not a linker or a
platform SDK. `rustup target add x86_64-pc-windows-msvc aarch64-apple-darwin`
puts the real `windows` and `objc2` crates -- genuine WinRT metadata, genuine
Objective-C class definitions -- in front of the type checker on a Linux
host. So both backends are now written and both compile against the
frameworks they call.

Compile-checking earned its place immediately. The Windows backend was
written against `ToastNotificationManager::CreateToastNotifier(&HSTRING)`,
which does not exist: the AUMID overload is `CreateToastNotifierWithId`.
Nothing short of a compiler holding the real metadata would have caught
that, and it would have shipped looking entirely correct.

Because a cfg-gated module can silently compile to nothing -- leaving a
green check that proves only that the module was skipped -- each backend was
verified to be genuinely reachable by injecting a type error and confirming
the target build failed. Both macOS and Windows were checked this way, then
restored.

Two platform decisions worth recording.

macOS uses NSUserNotification, not UNUserNotificationCenter, and that is a
deliberate downgrade to a deprecated API. `UNUserNotificationCenter.current()`
raises an Objective-C exception when the process has no bundle identifier;
that unwinds through Rust frames and aborts. A plain `cargo run` host has no
bundle, so the modern API would crash the caller instead of reporting
unavailable -- which is worse than deprecated. The nil check on
`defaultUserNotificationCenter` is there for the same reason: msg_send on nil
returns zero rather than crashing, so every later call would silently do
nothing, which is precisely the failure this crate exists to remove.

Windows requires the host to supply an AppUserModelID, because a library
cannot invent one. It comes from an MSIX manifest or a Start Menu shortcut,
and a fabricated id produces a notifier that constructs happily and then
fails at Show. `set_app_user_model_id` is therefore public, a no-op off
Windows so portable hosts call it unconditionally, and `is_available()` is
false with a reason naming exactly what is missing until it is called. Toast
payloads are XML, so text is escaped -- the same bug class as the Telegram
MarkdownV2 escaping p2p-intel needed before it dropped Telegram, and a
merchant nickname containing `&` is not hypothetical.

The support table now has two columns, "compiles" and "executed", because
they are different claims. All four backends compile; only Linux has posted
a notification. iOS remains unavailable by design: it needs UN* with a
bundle and an entitlement, which is an app-packaging concern rather than
something this crate can satisfy.

Off Windows the module defines no entry points at all rather than stubs.
Clippy was right to call them dead: sys/mod.rs dispatches elsewhere, so they
existed only to satisfy a symmetry nothing needs.

CI gains a step that clippies all four cross-targets with -D warnings, so a
cfg-gated backend cannot rot unnoticed -- which is exactly how the
CreateToastNotifier mistake would have survived.

51 tests, clippy clean on five targets, p2p-intel still at 225.
2026-09-01 18:54:57 +00:00
e16d1d1d44 feat(robius-notification): a zero-dependency D-Bus notifier, and p2p-intel's last limit closes on Linux
Some checks failed
p2p-intel / engine (push) Has been cancelled
p2p-intel / notifications (push) Has been cancelled
p2p-intel / coverage (push) Has been cancelled
p2p-intel / makepad-app (push) Has been cancelled
p2p-intel / exchange-tab (push) Has been cancelled
repo hygiene / hygiene (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
doc-engine / engine (push) Has been cancelled
doc-engine / coverage (push) Has been cancelled
doc-engine / consumer (push) Has been cancelled
spreadsheet / engine-coverage (push) Has been cancelled
spreadsheet / ui-controller-coverage (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
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (push) Has been cancelled
nigig-map / test (push) Has been cancelled
ADR 0035 left one of p2p-intel's four limits open: no OS notification
backend, so alerts stopped when the window closed. The seam existed with one
implementation that truthfully did nothing. This is the crate that fills it.

Posting a notification on Linux is one D-Bus method call. Three ways to make
it were measured rather than assumed: notify-rust with libdbus is twelve
crates but a C library, which needs pkg-config and breaks the Android and
iOS cross-compile this crate family keeps clean; notify-rust with zbus is
pure Rust and 169 crates including an async executor; writing the wire
format out is about 250 lines and nothing at all. robius-sms deleted polkit
and gio for exactly this reason -- its E9 note records they were the sole
source of two RUSTSEC advisories and an LGPL question for every consumer --
so pulling a 169-crate tree back into the same family for one method call
would reverse that decision for a worse reason.

The cost of hand-rolling is that the protocol has to be exactly right, and a
mistake makes the daemon disconnect with no diagnostic. That cost was paid
in tests: the suite starts a private dbus-daemon per test and talks to it.
This matters more than it sounds. The marshaller and the parser were written
from the same reading of the specification, so them agreeing with each other
proves only that I was consistently wrong or consistently right; only a
third party can say which.

It found three bugs no unit test would have.

The first is the one worth dwelling on. Every error reply parsed as success.
The header-field walk assumed all fields were strings, but REPLY_SERIAL is a
u32, and reading its four bytes as a string length desynchronised the cursor
so ERROR_NAME was never reached. `post` returned Ok against a bus with no
notification service running. That is precisely the bug this crate was
written to eliminate -- a notifier that reports success and delivers nothing
-- reintroduced by accident inside its own parser. I cannot think of a
stronger argument for testing against something you did not write.

Second, is_available() was true on a bare bus, because NameHasOwner
*succeeds* and answers false in its body; checking only for an error
reported a working notifier on a machine with no notification daemon.

Third, replies were not correlated. The bus sends NameAcquired unprompted
right after Hello, so "read the next message" consumed a signal and treated
it as the answer. Replies are now matched on REPLY_SERIAL, and a single read
carrying several messages is walked rather than truncated.

The suite also serialises every test that mutates DBUS_SESSION_BUS_ADDRESS
behind a mutex. The variable is process-wide and cargo runs tests in
parallel threads; three consecutive parallel runs are now green.
--test-threads=1 would have made the failures go away too, and would have
hidden a real hazard from whoever reads the file next.

On the four platforms, honestly. Linux is implemented and tested. Android is
implemented and *compiles* -- cargo check and clippy both pass for
aarch64-linux-android -- but has never run on a device, and the module says
so in its first paragraph. It handles the two things Android drops silently,
missing POST_NOTIFICATIONS on API 33+ and a missing channel on API 26+,
because both are the same accepted-and-discarded failure this crate exists
to remove.

Apple and Windows are deliberately not written. Neither could be compiled
here -- no macOS or Windows toolchain and no way to add one -- and objc2
message sends or WinRT calls that no compiler has ever seen are not an
implementation. They are plausible-looking text that would sit in the same
crate as tested code and be read as equally finished. Both return
PermanentlyUnavailable with a reason naming ADR 0036, and their module docs
record the call sequence so the next person starts from a design rather than
a blank file. The support table says "written" and "verified" in separate
columns for the same reason.

p2p-intel's dashboard now uses SystemNotifications instead of
UnavailableNotifications. The latter stays: on a platform with no backend it
is still the truthful answer, and a test needs something that reliably
cannot deliver. Alerts are tagged per fiat so a market replaces its own
previous notification rather than stacking -- a 30-second poll would
otherwise fill the shade, and a full shade is what makes someone turn
notifications off for the app entirely, which costs more than the feature is
worth. Two new tests pin the invariant that a sink must never report
delivery it did not achieve.

CI gains a notifications job that installs dbus and sets
ROBIUS_NOTIFICATION_REQUIRE_DBUS=1. The bus-backed tests skip when
dbus-daemon is absent so the suite stays green on a bare machine, but a
silent skip in CI would mean the integration tests quietly stopped running
while the build stayed green. I verified the guard fails by hiding
dbus-daemon behind a stub that exits 127.

50 tests in the new crate, 225 in p2p-intel, clippy clean on host and
Android, and the app still starts under Xvfb.
2026-09-01 18:43:07 +00:00
8bf62e2644 feat(p2p-intel): the exchange tab becomes spread intelligence, with rails, drift capture, and three of four limits closed
Some checks failed
doc-engine / engine (push) Has been cancelled
doc-engine / coverage (push) Has been cancelled
doc-engine / consumer (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
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (push) Has been cancelled
nigig-map / test (push) Has been cancelled
repo hygiene / hygiene (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
spreadsheet / engine-coverage (push) Has been cancelled
spreadsheet / ui-controller-coverage (push) Has been cancelled
p2p-intel / engine (push) Has been cancelled
p2p-intel / coverage (push) Has been cancelled
p2p-intel / makepad-app (push) Has been cancelled
p2p-intel / exchange-tab (push) Has been cancelled
The exchange tab in nigig-mpesa and nigig-pay was a swap mock-up -- Sell/Buy
cards showing hardcoded ETH and AAVE at $38,409.24, wired to nothing -- above
six rate labels, each built from the *first* cached advert for its exchange:

    Binance P2P: USDT BUY @ 129.92 KES  min 1000 max 200000  via BANK

That is a price, not an opportunity. It never compared the two sides of the
book, said nothing about whether the counterparty could be dealt with, and
ignored what it costs to move the money. Against the live KES book it would
have shown a 134.60 advert from a merchant with three completed trades.

Both pages now run the p2p-intel analyzer over the adverts the app already
caches. No new endpoint, no new traffic, api.rs untouched -- the data was
always there, nothing was being asked of it. The two files are
byte-identical and CI now fails if they drift apart, which they already had
once: data.rs differs between the apps by 188 lines of tests one copy has
and the other does not.

**Rails, because a spread alone is a lie.** 29 bps on a 10,000 KES trade is
29 KES of gross margin; M-Pesa Send Money costs 55 KES a leg, 110 round
trip. The trade is deeply negative and the spread says nothing about it.
rail.rs prices every route and ranks by what survives. M-Pesa fees come from
the published Safaricom bands in robius_ussd::mpesa_bands -- real tariff
rows, not estimates, and flat rather than percentage, which is exactly why
the same spread is ruinous at 500 KES and fine at 200,000. Both legs are
charged. An amount outside the tariff reports OutOfRange and an
unconfigured bank reports Unknown; neither reports zero, because zero is a
claim and it is the wrong one. Bank tariffs ship unconfigured for the same
reason: every bank differs and there is no table to default to.

**API drift capture.** These endpoints are internal and undocumented. When
one changes the symptom is an empty panel -- indistinguishable from a quiet
market -- and the response that broke it is gone by the time anyone looks.
Every parse failure is now recorded with the payload excerpt that caused it
and copyable as a plain-text report. Deduplicated, because a 30-second poll
against a changed endpoint fails 120 times an hour and 120 identical rows is
a log nobody reads; the excerpt is excluded from the equality check, since
two responses differing only in advert ids are the same drift. The exported
header states that it carries response excerpts only and never a request,
credential or account number -- the type can only be constructed from a
response body, so that is enforced rather than promised. Cutting the excerpt
is done on character boundaries: Binance really returns names like
BennyBoss and a byte slice would panic mid-character.

The repo's other clipboard code (nigig-build's crdt_widget.rs) answers a
Hit::TextCopy, which is the query-driven path the platform uses for Ctrl+C
on a focused widget. Right for a text editor, wrong for a button exporting a
report the user never selected, so this uses cx.copy_to_clipboard and
confirms in the UI -- a copy button with no feedback is one people press
three times.

On the four limits, the honest scoreboard is two closed, one usefully
sidestepped, one open. ADR 0035 records why, because rounding all four up to
"addressed" would have been the easy write-up and the wrong one.

The poll timer and the host clock are closed outright. cx.start_interval
delivers the tick as an ordinary UI-thread event, so there is no runtime and
nothing to join at shutdown, and the interval reads through
effective_poll_seconds so the fifteen-second floor still applies -- a config
file cannot be used to hammer a rate-limited endpoint, which a test pins by
name. Staleness is now shown, because a price from four minutes ago is not a
price. One ambiguity is recorded rather than hidden: last_scan_ms == 0 is the
"never scanned" sentinel, so a scan whose timestamp genuinely is 0 reads as
never. That only happens when the host clock is broken, which is exactly
when the UI should not claim the data is current.

The headless-backend limit is sidestepped, and the distinction matters
enough to write down. Xvfb is a real X11 server that draws into memory, so
the app gets the display it insists on and tools/test-p2p-app-smoke.sh runs
the real binary end to end. That earned its place immediately: it caught
`Row = <View> { ... }`, which is not valid in this fork's script language
and which cargo build is entirely silent about, because script_mod! is
parsed at *runtime* -- a broken widget tree compiles perfectly and then
renders nothing. The gate greps for [E] in the log for that reason; the
process exits 0. I verified it fails by reintroducing the bad syntax. What
it still cannot do is drive widgets: makepad_test's Selector::id(..).click()
needs the harness to own the event loop, so the six #[ignore]d interaction
tests stay ignored.

OS notifications stay open. There is no robius-notification crate beside
robius-sms and robius-ussd, and Makepad exposes none on any target; building
one means NotificationCompat on Android, UNUserNotification on iOS and a
D-Bus call on Linux. What shipped is the seam plus one implementation named
UnavailableNotifications -- named for what it is, because a DefaultNotifier
that silently discarded every alert would read like a working feature at
every call site. is_delivering() returns false and the UI shows the reason,
so the user is told that alerts stop with the window instead of assuming
they are covered. A no-op reporting success is the exact "declared versus
delivered" failure ADR 0017 exists to prevent.

223 tests, coverage 97.03% with fifteen per-file floors -- seven of them new
and all measured, after I noticed an earlier edit had silently failed to
apply and the new files were being counted in the total but gated by
nothing. CI gains the Xvfb smoke job and an exchange-tab job that builds
both host apps and diffs their pages.
2026-09-01 16:07:43 +00:00
fefde1ecca refactor(p2p-intel): Makepad's HTTP stack, in-app alerts, and micro_serde -- serde and reqwest are gone
Some checks failed
p2p-intel / engine (push) Has been cancelled
p2p-intel / coverage (push) Has been cancelled
p2p-intel / makepad-app (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
Three changes that turn out to be one change: the app now uses the
platform's own facilities instead of carrying its own. Networking moves to
Cx::http_request, alerting moves into the UI, and deserialisation moves to
makepad_micro_serde. The default dependency graph drops from serde +
serde_derive + serde_json + toml + reqwest + rustls + tokio + hyper to
**twenty-five crates total**, none of which is any of those.

Networking follows nigig-mpesa/src/pages/exchange/api.rs: build an
HttpRequest, hand it to Cx::http_request keyed by a LiveId, match the reply
in handle_network_responses. There is no HTTP client, no TLS stack and no
async runtime in this workspace any more. That is not only leanness -- on
Android and iOS the platform stack is the only one that works without
shipping a second TLS implementation, so the `live` feature that gated
reqwest has been deleted rather than made default.

Correlating replies is where the real trap was. A scan of five markets puts
ten requests in flight and the replies come back in whatever order the
network gives them, so the LiveId has to say which market and which side.
RequestKey encodes both plus a generation counter and round-trips through a
u64 with the high bit set, so a LiveId Makepad derived from a name is never
decoded as a scan reply, and a late reply from a previous round is dropped
instead of folded into fresh data. Six tests cover the codec, including the
one that matters most: the two sides of one market must not share an id, or
the second reply overwrites the first and every spread is measured against
itself.

The transport allowlist is lifted from the nigig-mpesa review, which reached
the same conclusion I would have: Makepad exposes no certificate pinning --
its only TLS control is set_ignore_ssl_cert, which weakens verification --
so what is enforceable at this layer is that only HTTPS to p2p.binance.com
can be dialled at all. The tests cover the two ways a naive check leaks: the
lookalike host p2p.binance.com.evil.example, which passes any starts_with
test, and the userinfo smuggle https://p2p.binance.com@evil.example/, which
resolves to evil.example while reading as Binance.

Telegram is gone, as asked, and the app alerts itself: a banner, an unread
badge on the status line, an Alerts tab holding the history, and a chime.
Removing it removes a bot token from the threat model entirely -- a token in
a config file is a bot anyone who reads the file can drive -- and removes a
second network dependency from a tool already gated on one endpoint. One
test now records the *absence* of a bug rather than its fix: Twin_traders00
is a real merchant from the live capture, and its underscore previously had
to be escaped or Telegram rejected the whole message with a 400 and
delivered nothing. Rendering in our own UI deletes that failure mode, and
the test asserts the name appears unescaped.

The trade-off is stated in the README rather than glossed: an in-app alert
only reaches you while the app is running. No OS notification is raised, so
a minimised window is a missed alert.

The chime is synthesised rather than bundled -- a two-note rising blip
generated at the device's sample rate, which is a few dozen lines instead of
an audio asset shipped on three platforms, and which can therefore be
tested. It is, and the tests found the bugs you would expect from writing
audio: a freshly rendered chime starts *finished* so opening the output does
not announce itself at startup, both note edges fade so neither clicks, the
tail pads with silence rather than replaying whatever the buffer last held,
and a nonsense sample rate falls back instead of panicking. Rising rather
than falling because a falling interval reads as a dismissal and this is an
invitation to act.

The micro_serde migration surfaced two behaviours that differ from serde and
both bit before they were understood.

**micro_serde is strict by default.** deserialize_json errors on the first
key it does not model. Binance sends about forty fields per advert and we
model eight, so the strict parser cannot read the response at all -- and for
a config file it means an older build cannot open a file written by a newer
one. Everything uses deserialize_json_lenient, and a test pins that the
strict call *would* have failed, because the two differ by one word and the
strict one looks more correct.

**There is no #[serde(default)].** Optional config entries are modelled as
Option<T> on a Raw* struct and resolved into the real Config by hand. A few
more lines in exchange for two fewer dependency trees. config.toml became
config.json for the same arithmetic: micro_serde has no TOML reader, and
toml depends on serde, so a single config file would have dragged all of
serde back in through the back door.

Also worth recording: DeJsonErr implements Debug but not Display, and Debug
is the variant carrying line and column, so every error path formats it with
{e:?} deliberately rather than by accident.

CI gains a check that fails the build if reqwest, tokio, rustls, hyper,
serde, serde_derive, serde_json or toml reappears in the default graph. I
verified it fails by adding serde back to p2p-core and watching it match
serde_derive, then reverted. A gate that cannot fail is decoration.

136 tests in the default feature set and 36 more with --features ui,
including the RequestKey codec, which cannot be measured by the coverage job
because it lives behind the feature that needs Makepad. Coverage 96.92% with
ten per-file floors, up from eight -- chime.rs at 100% and client.rs at
95.69% are new. The desktop binary was built and linked to prove the app
half is real.
2026-09-01 09:37:20 +00:00
eaeebd3910 feat(p2p-intel): Binance P2P spread intelligence, shaped by what the live book actually contains
Some checks failed
p2p-intel / engine (push) Has been cancelled
p2p-intel / coverage (push) Has been cancelled
p2p-intel / makepad-app (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
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
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (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
spreadsheet / engine-coverage (push) Has been cancelled
spreadsheet / ui-controller-coverage (push) Has been cancelled
A new nested workspace under crates/apps/p2p-intel: six engine crates, a
CLI, and a Makepad dashboard that builds for desktop, Android and iOS. It
reads public P2P adverts, measures the spread that is actually fillable,
alerts when one is worth acting on, and tracks what the float really cost.

It never places an order. Binance publishes no P2P trading API, and
automating an escrow release is how a merchant loses their float to
chargeback fraud. This is the intelligence layer; execution stays manual.

The design came from a live capture rather than a sketch, and the capture
contradicted the sketch three times. All three are now pinned by tests
against checked-in real payloads.

**The best price is routinely the least fillable one.** In the KES book the
top sell advert was 134.60 from a merchant with three completed trades,
implying a 3.6% spread; the next was 130.30. Another advert showed a 0%
completion rate. A best-price scan with no quality floor does not find
opportunities, it finds outliers, and outliers on a P2P book are bait or a
merchant about to run dry. QualityFilter defaults to 95% completion and 50
orders, and analyse() reports every exclusion with its reason rather than
dropping it silently.

The honest consequence is recorded in the integration suite: at those
defaults **not one sell-side advert in the captured KES book qualified**.
There was no fillable arbitrage. A tool that reported the raw best-price
number would have sent its user after a trade that does not exist, so the
test asserts best_sell is None and net_bps is None rather than asserting a
comfortable number.

**tradeType is inverted between request and response.** Asking the endpoint
for tradeType "BUY" returns adverts whose own adv.tradeType reads "SELL".
Both are correct: the request parameter is what you want to do, the response
field is what the advertiser is doing. Conflating them inverts every spread
and the result still looks plausible, which makes it the most expensive
mistake available here. Side keeps the two apart with
request_trade_type()/advert_trade_type(), and a test asserts they are never
equal.

**An empty market answers HTTP 200 with success: true.** NGN returned zero
adverts. "No ads" and "no answer" need opposite responses, so
is_empty_market() is a named predicate and ScanError separates Malformed
(Binance changed the payload; retrying makes it worse) from Network
(transient). basis_points_above returns None against a zero base rather than
an infinity, so an empty book cannot read as an infinite opportunity at 3am.

Money is never a float, following the rule in nigig-pay-domain/src/money.rs.
IEEE 754 cannot represent 0.1 and a spread is a difference of two nearly
equal numbers, which is exactly where binary floating point loses the digits
that matter. Binance sends prices as decimal strings, so Price parses them
straight into scaled i128 integers and never passes through f64. i128 rather
than i64 because the intermediate in a bps calculation overflows, not the
result. Excess precision is refused rather than rounded and a thousands
separator is refused rather than dropped: "1,299.92" read as 129992 is a
1000x error that still looks like a price. Tests pin 0.1 + 0.2 == 0.3 and
rotate 100 round trips at one price asserting exactly zero P&L.

Alerting is mostly restraint. At a 30-second poll one wide spread would fire
120 identical messages an hour, and a channel that cries wolf gets muted, at
which point the tool has negative value because the user believes they are
covered. AlertGate suppresses repeats inside a cooldown and re-alerts early
only when the spread improves materially -- a collapsing spread is not worth
waking someone for. Telegram MarkdownV2 escaping is tested against a real
merchant name from the capture, Twin_traders00, whose underscore would
otherwise make Telegram reject the message with a 400 and deliver nothing.

Writing the dashboard view model found a bug in my own comparator: sorting
descending by swapping the tuple to (b, a) also silently swaps the meaning
of the None arms, which put dead markets at the top of the opportunity list.
The test that caught it was written first and named for the behaviour, not
the implementation.

Networking is behind a non-default `live` feature, so an ordinary cargo test
cannot make a request and CI never depends on Binance being reachable. A CI
step asserts reqwest is absent from the default dependency graph so this
cannot regress quietly. A live scan was run once to confirm the fixtures
match reality; it reported a negative spread for KES and an empty NGN book,
which is the tool working correctly.

Conventions follow the repo rather than the generic layout in the request:
.forgejo/workflows/p2p-intel.yml rather than .github, and no Dockerfile,
since the stack is pure Rust and nothing else here is containerised.
error_set is used instead of anyhow, matching nigig-core. The root
Cargo.toml excludes the nested workspace by name, as it already does for
makepad_table, so the isolation is intentional rather than dependent on a
table inside someone else's manifest.

106 tests, coverage 96.86% with per-file floors enforced by
tools/test-p2p-coverage.sh. Both the total and per-file gates were verified
to actually fail by running them with impossible floors; a gate that cannot
fail is decoration. Two files are excluded and only because they were first
emptied of decisions: the Makepad widget, which needs a GPU and a windowing
backend this repo has no headless backend for, and the CLI main, which is
argument parsing and println. Every rule the widget renders lives in
view_model.rs, measured at 97%. That split is deliberate --
spreadsheet-ui/grid.rs once hid 36 pure functions behind a file-level
exclusion, and excluding a file you have not emptied of logic is how that
happens.

The Makepad desktop binary was built and linked in the sandbox to prove the
app half is real and not just a compiling stub.
2026-09-01 09:13:43 +00:00
a5ba719d8d perf(cad): add measured sub-pixel LOD to the 2D path -- Phase 5
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
Phase 4 reduced the number of tessellation calls, but it left the geometry volume unchanged: a zoomed-out 2,000-part plan still queued a full outline for every visible part. At the measured 200 m site view a 1 m part is only about 2.7 logical pixels across, so the remaining geometry was not resolvable detail.

Add a pure lod policy that projects plane extents into logical pixels, keeps selected and hovered parts full detail, and conservatively falls back to a full outline for malformed state. The renderer and CadViewport::frame_budget call the same policy. Ordinary sub-pixel parts use one bounded 2x2 filled marker, while FrameBudget reports full outlines, markers, strokes and fills separately.

Also fold the remaining 2D 1.2 margins into render_budget::VIEW_MARGIN, extend the structural benchmark and coverage harness, correct the phase documentation, and explicitly leave 3D mesh LOD deferred until a real GPU/window measurement justifies a second geometry policy.
2026-08-26 15:40:02 +00:00
a2b05c56c9 feat(makepad-table): opt-in capabilities feature, and raise the matrix_client defect
Some checks failed
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 two caveats from the dependency investigation.

## The capabilities feature

Camera and location attachments are now available behind
`features = ["capabilities"]`, which pulls `nigig-uikit` and supplies
`UikitAttachmentProvider`.

Measured: 89 crates by default, 275 with the feature on. That cost is real
and it is inherent, not packaging waste. `camera_widget` imports
`send_geocode_request` and `request_map_tile` from `nigig-core`, both of
which call `spawn_async` — the shared Tokio runtime — and the first makes an
HTTPS call to Nominatim. A camera that geocodes needs an async runtime and an
HTTP client; there is no lighter honest version.

It is affordable because it is opt-in, and because any app enabling it
already depends on `nigig-core`, so that app's own tree grows by nothing.

Everything touching `nigig-uikit` is in one module, so the boundary is a file
rather than `#[cfg]` scattered through the widget. The provider holds no
widgets of its own: the host owns the `CameraWidget` already in its tree and
this asks it to open, because a provider that instantiated a second camera
would fight the first for the device.

A second request while one is outstanding is refused rather than overwriting.
The table turns that refusal into `AttachmentUnavailable`, so the user is
told the camera is busy instead of watching their first request vanish.

File picking is deliberately declined here — `robius-file-picker` already
ships unconditionally and costs nothing, and two paths for one job is one too
many.

Two CI gates, both verified to fail when they should: the opt-in build must
keep compiling, and the default build must pull none of `tokio`, `reqwest`,
`hyper`, `clap`, `csv`, `image`, `nigig-uikit` or `nigig-core`. The second
checks the resolved `cargo tree` rather than the manifest, because feature
unification can switch an optional dependency on from a sibling crate.

Tests 99 default, 105 with the feature. Both clippy-clean.

## The matrix_client defect

Raised in REVIEWS/MATRIX_CLIENT_FEATURE_GATE.md rather than fixed. It is not
my crate, nothing depends on the broken combination, and a blind fix could
change behaviour someone relies on.

`matrix_client` declares `native = ["dep:tokio", "dep:reqwest",
"dep:rusqlite"]` but its source gates on `#[cfg(not(target_arch =
"wasm32"))]`. Two switches for the same modules, so on a native target with
the feature off the modules compile and their dependencies do not — 19
errors, 26 ungated uses across 7 files. There is no CI job for the crate,
which is why it rotted unnoticed.

The note corrects an overstatement I made while arguing for the trait hook.
I said fixing this would unblock wasm. It would not: `matrix_client` already
builds clean for wasm32 with `--no-default-features`, and `nigig-core` has 8
wasm errors of its own (`crate::platform::spawn` missing) that have nothing
to do with it. The only broken combination is native-target-with-feature-off,
which nothing builds.

I also said earlier that `matrix_client` was heavy — it is a 7-dependency
local crate, not matrix-sdk. That was wrong and it inflated the case for the
trait hook; the note records the measured numbers instead.
2026-08-18 18:03:26 +00:00
9989043a37 ci: gate the coverage that was already measured and unenforced (Phase 0)
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
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (push) Has been cancelled
spreadsheet / engine-coverage (push) Has been cancelled
spreadsheet / ui-controller-coverage (push) Has been cancelled
Phase 0 of REVIEWS/REPO_COVERAGE_100_PLAN.md, and the reason it is
Phase 0: no new tests, no new measurement, just ratchets on numbers that
were already good and already decaying-capable.

**spreadsheet** — `tools/test-spreadsheet-coverage.sh` has had a 96
floor for the engine and another for the UI controllers, and no CI job
has ever run it. New `.forgejo/workflows/spreadsheet.yml`, two jobs:

  engine-coverage          98.83% of lines (floor 96)
  ui-controller-coverage   98.85% of lines (floor 96)

Split in two because the halves cost very differently. The engine is
pure Rust and finishes in about three minutes; the UI half has to build
Makepad's Linux backend to link a test binary. One job would hide an
engine regression behind a ten-minute build.

**CAD widget layer** — `cad-widget-coverage` in nigig-build.yml,
deliberately REPORT-ONLY. It sits at 13.25% of 10,637 lines with six
files at exactly zero, and a floor there would read as a blessing
rather than a debt. What the job buys is that the number is printed on
every push instead of being rediscovered in six months. The first real
input test should set a floor behind it.

Also corrects the plan. It claimed the doc workspace module was
ungated; it is not — nigig-build.yml has run doc-workspace-coverage
since before the plan was written. I had surveyed by grepping workflow
files for the word "coverage" and attributed nigig-build's coverage
jobs to CAD alone. I nearly committed a duplicate workflow on the
strength of it. The census table was right; the prose under it was not,
and the correction is in the file.

One thing checked and deliberately NOT changed: the spreadsheet script
appears to skip its UI half when the native packages are absent. It
does not. `makepad-native-libs.sh --check` returns 1, the script runs
under `set -e`, and it aborts. What misled me was reading `$?` after
piping the script into `tail` — which reports tail's status, not the
script's. The same class of mistake this repository's CI comments warn
about; no fix was needed and none was made.

Verified by running each job's exact command line:
  COVERAGE_TARGET=engine ./tools/test-spreadsheet-coverage.sh   rc=0
  COVERAGE_TARGET=ui     ./tools/test-spreadsheet-coverage.sh   floors met
  ./tools/test-cad-widget-coverage.sh                           13.25%, rc=0
2026-08-18 10:20:22 +00:00
3928063392 ci(email): raise the domain floor; record the finance-email path
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
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
email.yml: FLOOR 225 -> 230. The review doc records the email-to-finance
sharing and notes the chat/Matrix path remains unbuilt (matrix_client has
login+sync only).
2026-08-18 10:07:54 +00:00
nigig-ci
6e784fffee fix(ci): the sample_thread gate was inverted under pipefail
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
repo hygiene / hygiene (push) Has been cancelled
The 'Development sample data must not reach the UI' gate ran
  count=$(grep ... | wc -l)
under set -euo pipefail. When sample_thread is correctly ABSENT from
the UI, grep returns exit 1 (no matches), pipefail propagates it, and
set -e kills the script -- so the gate reported FAIL in the GOOD state
and would have passed in the BAD state.

Add || true so a zero-match result is counted as 0 and the gate
passes, as intended. Verified: all 11 gates now pass, and
sample_thread is confirmed gone from the UI crate (only in
email_store.rs, definition + tests).
2026-08-18 09:36:35 +00:00
nigig-ci
632479c964 fix(ci): email.yml has been invalid YAML for six commits
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
`python3 -c "yaml.safe_load(open('.forgejo/workflows/email.yml'))"` fails:

    mapping values are not allowed here
      in ".forgejo/workflows/email.yml", line 455, column 35

A workflow that does not parse does not fail -- it does not RUN. So every
gate in this file has been silently absent: the S2 password checks, the
multi-recipient regression check, the TLS check, the coverage floors. All
of them. The file has looked like protection while providing none.

Cause: the "Coverage floors" step was rewritten to call
tools/test-email-coverage.sh, and ten lines of the previous inline
implementation were left behind underneath the new `run:` scalar. YAML
reads the first `echo "$out" | grep -E '^test result:'` as a new mapping
key and gives up.

Broken by 3dab4a1 and still broken at 6bf138d -- six commits, every one of
which believed it was adding or tightening a gate.

Verified the removal is safe: tools/test-email-coverage.sh exists, is
executable, and enforces the floors itself (TOTAL_FLOOR=88 plus per-file
floors), so the orphaned lines were duplicating work the script already
does. Nothing was lost.

The repo's own repo-hygiene.yml WOULD have caught this -- it has an
"Every workflow file must be valid YAML" step, added precisely because
commit 8c9ccb9 once broke nigig-build.yml the same way and silently
disabled the CAD gates. I ran that step by hand against this tree and it
fails, correctly. It did not catch it because no runner is registered, so
repo-hygiene has never executed on these commits.

That is the actual lesson here and it is not about YAML: a gate that has
never run is indistinguishable from a gate that does not exist. This is
the third time in this crate's history that a check existed, looked
right, and was doing nothing.

After the fix: YAML valid, 4 jobs / 32 steps, all 12 source gates pass,
cargo check --all-targets clean on both crates, 213 email domain tests
pass, fmt clean.
2026-08-18 05:33:18 +00:00
6bf138d027 ci(email): cover the trip-report modules
Some checks failed
email.yml / ci(email): cover the trip-report modules (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
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
doc-engine / engine (push) Successful in 18s
doc-engine / coverage (push) Successful in 30s
doc-engine / consumer (push) Successful in 4m58s
nigig-map / test (push) Failing after 2m18s
sms / gates (push) Successful in 3s
sms / robius-sms (push) Failing after 11m46s
sms / android (push) Successful in 1m48s
sms / nigig-sms (push) Successful in 5m42s
sms / supply-chain (push) Successful in 7s
The domain test filter and floor (225) now include finance_report and
email_receipts, and test-email-coverage.sh instruments both new files.
Domain tests 216 -> 234; coverage 90.6% over 15 files. The review doc
records the new feature.
2026-08-17 12:08:19 +00:00
arena-agent
b478945c34 ci(doc): gate the doc-workspace coverage floor on every push
Some checks failed
email.yml / ci(doc): gate the doc-workspace coverage floor on every push (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
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
Adds the doc-workspace-coverage job to the nigig-build workflow,
mirroring the CAD gate: checkout, then
./tools/test-doc-workspace-coverage.sh, which installs its own
instrumented toolchain into a shell-trap-cleaned temp dir and fails
if the total floor (92%) or any per-file floor is not met. The script
joins the workflow's push/PR path filters next to
tools/test-cad-coverage.sh so edits to the harness itself re-run the
gate. The doc README gains the milestone section recording the
28.55% -> 96.76% line measurement, the honest exclusions (widget
layer, persistence write-path wrappers, defensive traversal guards)
and the behavior pins and defect fixes the drive surfaced.
2026-08-17 10:25:27 +00:00
b83e7122c4 feat(makepad-table): file picker, search, recents, New/Delete (Invoicer UI Phase 3)
Some checks failed
email.yml / feat(makepad-table): file picker, search, recents, New/Delete (Invoicer UI Phase 3) (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 open phase. The sidebar gains a search box, a filtered document
list and a recents list; the toolbar gains New Invoice/Quote/Receipt, Open,
Save As and Delete.

**The file picker is robius, not makepad's.** Makepad has an
`open_system_openfile_dialog`, and it is implemented on macOS only — the
Linux and Android backends never handle `CxOsOp::SelectFileDialog`, so the
op is queued and dropped. It compiles, it runs, the dialog never appears.
That is the worst kind of broken, so this uses `robius-file-picker`, the
same crate `nigig-build`, `nigig-pay-ui` and `nigig-sms` already depend on
at the same pinned revision, which goes through `rfd` on desktop and the
platform picker on Android. CI gates against the macOS-only call returning.

The picker's callback runs off the UI thread with no `Cx`, so it parks its
outcome in a mutex and signals; `drain_file_picker` applies it on the next
`Event::Signal`. Same shape as the SMS bulk CSV import.

Model additions, in `makepad-doc-model` so they are testable without a
window: `DocKind` with `blank()` constructors, `DocumentLibrary::create`,
`remove`, and `selection_after_remove`.

Decisions worth naming, because each has a wrong answer that looks fine:

- **A new document is empty**, not seeded from the samples. A blank invoice
  arriving with "Acme Studio LLC" on it invites someone to export it without
  noticing whose name is there. `issue_date` is blank too — there is no
  clock in that crate and a guessed date is worse than none.
- **Generated numbers cannot collide**, including with documents loaded from
  disk, and they reuse gaps left by deletions. The number becomes the
  filename: two documents called INV-1 save over each other and one is lost
  silently.
- **Delete removes the row, not the file.** Removing an entry from a list is
  not consent to delete a document off disk, and there is no undo here. The
  status line says the file is untouched.
- **Save reports "Choose where to save…", not "Saved."** The dialog being
  open is not the file being written.
- **Search filters on every keystroke**, unlike the header fields, which
  commit on Return. Every prefix of a query is a valid narrower search;
  there is no such thing as a half-typed one.
- **Searching does not move the selection.** Filtering is a view change, and
  switching the open document because a letter was typed loses the user's
  place.
- **`selection_after_remove` is separate and exhaustively tested.** Deleting
  before the selection shifts it, deleting the selection keeps the index
  unless it was last, deleting after it changes nothing, and emptying the
  library selects nothing. Every wrong answer silently shows a different
  document; one of them indexes out of range.

The document list is a fixed pool of 12 button slots rather than a
`PortalList`, because this app opens documents one at a time. The pool is
honest about its limit: anything past it renders as "+n more — narrow the
search to reach them" rather than being dropped.

Tests 79 -> 90. Six of them are the invoicer's first: `App` derives `Script`
and cannot be built outside a live `Cx`, so the sidebar's presentation logic
was extracted into four pure functions and tested there. Verified by
reintroducing six defects across the two crates — silent overflow, a
selection marker that shifts the indent, whitespace counting as a search,
colliding numbers, a selection that ignores the shift, and a `blank()` that
pre-fills.

Also fixed, all pre-existing and all now blocking the `-D warnings` gate
that has been running on these crates since the workflow was added:
`std::io::Error::new(ErrorKind::Other, _)` in two crates, a manual
`RangeInclusive::contains`, a manual `is_multiple_of`, a single-arm `match`,
and a duplicated `#[test]` attribute that was annotating one function twice
— which is why the count reads 36 rather than 37 here; no test was lost.

The sample data keeps its `12_000_00` money literals, where the last group
is the minor units and the number reads as "12,000.00" at a glance.
`inconsistent_digit_grouping` is allowed at the crate root with that
reasoning, rather than regrouping every amount into thousands and making
each one need arithmetic to check against its comment.
2026-08-17 10:01:43 +00:00
189377a3a3 ci(email): build the wasm path; document the closed §8 gaps
Some checks failed
email.yml / ci(email): build the wasm path; document the closed §8 gaps (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
doc-engine / engine (push) Has been cancelled
doc-engine / coverage (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: install the wasm32-unknown-unknown target and check the email
domain's credential-bearing wasm half (call_email_api, WasmFetchTransport,
set_email_api_url) so a browser-only breakage cannot reach main unseen.
The domain test floor ratchets 205 -> 210.

The review doc's §8 is rewritten: the TLS handshake, the wasm build, B1
and the test/clippy baselines are now executed/measured; the only entries
left are the ones that genuinely cannot run in CI (a live relay's cert, a
browser's fetch), stated with their exact reasons.
2026-08-17 09:39:04 +00:00
89ca5186c6 docs(pdf): Phase 4 is not 100% — audit it, and verify the half that is
Some checks failed
email.yml / docs(pdf): Phase 4 is not 100% — audit it, and verify the half that is (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
Asked whether Phase 4 was complete, I checked the tree instead of my own
commit message, and the commit message was wrong.

Three items named in the Phase 4 spec are **not** implemented, and the
status line said "complete" over them:

- **Field-value reconciliation** (`form_reconcile_test.dart`). Setting a
  value writes /V, marks the field dirty and regenerates /AP — that all
  works. What is missing is the reconciliation case: a file opened with
  /V and /AP *already disagreeing*, where the right answer depends on
  /NeedAppearances. Nothing decides that today.
- **Type1/CFF embedding.** The spec hedges with "if feasible", so this is
  a legitimate deferral rather than an oversight — but "complete" did not
  say so. `sfnt.rs` detects CFF outlines and `font.rs` reads an existing
  /FontFile3; nothing writes one. Creation is TrueType-only.
- **`repair-cmap`.** No equivalent exists.

`text_box_appearance_test.dart` *is* covered, by appearance.rs:235 — it
just does not carry that filename, which is why a grep for the dart test
names is a starting point and not an answer.

The other half of the exit criterion — "generated PDFs open cleanly in
external viewers" — had never been checked at all. The sample generator's
own doc comment admits no test in this repository can assert it. So I
ran it through implementations we share no code with, and **it passes**:

  qpdf --check           no syntax or stream encoding errors
  pdfinfo                title, author, subject, keywords, 2 pages,
                         Form: AcroForm
  pdftotext              all text, including the embedded DejaVu subset
                         and its em-dash
  qpdf --list-attachments  readme.txt, extracted by name with description
  catalogue              /Outlines /Names /EmbeddedFiles /PageLabels
                         /Dests /PageMode /ViewerPreferences /AcroForm

`tools/check-pdf-external-readers.sh` makes that repeatable, and pdf.yml
runs it. It treats a qpdf *warning* as failure, not just an error: qpdf
warns where it had to reconstruct, and reconstructing is exactly what a
stricter viewer will refuse to do. Negative-tested twice — removing the
attachment fails 3 checks, and corrupting the startxref offset makes
qpdf report "file is damaged".

Two defects that audit found:

- **The sample never exercised XMP**, so the Phase 4 feature most likely
  to be silently missing was also the one nothing looked at. Probed
  separately: `set_xmp_metadata` works, pdfinfo reports
  `Metadata Stream: yes`.
- **A `Banner` naming an unregistered font produces a structurally valid
  PDF that renders no text.** qpdf --check passes; poppler says
  `Unknown font tag 'F1'` and draws nothing. `stamp.rs` cannot register
  the font itself — fonts belong to the document, and a banner does not
  know which document it will be drawn into — so this is now documented
  on `Banner` with a worked example, and pinned by
  `a_banner_font_must_be_registered_or_the_page_lacks_the_resource`,
  which asserts on the page's /Font resources because that is the thing
  actually missing and the thing a caller can check.

The plan now records that it was wrong once, rather than quietly
correcting itself. A status line that has been overstated should show its
working.

Engine suite 952 -> 953. Phase 4's engine half is verified end to end
against third-party readers; the ui.rs interaction half is written and
still blocked on the Makepad headless backend.
2026-08-17 09:11:03 +00:00
fc0b1f287f ci(email): run the conversation-kit tests; mark Phase E complete
Some checks failed
email.yml / ci(email): run the conversation-kit tests; mark Phase E complete (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
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 domain test floor ratchets 190 -> 205, and the nigig-email
job runs cargo test -p nigig-uikit --lib -- conversation so an email-driven
regression in the shared kit cannot silently surface in SMS.

The review doc marks E1-E6 done and records the honest correction E5
surfaced: lettre's timeout bounds only the TCP connect, not the
greeting/command reads — the send path now bounds the whole operation.
2026-08-17 05:09:15 +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
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
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
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
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
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
nigig-ci
901cddc716 fix(email): abandon_send shipped as dead code; wire it and gate it (B6)
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
repo hygiene / hygiene (push) Has been cancelled
Auditing Phase B against the tree rather than against my own notes found
that abandon_send() existed in nigig-core and NOTHING called it. The user
had no way to stop waiting on a hung send. I had marked B6 "partial" for
the right reason -- lettre cannot cancel mid-transaction -- and missed
that the part I did implement was unreachable.

A control the user cannot reach is not a control. It is dead code wearing
a safety label, which is worse than an acknowledged gap because it reads
as done.

Now wired: while a send is in flight the Send button becomes "Stop
waiting". The label is deliberately not "Cancel" -- this does not stop
delivery, because once DATA is accepted the message is sent whether we
wait for the reply or not. It frees the UI and suppresses a result the
user has stopped caring about. The 20s timeout from A6 bounds the window.

New gate: abandon_send() must exist in nigig-core AND be called from the
UI. The wiring is the thing checked, not the function.

That gate was ALSO broken when first written -- it grepped for
`abandon_send()` across src/, and the comment block explaining why the
control exists mentions it by name, so unwiring the call left the gate
green. Same flaw as the B5 gate in the previous commit, found the same
way: delete the fix, watch the gate. Now excludes comment lines.

Twice in two commits I have written a gate that its own explanatory text
satisfied. Worth stating rather than quietly fixing: a gate is only
evidence if you have watched it fail.

Phase B verified closed: B1-B6 all done, 11 gates pass, 99 domain tests,
check --all-targets clean on both crates, fmt clean.
2026-08-16 20:04:41 +00:00
nigig-ci
d889cbecd4 ci(email): gate multi-recipient send, and a gate that did not work
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
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
Two new gates, and one of them was broken when I first wrote it.

B1 gate: the send path must call email_send::parse_recipients, must NOT
contain a single-Mailbox parse of the whole To field, and must add every
accepted recipient. Three checks rather than one, because each failure
mode is separately reachable.

B5 gate: spawn_send_email must keep the SEND_IN_FLIGHT swap.

THE B5 GATE DID NOT WORK AS FIRST WRITTEN. It grepped the whole file for
`SEND_IN_FLIGHT.swap(true`, and the unit TESTS for the guard contain that
same string -- so deleting the guard from production code left the gate
green. I found it by negative-testing, which is the only reason I know.
Now scoped to the text before `#[cfg(test)]`.

That is worth recording rather than quietly fixing: a gate whose own test
fixtures satisfy it is indistinguishable from a gate that works, and the
only way to tell them apart is to break the thing on purpose.

Negative tests, all confirmed firing:
  remove the list parse                     -> fires
  reintroduce `let to_mbox: Mailbox = ..`   -> fires
  delete the in-flight guard                -> fires (after the fix)
and all 10 gates pass on the clean tree.

Test floor 60 -> 95 (actual 99).

Bulk page: builds through EmailSendRequest, so a partly-invalid list
reports what was dropped instead of refusing everything, and requires a
second tap before sending. The prompt quotes the recipient count and any
duplicates or rejections, so the user knows what they are confirming.
Editing the message after arming re-prompts rather than sending the old
confirmation.
2026-08-16 19:51:21 +00:00
nigig-ci
b3d562f4e2 ci(email): gate the Phase A security properties, and surface the warning
Three new gates in email.yml, each negative-tested by reverting the fix
and confirming the gate fires:

  1. SmtpConfig.password must be a Secret, AND SmtpConfig must not derive
     Serialize/Deserialize. Two separate checks, because either alone
     re-opens S2: a Secret that gets serialised is still exposed, and a
     String that never gets serialised still Debug-prints.

  2. set_email_api_url must call email_api_url_is_safe. Checks both that
     the validator exists and that the setter uses it -- a validator
     nobody calls is decoration.

  3. tls_mode_for_port must exist, and Tls::None / Tls::Opportunistic must
     not appear. Opportunistic is the dangerous one: it silently accepts a
     downgrade, which is exactly the attack starttls_relay's Tls::Required
     prevents.

Negative tests, all confirmed firing:
  password: Secret -> String              gate fires
  re-add #[derive(.., Serialize)]         gate fires
  remove the validator call               gate fires
  introduce Tls::None                     gate fires
and all 8 gates pass on the clean tree.

Domain test floor raised 38 -> 60 (actual: 68) and the filter widened to
include the secret:: module, so the new tests are actually covered by the
floor rather than sitting outside it.

Also surfaces config_warning() in the setup flow, so a from/username
mismatch is shown while the user can still fix it, rather than becoming a
silent provider rejection later.

One YAML trap worth recording: the test filter ends in `secret::`, and a
bare trailing colon makes YAML parse the line as a mapping. The run string
has to be quoted. Caught by validating the workflow before committing,
which is the only reason this is not a broken pipeline.
2026-08-16 19:22:40 +00:00