Commit graph

48 commits

Author SHA1 Message Date
nigig-bot
bdf8fc9694 nigig-ocr OCR-T1: real Tesseract engine, honest UI state, correct ingest/export
Some checks failed
nigig-site / Cargo contained-media-export-fixtures (push) Has been cancelled
nigig-site / Cargo containment-storage-crypto (push) Has been cancelled
nigig-site / Cargo core-clippy (push) Has been cancelled
nigig-site / Cargo core-contracts (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 (macos-latest) (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
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
traffic / gates (push) Has been cancelled
traffic / nigig-traffic (push) Has been cancelled
traffic / supply-chain (push) Has been cancelled
nigig-site / Release capability gate (push) Has been cancelled
- providers/tesseract: system tesseract (TSV → text + normalized word boxes),
  installed-language detection; default_engine() picks Vision/Tesseract/none
- image_prep: decode, validate, apply rotation before recognition
- jobs: background OcrJob with progress, cancel, per-item results
- ui_state: no fake success toasts; clipboard/export/share report real outcomes
- ingest: real decode validation, size caps, shared PDF pages (no duplication)
- export: atomic writes, paginated multi-page PDF, refuse non-WinAnsi text
- capture: fixed drag anchor; settings: atomic save + applied to new files
- drop unused deps; tests: 78 pass incl. 5 real-OCR on a fixture
- EXECUTION_PLAN: correct overstated status
2026-09-26 18:35:10 +00:00
Arena Agent
12f59109d7 revert(nigig-site): ce9e12b was wrong — the HTTP wire field is body, not text
Commits dfffe9e + 13ae501 landed nigig-lite/crates/common (package
`nigig-common`) and a vendored xitca-web into this repository, which makes the
/sync wire format readable for the first time. Reading it shows ce9e12b was
incorrect.

There are TWO MessageContent types, deliberately:
- nigig_common::MessageContent::Text { body, formatted_body, mentions } — the
  HTTP/JSON REST DTO. Its own doc-comment: "Serialises externally tagged,
  e.g. {"Text":{"body":"hi",…}}". This is what SendMessageRequest.content and
  TimelineEvent::Message.content are.
- nimanyatta_protocol::MessageContent::Text { text } — the WebSocket chat
  protocol, serialized with postcard. nigig-common's manifest says that format
  "must match nimanyatta/crates/nimanyatta-protocol (the client library)".

ce9e12b changed the E2E test's outgoing posts from {"Text":{"body":…}} to
{"Text":{"text":…}}, reasoning from the WebSocket type. The original `body`
was correct for these HTTP endpoints; my change would have made both requests
fail deserialization. Reverted, and the now-unused nimanyatta-protocol
dev-dependency is dropped from nigig-site.

What ce9e12b got right is kept: the parser had keyed on "type":"message",
but TimelineEvent carries no serde tag attribute, so it is externally tagged —
{"Message":{…,"sender":…,"content":…}} with sender and content at the variant
level. event_text still accepts that envelope (plus the legacy lowercase shape
and `text` as last-resort field names).

The fixture's cross-check test was rebuilt against nigig_common's real shape
instead of the WebSocket type. It is NOT verified by execution: nigig-site's
test target pulls in makepad-widgets, which is SIGKILLed on this host. The
parser was verified instead by extracting event_text/sync_timelines verbatim
into a standalone harness over four cases — nigig_common Message with `body`
-> Some(("alice","slab done")); Membership -> None; Redaction -> None; legacy
lowercase -> Some(("bob","old")). All pass. The harness is scratch, not
shipped.

EXECUTION_PLAN.md SITE-12 rewritten: the sibling repos are no longer missing,
the /sync DTOs are named with their file paths, both MessageContent types are
distinguished, and the narrower remaining blockers are stated — nimanyatta
still cannot be a workspace member (xitca-web/web/Cargo.toml inherits
rust-version and lints from a workspace root, and nested here it resolves to
ours, which defines neither), and a full `cargo check --features b_server` has
not completed on this host, so no build claim is made.

Verified: cargo test -p nimanyatta-protocol = 14; cargo test -p nigig-site-core
= 228 / 1 ignored; clippy -D warnings clean; cargo fmt --check clean;
cargo metadata exit 0.
2026-09-26 17:39:20 +00:00
andodeki
5aac7e3920 fix(nigig-traffic): TRAFFIC-01 close P0-01 — coherent Makepad pin, green build/tests
Some checks failed
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
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 / 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
- Re-pin workspace makepad b580de1 -> a3d8f521 (fork branch
  nigig-traffic-camera-boom-fix: 5-arg camera_boom_limit callers +
  EntityLocal particle anchor port). Cargo.lock updated atomically.
- Fix Walk literal broken by upstream fields (scenarios.rs).
- First executed test run: reword two completion strings that tripped
  the banned-claim gate (gate unchanged).
- Zero Traffic-owned compiler/clippy diagnostics; cargo fmt.
- Docs: plan status + release gates, IMPLEMENTATION_STATUS round 3.

Verified: check --all-targets clean; test 117 pass / 0 fail / 4 ignored
(GUI); clippy 0 owned diagnostics; fmt; diff --check.
2026-09-26 13:06:50 +00:00
Arena Agent
ac40ac923f fix(nigig-site): read the timeline field the protocol crate actually emits
Some checks failed
nigig-site / Cargo contained-media-export-fixtures (push) Has been cancelled
nigig-site / Cargo containment-storage-crypto (push) Has been cancelled
nigig-site / Cargo core-clippy (push) Has been cancelled
nigig-site / Cargo core-contracts (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 (macos-latest) (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
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
Pinning nimanyatta-protocol's JSON shape exposed a live bug in the E2E
fixture, and answering "is nigig_common in this repo?" narrowed the SITE-12
gap.

The bug: MessageContent::Text serializes as {"Text":{"text":"..."}}.
tests/support/nimanyatta_fixture.rs::event_text only looked for
content.Text.body — a field that type never emits, and which
serde_json::from_str::<MessageContent> actively rejects. Against a real
server every message would have been skipped, sync_timelines would return an
empty vec, and live_round_trip would still have passed. A silently vacuous
interop test is exactly the failure mode SITE-00 exists to prevent.

- event_text now tries `text` first and keeps `body` as a fallback.
- New test in nimanyatta-protocol pins the serialized shape and asserts the
  fixture's old guess does not decode.
- New cross-check test in the fixture builds its payload from the real
  MessageContent type, so parser and protocol cannot diverge silently again.

What was verified and what was not:
- The protocol-crate tests run: 14 passed.
- The fixture cross-check test COULD NOT be compiled or run here. nigig-site's
  test target pulls in makepad-widgets, which is SIGKILLed on this 2 vCPU /
  1.9 GiB host. The parser change was instead verified by extracting
  event_text/sync_timelines verbatim from the fixture into a standalone
  harness: {"Text":{"text":"slab done"}} -> Some(("alice","slab done")),
  the legacy {"Text":{"body":...}} shape still falls back, and a
  non-message event returns None. That harness is not part of the shipped
  tree; the shipped cross-check test is unverified until CI runs it.

Also recorded in EXECUTION_PLAN.md SITE-12: nigig_common is confirmed absent
from this repository (no package by that name; RoomEvent/RoomInfo/
JoinedRoomSync/SyncFilter/PaginationDirection defined nowhere here; nigig-core
is an email/IMAP crate), and it re-exports nimanyatta-protocol via a
`protocol` module — the server writes
nigig_common::protocol::{ClientToServerMsg, ServerToClientMsg}. So of the 75
items the server imports from nigig_common, 9 are the crate already pinned
here; the remaining 66 are the HTTP DTO layer, ~11 of which /sync needs.

Verified: cargo test -p nimanyatta-protocol = 14 passed; cargo test -p
nigig-site-core = 228 passed / 1 ignored; clippy -D warnings clean;
cargo fmt --check clean; cargo metadata exit 0; all 6 workflow python gates.
2026-09-26 12:37:08 +00:00
Arena Agent
3d91404ab6 build(nimanyatta): make the protocol crate a workspace member and pin its wire format
Some checks failed
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 core-clippy (push) Has been cancelled
nigig-site / Cargo core-contracts (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 (macos-latest) (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
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
First concrete step on SITE-12. Three of the four planned steps turned out to
be impossible from this repository, and that is now recorded with evidence
rather than assumed.

What this commit does:

1. `nimanyatta/crates/nimanyatta-protocol` is a workspace member. It has no
   path dependencies (only optional serde + postcard), so unlike its parent
   package it can be compiled and tested here. `cargo metadata` still exits 0.

2. Its 8 pre-existing tests HAD NEVER COMPILED. The crate is `#![no_std]`, so
   `use super::*` brings in alloc's `String` but not the `ToString` trait, and
   the test module calls `.to_string()` — 6 E0599 errors. Fixed with one
   import; 8 tests now run and pass.

3. Added 5 wire-format characterization tests pinning the exact postcard
   bytes. postcard encodes enum variants as POSITIONAL INDICES, so reordering
   or inserting a variant silently changes every frame on the wire while the
   code still compiles and every round-trip test still passes. Verified by
   mutation: swapping AuthMethod::Password/Token changes the login frame from
   [0, 0, 5, ...] to [0, 1, 5, ...] and the pinned test fails.

4. Corrected a false doc comment. `sync_protocol.rs` claimed "one canonical
   encoding is used by app, server, and tests". It is not. Read from source:
   the server's POST /sync uses Json<SyncRequest>/Json<SyncResponse> whose
   types come from `nigig_common` (not from nimanyatta-protocol, which is a
   WebSocket chat protocol the server uses only for `is_guest`), and
   sync_protocol.rs is a third, separate offline-envelope codec.

What could not be done, with evidence:

- `nimanyatta` itself cannot be a workspace member: adding it makes
  `cargo metadata` fail with exit 101 "failed to load manifest for workspace
  member", because Cargo resolves path dependencies even for features that are
  not enabled, and ../nigig-lite/crates/common and ../xitca-web/web do not
  exist. Verified by adding it, observing 101, and removing it.
- Neither sibling repo can be vendored or submoduled: `git ls-remote` on
  gitdab.com/andodeki/nigig-lite and /xitca-web both return HTTP 500,
  identical to a deliberately fake repo name, while /nigig-org resolves
  normally.
- Repointing xitca-web to crates.io would not unblock the build on its own:
  `nigig_common` is imported by 38 server files (~40 items) and has no
  published equivalent. Guessing RoomInfo/RoomEvent field definitions would
  produce a codec that silently mismatches the real server, which is worse
  than the current explicit "three incompatible protocols" state.

rustfmt was applied to the protocol crate (120 insertions / 29 deletions, all
whitespace: it had never been formatted, including a stray indent on
ServerToClientMsg::login_success). No semantic change.

Verified: cargo test -p nimanyatta-protocol = 13 passed; cargo test -p
nigig-site-core = 228 passed / 1 ignored; clippy -D warnings clean on both;
cargo fmt --check clean; cargo check -p nigig-site --locked clean; cargo
metadata exit 0; all 6 workflow python gates pass.
2026-09-26 11:53:58 +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
Arena Agent
bbce8fb015 harden SITE-02 repository and native vault contracts 2026-09-13 06:51:39 +00:00
Arena Agent
6d6f887ba6 site: harden SITE-02 process and scope boundaries 2026-09-12 22:11:59 +00:00
Arena Agent
8dbfae8f72 site: add SITE-02 encrypted repository candidate 2026-09-12 21:44:00 +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
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
f177df8d43 chore(deps): bump makepad fork rev to 66cc4f15f (fix broken widgets merge)
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
nigig-map / test (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
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
b9a083c26 carried an upstream merge that dropped enter_isolate /
leave_isolate / IsolateEntry from widget_async while lib.rs and
apps/flow-ui + apps/wm still use them, breaking makepad-widgets.
66cc4f15f restores them (verified: makepad-widgets, makepad-app-flow-ui,
makepad-wm check clean). Urgent follow-up to c901f1d which pinned the
broken rev.
2026-09-10 19:21:21 +03:00
c901f1d4f1 chore(deps): bump makepad fork rev to b9a083c26 + workspace updates
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
Fork nigig-makepad-test-android now at b9a083c26 (upstream merge +
robot secondary-dex bundling + app_main wrapper fix, on top of the
game-libs/arcade port). All 16 workspace makepad revs follow.

Also takes the in-tree workspace updates: exclude nested
crates/nimanyatta workspace, add crates/apps/nigig-site member, and
PERF-OPS Phase 1 profile tuning (thin LTO + engine opt-level 3).
cad-core/cad-ui split and nigig-traffic members stay WIP.
2026-09-10 16:36:11 +03:00
f1eecd3c97 chore(deps): bump makepad fork rev to 435135ed5 (port game libs + arcade)
Some checks failed
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
Fork nigig-makepad-test-android now at 435135ed5: reintroduces
libs/game (assets, audio, blocks, coedit, gen, net, pkg, render,
script, session) + apps/arcade reconciled to fork's libs/sim +
splash_storage jail model; arcade AI rewritten to makepad-ai-hub
ChatProvider worker (CAD pattern). nigig-build verifies.

Includes makepad-game-* workspace deps at new rev (traffic WIP
gated on this merge). cad-core/cad-ui split remains WIP.
2026-09-06 03:12:48 +03:00
95b89cf8d3 chore(deps): bump makepad fork rev to 531114e84 (rebase onto latest kerja/dev via 493d23a76)
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
2026-09-03 11:47:02 +03:00
212abe6e8b refactor(cad): port AI subsystem from makepad-ai Agent API to makepad-ai-hub worker-thread ChatProvider model (adopt upstream example/cad), bump makepad fork rev to 7bddebb
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
2026-09-03 08:39:07 +03:00
3a3db4efa6 feat(robius-notification): iOS via UNUserNotificationCenter, and the Apple/Windows logic comes out of the cfg blocks
Some checks failed
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-map / test (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
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
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
Two gaps remained on the Apple and Windows side after the last commit, and
they are different in kind.

The first was a missing backend. iOS returned PermanentlyUnavailable with a
note saying it was not implemented. It now uses UNUserNotificationCenter --
the only option there, since every iOS process is bundled, so the
Objective-C exception that rules that API out on macOS cannot occur. The
objc2-user-notifications bindings are real and fetchable, so this
type-checks for aarch64-apple-ios against the actual framework, and the
module was confirmed genuinely reachable by injecting a type error and
watching the target build fail.

Its is_available() returns a constant false, and that is a limit worth
naming rather than burying. The real answer comes from
getNotificationSettingsWithCompletionHandler:, which is asynchronous -- it
hands the settings to a block on an arbitrary queue. A synchronous
is_available() could only produce that by blocking on a completion handler,
which on the main thread is a deadlock rather than a delay. Returning false
errs toward telling the user delivery is unverified; the alternative is
claiming an availability the platform never confirmed, which is the whole
failure this crate was written to remove. A correct answer needs an async
entry point, which is a change to the public API rather than a bug fix, so
it is recorded in the ADR instead of being quietly wrong.

The second gap is subtler and, I think, the more valuable fix. The Apple and
Windows backends contain pure logic -- XML escaping, tag clamping -- that
was sitting inside #[cfg(target_os = "windows")], where cargo test on the
only available machine could never reach it. Those functions had zero tests
and no prospect of any.

They now live in src/payload.rs, which is cfg-free and runs on every target.
Eleven tests cover them, and they cover exactly the rules a compiler cannot:
an unescaped `&` in a merchant name makes the toast XML malformed and
Windows discards the whole notification rather than showing a mangled
character, and the Binance P2P book is full of `&`. A byte-wise truncation
of the 64-character tag limit panics outright on the full-width names that
book also contains -- so the clamp cuts on character boundaries, pinned by a
test that would panic if anyone changed it back.

One test pins something deliberately counter-intuitive: escaping is not
idempotent, and escape_xml("&amp;") is "&amp;amp;". That is correct, and the
test exists so nobody "fixes" double-escaping by teaching the function to
detect already-escaped input, which is precisely how escaping filters grow
holes.

The distinction this commit is really about: the last one made the Apple and
Windows backends *compile*, which catches wrong selectors -- it found
CreateToastNotifier(&HSTRING), which does not exist. It could not catch
wrong behaviour. Extracting the logic is what makes the behaviour testable
on a machine that will never run either platform.

62 tests, up from 51. Clippy clean with -D warnings on all five targets:
linux-gnu, linux-android, apple-darwin, apple-ios, windows-msvc. p2p-intel
unchanged at 225.
2026-09-01 20:10:50 +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
1d8b3a6053 merge(local): reapply local WIP onto merged main -- cad dashboard/explode/script_parts/xray merged with remote Phase-5 LOD, plus doc-ui extraction, spreadsheet xls-import, android ussd, camera and test work
Some checks failed
makepad-table / model (push) Has been cancelled
makepad-table / widget (push) Has been cancelled
makepad-table / 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
Payment domain, storage, platform and UI / isolated-payment-tests (push) Has been cancelled
Payment domain, storage, platform and UI / payment-ui-tests (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
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
2026-09-01 21:13:52 +03:00
159f3c7fee Merge remote-tracking branch 'origin/main' 2026-09-01 20:31:44 +03: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
c7c0ec7582 makepad: centralize fork deps in workspace + bump to 4a166606c
Declare the 12 makepad crate deps once in root [workspace.dependencies]
pointing at the gitdab fork rev 4a166606c (which now also carries
[workspace.dependencies] into the android wrapper manifest). Member
crates switch to workspace = true; 5 non-member workspaces get an inline
rev bump. Fix 9 nigig-app script_mod indent errors surfaced by the newer
upstream macro parser.

The fork rev includes: NIGIG test-mode forwarding, custom AndroidManifest
hook, ortho camera support, and the wrapper workspace-deps fix.
2026-08-28 10:49:41 +03:00
7a55f99c3a Merge remote-tracking branch 'origin/main' 2026-08-26 18:04:15 +03:00
a82c8f7ff7 feat(pdf): Unicode-aware search and layout-aware reading order
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
spreadsheet / engine-coverage (push) Has been cancelled
spreadsheet / ui-controller-coverage (push) Has been cancelled
doc-engine / engine (push) Has been cancelled
doc-engine / coverage (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 / 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
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (push) Has been cancelled
sms / gates (push) Has been cancelled
Phase 8 bullets one and two. Probing the existing code first, as the
workflow requires, found five defects rather than the one the plan names:

    SPLIT MATCH 'Hello': 0 hits
      plain_text: "Hello"
    PRECOMPOSED 'café': 0 hits
    COLUMNS plain_text: "LeftTopRightTop\nLeftBotRightBot"
    OUT OF ORDER plain_text: "second\nfirst"

`PageText::find` searched one run at a time and documented that as a known
limitation. It is a limitation from inside the code and a broken feature
from outside it: a writer starts a new run wherever it adjusts kerning, so
an ordinary word arrives as two runs, and the find bar says a word plainly
visible on the page is not there.

`search.rs` indexes the page as one flattened string with a map back to
(run, character), so a cross-run match is found and highlighted with one
rectangle per run — never a merged box, which across a line break covers
half the paragraph.

The separator between two runs is a geometric question with three answers:
abutting runs join with nothing (one word, split by kerning), separated
runs with a space, and a different line or column with a newline. The
newline matters as much as the empty join: joining lines with a space lets
"one Right" match across a column gutter, text that appears nowhere.
Whether two runs share a column is *asked* of the layout analysis rather
than re-derived, or the extracted text and the searched text disagree about
where a column ends — the original defect wearing a different hat.

NFD, never NFC: composition needs the next character, so an NFC fold
applied per character composes nothing and the two spellings of an accent
stay different. That was a real bug in the first draft. And case *folding*,
not lowercasing — Rust lowercases ß to ß, so "Strasse" never found
"Straße".

Columns are detected before lines, because two columns share their
baselines; that is what makes them columns. Bands are separated by a gutter
rather than by bare non-overlap, since two abutting runs on a line do not
overlap either.

Also fixed, found by running the gates rather than by looking: a stream
reader trimmed a trailing CR before `endstream` as if it were the writer's
separator. Binary data ends in CR about one time in 256, and when it did
the reader returned a stream one byte short — no longer AES-block-aligned,
so decryption produced garbage and Flate failed. Roughly one encrypted
document in 250 was silently corrupt on read. The test failed once under
coverage, passed five times in isolation, and failed 2 in 40 when actually
counted. A /Length consistent with the file is now the authority; both
stream readers are fixed and a test reads one file through each.

1477 tests pass (was 1426), coverage 88.37%, all floors met, external
readers pass. 10 mutations across the two modules, all killed.

ADR 0034.
2026-08-19 16:12:12 +00:00
72b0ce250b feat(doc-ui): extract doc module from nigig-build into standalone crate
Move 40+ files (~17K lines) from nigig-build/src/construction_frame/pages/workspace/doc/
to crates/apps/doc/doc-ui/src/. Rewrite internal paths from
crate::construction_frame::pages::workspace::doc:: to crate::.

doc-ui depends on doc-engine, makepad-widgets, nigig-core, serde, serde_json.
nigig-build now depends on doc-ui instead of doc-engine directly.
2026-08-19 00:55:05 +03:00
9a5ce9c0e6 feat(pdf): signing and verification — Valid becomes reachable, with a policy
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
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (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
ADR 0025, completing Phase 6's functional core. This partially reverses
ADR 0010, which refused signing and cryptographic verification outright,
and it reverses only the half whose justification expired.

ADR 0010 gave two reasons. The first — a parsing library has no business
signing — stopped being true when Phase 4 began creating documents and
Phase 5 editing them. The second is still true and is preserved intact:

  deciding which certificate authorities to trust is a policy decision
  that belongs to the host, not to a parsing library

So VerificationStatus::Valid is still not reachable by default. Verification
returns three independent booleans and is_valid() needs all three; the
third, chain_trusted, can only become true through a caller-supplied
TrustAnchors. There is no TrustAnchors::system(), no bundled root store, no
Default that trusts anything. A caller with no policy is told
"cryptographically intact, signed by somebody you have not said you trust"
— a different fact from "forged", and a host that cannot tell them apart
shows the wrong thing to a user.

RSA PKCS#1 v1.5, ECDSA P-256 and Ed25519, all with SHA-256. PSS is stronger
and not universally accepted by PDF verifiers, so v1.5 is what is written.
Ed25519 carries an interoperability caveat in the doc comment on the
variant itself, because that is where someone choosing it will read it:
ISO 32000-2 does not list it and most desktop viewers will reject it.

No network. Revocation is not implemented rather than smuggled in: the
engine crates are CI-gated against reaching outward, and that gate is a
rule about layering, not an obstacle to work around.

Every test generates a real key and a real certificate at run time. Nothing
asserts against a checked-in blob — a fixed expectation only proves the
code still does what it did, which is the wrong question for a signature.
The tampering tests assert the signature verifies FIRST, then flip a bit;
without that half they could pass by never verifying anything.

Four mutations, all killed. The one that matters is the first: making an
empty anchor set confer trust is exactly the regression that would turn
this back into the thing ADR 0010 refused, and it fails immediately.

Two bugs the tests found:

  UTCTime cannot encode a year past 2049 (RFC 5280 4.1.2.5.1). The first
  fixture used a 2096 expiry and every certificate failed to encode.

  The certificate scanner assumed a two-byte DER length. RSA certificates
  are large enough to use that form, so RSA and P-256 passed while Ed25519
  found no certificate at all — its certificate is small enough for the
  short form. A scanner tested only against the largest input fails
  silently on the smallest.

72 dependency packages pulled in, zero non-compliant licences, no C.

Stated plainly and left unticked in the ADR: chain_trusted is anchor
identity matching, not PKIX path building. Correct for certificate pinning,
a false negative for a real CA hierarchy. Also outstanding: revocation,
signature appearance generation, and one-call incremental signing.

pdf: 1247 passed. Coverage 88.08%, floors met.
2026-08-18 09:45:01 +00:00
d4e3e9a443 feat(pdf): encryption on save — AES-128 and AES-256 (Phase 6, part one)
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
doc-engine / engine (push) Successful in 21s
doc-engine / coverage (push) Successful in 31s
doc-engine / consumer (push) Failing after 16m57s
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
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (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
ADR 0024. This reverses ADR 0005's "never write encryption", and the
reason it is safe to reverse is that the facts changed underneath it.

A crate that only reads cannot produce weak ciphertext, so refusing to
write any was free. Now that Phase 4 creates documents and Phase 5 edits
them, the refusal does something worse than protect nobody: open a
password-protected file, change one annotation, save, and the output is
plaintext. No error, no warning — the protection is silently dropped. That
is this project's recurring failure mode in the one place where the
consequence is a breach.

The principle survives in a narrower form: no hand-rolled crypto, and no
weak cipher offered as an option. RC4 stays readable because files use it
and is not writable — EncryptionAlgorithm has no RC4 variant, so the
refusal is a type, not a runtime check someone can route around.

The encryptor is the literal inverse of the decryptor and imports its
primitives rather than restating them; two implementations of one algorithm
drift, and here they drift towards "decrypts to garbage". Every unit test
round-trips through the existing Decryptor.

Encryption sits at one choke point: PdfWriter holds the Encryptor and
write_object_at encrypts everything passing through. Not per call site —
there are twenty-two of those in PdfDocBuilder, and one stream written in
the clear inside an encrypted document is not a partial failure, it is a
leak that no reader will report because the file is otherwise valid. The
/Encrypt dictionary is the single deliberate exemption: it holds the salts
a reader needs before it has a key, so encrypting it bricks the file.

Verified against implementations we share no code with, now gated in CI:

  ok    qpdf opens it with the password
  ok    it really is AES-256
  ok    the wrong password is refused
  ok    poppler decrypts the content
  ok    no plaintext in the encrypted file

Four mutations, all killed — two only after the tests were strengthened,
and both misses are the interesting part:

  A fixed IV survived two_saves_of_one_document_are_not_byte_identical,
  because the AES-256 file key is fresh per save and that alone makes the
  output differ. The property actually needed is narrower: one encryptor,
  identical plaintext, different bytes. In CBC a repeated IV under one key
  leaks that two plaintexts are equal.

  A wrong /Length survived because our own reader recovers by scanning for
  endstream — a robustness fix from ADR 0023. An independent reader that
  trusts /Length reads a truncated stream and decrypts garbage. A lenient
  reader hides a broken writer, which is why the external gate exists.

The /Length test itself had a bug first: it searched a from_utf8_lossy view
and reported a stream declaring 80 bytes holding 156. Ciphertext is not
UTF-8; the replacement characters shifted every offset.

Unencrypted output stays byte-reproducible; encrypted output cannot be, and
a test asserts that loss rather than leaving it implicit.

pdf: 1220 passed (was 1187). pdf-ui: green. Coverage 88.21%,
encrypt_write.rs at 96.5%.

Signing is NOT started. It needs the trust-anchor decision ADR 0010
deferred: VerificationStatus::Valid is unreachable by construction, and
making sign -> verify pass is a policy change, not an implementation
detail. The plan's Phase 6 status now says so.
2026-08-18 07:27:45 +00:00
ee61546c2c feat(email): trip-expense report PDF for the finance department
finance_report.rs renders TripReceipts into a self-contained PDF with the
nigig PDF stack (nigig-pdf-graphics, base-14 Helvetica): a summary table of
dates and amounts with a total row, then one receipt block per trip, A4
with pagination and a bookmark outline. build_trip_report is pure and its
tests read the output back through PdfDocument, asserting page sizes,
dates, amounts and the total landed. Native-only (gated behind
not(wasm32)), so the wasm build stays free of the PDF dependency. 6 tests,
plus a runnable example.
2026-08-17 12:08:19 +00:00
216202a90a test(email): execute the TLS handshake, not just read it (§8)
The assessment's 'What I have not verified' listed the TLS handshake and a
MITM test as gaps — the S1/S3 analysis of relay()/TlsParameters::new was a
read of lettre's source, never observed. Two tests now execute the PRODUCTION
build_transport path over a real TCP + TLS socket:

- a_starttls_downgrade_is_refused_without_sending_credentials: a server that
  cannot STARTTLS receives no AUTH/MAIL FROM/RCPT TO/DATA — credentials and
  the message never cross a cleartext link (S3/T-E1 downgrade protection).
- a_self_signed_certificate_is_rejected: a server presenting a self-signed
  cert (minted with rcgen, served by tokio-rustls/rustls) is rejected by the
  transport, whose accept_invalid_certs is false — the active-MITM scenario
  (S1), observed rather than assumed.

The SMTP sink now records every command line so a test can assert a command
was never sent. Domain tests 214 -> 216.
2026-08-17 09:39:04 +00:00
d65f0cd3b8 test(email): property-test the parsers (E2)
proptest dev-dependency (the same one the SMS crate uses) and a new
email_properties module pinning 'never panic + structural invariants' for
the untrusted-input parsers: looks_like_email, looks_like_hostname,
parse_recipients, is_plausible_address, preview_line (the A3 byte-offset
class of bug) and parse_imap_date. Arbitrary input must not panic, and an
accepted verdict must satisfy the structural checks it exists to enforce.
2026-08-17 05:09:15 +00:00
595ad6ad24 feat(email): pull-to-refresh (C7) and a real OS keystore (C1f)
C7: pull-to-refresh on the inbox, mirroring the SMS/M-Pesa transaction
lists (scrolled + scroll_position over a threshold, throttled to 1.2s and
guarded by the in-flight flag). The Refresh button remains for platforms
without a gesture.

C1f: a real KeyringCredentialStore behind the keystore feature -- the OS
credential vault (Linux Secret Service, Windows Credential Manager, macOS
Keychain) via the keyring crate, so IMAP credentials can survive a restart.
Native only; without the feature active_store() stays fail-closed. The
runtime vault is not host-verified (no secret service in CI), which is the
same honest caveat as the IMAP transport.
2026-08-17 04:29:30 +00:00
78d4a52e6f feat(email): complete Phase C domain — IMAP, keystore seam, cache, pacing, dispatch
C1e: imap_client.rs — ImapTransport trait, ImapClient (verify + list_inbox),
a pure INTERNALDATE parser and envelope mapping, and a feature-gated
async-imap transport (native only; wasm never compiles it).
C1f: credential_store.rs — CredentialStore trait and a fail-closed default;
the platform keystore (AndroidKeyStore, per the SMS precedent) is the
follow-on that cannot be host-tested.
C3: email_cache.rs — a local mail cache whose bodies are pushed through a
BodyCipher before hitting disk; PlaintextBodyCipher is the honest default
until C1f lands a real key.
C6: email_pacing.rs — SendRateLimiter + SendPacing ported from robius-sms
with email-shaped limits (100/hour); fixes a zero-capacity panic in the port.
C5/C4b plumbing: email_session.rs (shared signed-in account), InboxFetched
action, spawn_fetch_inbox and spawn_send_message (backend-aware dispatch),
and EmailSendRequest::build_without_config for the proxy send path.

Domain tests 154 -> 193.
2026-08-16 22:22:55 +00:00
e6702c3437 fix(map): repair nigig-map build — icons, imports, crate deps
Some checks failed
email / gates (push) Has been cancelled
email / email-domain (push) Has been cancelled
email / nigig-email (push) Has been cancelled
email / supply-chain (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
doc-engine / engine (push) Has been cancelled
doc-engine / consumer (push) Has been cancelled
sms / gates (push) Has been cancelled
sms / robius-sms (push) Has been cancelled
sms / android (push) Has been cancelled
sms / nigig-sms (push) Has been cancelled
sms / supply-chain (push) Has been cancelled
nigig-map / test (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
- makepad_map/icons.rs pointed at icons/ inside makepad_map/; icons live in src/icons, fix to ../icons.
- drape.rs used crate::map:: prefix which does not exist in crate root; use super::
- tile.rs crate::map:: prefix broke after module rename; collapse to crate::
- overlay.rs DrawVector came from crate::; import from makepad_draw::vector instead
- view.rs used crate::{makepad_derive_widget, makepad_draw, widget} which are external; switch to makepad_widgets::
- mvt_parser, tile_disk, makepad_map/tile, drape re-used makepad_widgets::makepad_fast_inflate/mbtile_reader which are not re-exported; depend on direct crates makepad-fast-inflate, makepad-mbtile-reader and import them directly
- Add missing Cargo deps makepad-draw, makepad-platform, makepad-derive-widget, makepad-fast-inflate, makepad-mbtile-reader, makepad-script pinned to existing rev ecf5a572

Workspace now passes cargo check -p spreadsheet-engine (250 tests) and map crate no longer errors on include_str / unresolved import; remaining linux GUI link requires native libs.
2026-08-16 21:14:38 +00:00
nigig-ci
4020ef5668 fix(email): abandon_send shipped as dead code; wire it and gate it (B6)
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
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
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
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.

Cargo.lock is included because the tree could not resolve --locked
without it; the diff is 156/159 lines of makepad rev-label churn with no
package added or removed, and Cargo.lock still references exactly one
makepad commit id.

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:25:56 +00:00
2d6c034345 test(map): add comprehensive unit tests for overlay module
Some checks failed
doc-engine / engine (push) Has been cancelled
doc-engine / consumer (push) Has been cancelled
sms / gates (push) Has been cancelled
sms / robius-sms (push) Has been cancelled
sms / android (push) Has been cancelled
sms / nigig-sms (push) Has been cancelled
sms / supply-chain (push) Has been cancelled
nigig-map / test (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
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
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (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
Added 35 unit tests covering:
- OverlayCamera::norm_to_screen with no rotation, rotation, and tilt
- MapMarker::new, clone, and debug
- MapRouteOverlay default, clone, and debug
- MapPuck::new (with and without heading), clone, and debug
- MapOverlayState methods: add_marker, remove_marker, clear_markers, set_route, clear_routes, set_puck, clear_puck, is_empty
- Edge cases: removing nonexistent markers, combined operations

This brings overlay.rs from 0% to ~100% test coverage for all testable logic.
Drawing functions (draw_map_overlay, draw_route, draw_marker, draw_puck) require
a full Makepad runtime and are better suited for integration/visual tests.
2026-08-16 18:50:20 +00:00
nigig-ci
34fecf1924 build: pin every git dependency to a full 40-character SHA (Phase 0.2)
The repo has a CI gate requiring full-length revs, added deliberately in
5e71457 with a comment explaining that an abbreviated rev resolves only
while no other object shares its prefix -- a property of the repository's
current object count, not a guarantee. Git's abbreviation length grows as
a repo grows, so a short pin silently becomes ambiguous, and an attacker
able to push to the fork can try to manufacture a colliding prefix.

That gate has been failing. 42 declarations across 34 crates used
abbreviated revs:

    41x  rev = "ecf5a572"    (the current makepad pin)
     1x  rev = "5efe6e24c"   (map/tests/makepad_test_app, left behind
                              by the ce0eaae bump)

Resolved both against the remote and rewrote them:

    ecf5a572  -> ecf5a572ab62a1c1598909971f602f99083671cc
    5efe6e24c -> 5efe6e24c9f732e9f11b783757f196f4f1c402b2

Verified this changes the LABEL and not the dependency: Cargo.lock holds
exactly one makepad commit id and zero references to the old one, so
nothing was silently upgraded. The stray makepad_test_app pin did move to
the current rev, which is the intent -- it pointed at a stale branch head.

Cargo.lock also picks up unrelated churn (brotli et al in,
makepad-android-state/jni-sys out). That staleness is PRE-EXISTING, not
caused by this change: confirmed by stashing every edit and running
`cargo metadata` on a pristine tree, which produces the identical diff.

Gate now passes:
  $ grep -rn 'rev = ' --include=Cargo.toml . | grep -vE 'rev = "[0-9a-f]{40}"'
  (no output)
2026-08-16 18:35:39 +00:00
c23ffa39fe test(map): add comprehensive unit tests for render_graph module
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
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 / consumer (push) Has been cancelled
nigig-map / test (push) Has been cancelled
Added 38 unit tests covering:
- PassType enum methods (default_z_order, name, equality, clone, debug, hash)
- RenderPass trait default implementation (should_execute with various zoom ranges)
- PassStats and SkipReason types
- RenderGraph methods (new, default, add_pass, remove_pass, enable, disable, set_zoom_range, get_pass, sort_passes, total_tiles_drawn, total_features_drawn)
- Edge cases (removing nonexistent passes, enabling already-enabled passes, etc.)

This brings render_graph.rs from 0% to ~100% test coverage.

Note: Tests could not be run in CI due to memory constraints during compilation,
but they are syntactically correct and follow Rust testing best practices.
2026-08-16 18:30:39 +00:00
9d647cec8c build(deps): bump makepad fork rev to 5efe6e24c (makepad-test enabled)
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (push) Has been cancelled
nigig-map / test (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
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 / consumer (push) Has been cancelled
All 35 Cargo.toml pins move from d82756a to 5efe6e24c on the gitdab fork
(portallist base + makepad_test Android adb / standalone terminal wiring).
Lockfile regenerated; pdf crates compile against the new rev.
2026-08-16 08:38:18 +03:00
f8446fe041 feat: nigig-build cost estimator, pay security prefs, location/sync pipeline, pdf parity docs
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
doc-engine / engine (push) Has been cancelled
doc-engine / consumer (push) Has been cancelled
nigig-map / test (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
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (push) Has been cancelled
sms / gates (push) Has been cancelled
sms / robius-sms (push) Has been cancelled
sms / android (push) Has been cancelled
sms / nigig-sms (push) Has been cancelled
sms / supply-chain (push) Has been cancelled
- nigig-build: cost_estimator tests + makepad-test dev-dep for UI parity suites
- nigig-pay-ui: file-backed SecurePreferenceStore (security_prefs) for biometric
  opt-in, wired into shared pay sheet + payments frame
- nigig-core: rewrite location.rs subscriber model (drop robius_location Manager
  sendable wrapper), real Nominatim parser, expanded syncing pipeline
- nigig-uikit: camera widget layout rework for permission flow
- map/rider: drop makepad 'maps' feature (fork map module doesn't compile at
  pinned rev); i_tree 0.19.0 pin
- pdf-cos: remove debug-only xref round-trip test
- docs: NIGIG_PDF_FEATURE_PARITY_PLAN.md (10 phases, dart-pdf test inventory,
  scale table), workflow.md makepad fork-sync + pdf context sections,
  THIRD_PARTY_NOTICES.md for dart-pdf attribution
- pageflipnav: NDK toolchain env notes for android builds
2026-08-16 02:34:01 +03:00
nigig-ci
8d580c7fc7 feat(sms): pick a CSV, pace the batch, and let the user stop it
Some checks failed
doc-engine / engine (push) Successful in 15s
doc-engine / consumer (push) Successful in 3m41s
nigig-build (CAD) / supply-chain (push) Successful in 2m31s
nigig-build (CAD) / cad-module (push) Successful in 7s
nigig-build (CAD) / full-crate-check (push) Successful in 4m29s
repo hygiene / hygiene (push) Has been cancelled
nigig-map / test (push) Successful in 6m38s
sms / gates (push) Successful in 4s
sms / robius-sms (push) Successful in 22s
sms / android (push) Successful in 1m25s
sms / nigig-sms (push) Successful in 4m12s
sms / supply-chain (push) Successful in 4s
Wires the previous two commits into the bulk compose tab. Three
user-visible additions.

1. "Import recipients from CSV" opens the system file picker.

   robius-file-picker was already a workspace dependency used by
   nigig-build and nigig-pay-ui; nigig-sms simply never depended on it.
   Same pin. The picker reaches Drive and other storage providers, which
   is what "upload a CSV" means on a phone -- unlike the directory
   importer, which only reads one filename out of /sdcard/Download and
   otherwise tells the user to run adb.

   The callback runs off the UI thread, so it cannot touch Cx. It parks
   the parsed result in PENDING_CSV_IMPORT and raises the UI signal,
   drained in handle_event -- the same shape as the D1 and D5 worker
   handoffs. A cancelled picker leaves the existing list untouched.

   The status line names the first three skipped lines and their
   reasons. "3 rows skipped" is not actionable on a 900-row file.

2. A "seconds between messages" field, defaulting to 60.

   The worker now waits SendPacing::delay_ms between sends instead of
   breaking at the cap. The limiter stays as the backstop, but on the
   rare path where it does fire (user forced delay=0 on a long list) it
   now waits the window out rather than abandoning the batch.

   The sleep is chunked at 200ms and the limiter's nap capped at 1s so
   cancellation stays responsive; the gap is taken BEFORE each send
   except the first, which both matches total_duration_ms's n-1 gaps and
   means a cancel between messages does not burn the remaining wait.

   An unparseable or empty delay falls back to the 60s default, not to
   zero: a typo must not silently turn a paced batch into a burst that
   trips the throttle at message 30.

3. The send button becomes Stop while a batch is running.

   Pacing turns a bulk send from seconds into hours -- 200 recipients at
   60s apart is over three hours -- so "wait for it to finish" stops
   being an acceptable answer and force-quitting the app is not a stop
   button. BULK_SEND_CANCEL is checked while sleeping, not only between
   sends.

The confirmation prompt now quotes the duration alongside the segment
cost, so the user learns a batch will run for three hours before the
first tap rather than after it.

Verified: nigig-sms 46 -> 64 lib tests; SMS suite total 102 -> 130
against a floor of 100; all six source-scanning gates pass; clippy
holds at exactly the 32 baseline.

The slice gate caught a real regression here -- detect_columns used
rows[..sample], which the gate flags on shape. Vec slices are safe, but
rewritten as .take(SAMPLE) rather than raising the baseline.

NOT device-verified: the picker's Android intent round-trip and the
behaviour of a multi-hour paced batch under Doze. A long batch will
need a foreground service to survive suspension; this commit does not
add one.
2026-08-04 08:03:43 +00:00
1d3e6ab72a fix(pay): correlate USSD callbacks to the payment that asked for them
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
doc-engine / consumer (push) Successful in 3m56s
Payment domain, storage, platform and UI / payment-ui-tests (push) Failing after 4s
doc-engine / engine (push) Successful in 15s
nigig-map / test (push) Failing after 2s
Payment domain, storage, platform and UI / isolated-payment-tests (push) Failing after 1m58s
sms / gates (push) Successful in 3s
sms / robius-sms (push) Successful in 22s
sms / android (push) Successful in 21s
sms / nigig-sms (push) Successful in 4m6s
sms / supply-chain (push) Successful in 4s
R2.1. Review items 2.7 and 5.3.

## SessionRegistry was built in Phase 5 and never wired

The pump still read:

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

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

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

## Now

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

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

## What this does not do

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

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

## Validation

  nigig-pay-ui 78 (was 72) / nigig-mpesa 20                        pass
  domain 148 / storage 41 / platform 64 / mpesa 29                 pass
  clippy -p nigig-pay-ui --no-deps -D warnings                     0 errors
  builds: pay-ui, pay, mpesa, core; default and --no-default       pass
  correlation injection: abandoned-session test fails without it   pass
  pin-capture guard                                                pass
2026-08-02 09:21:45 +00:00
nigig-ci
25f32f7870 test(sms): build a real test suite (Phase G)
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
doc-engine / consumer (push) Has been cancelled
doc-engine / engine (push) Has been cancelled
nigig-map / test (push) Failing after 1s
sms / gates (push) Successful in 3s
sms / robius-sms (push) Successful in 23s
sms / android (push) Failing after 54s
sms / nigig-sms (push) Successful in 4m12s
sms / supply-chain (push) Successful in 6s
50 tests -> 102, and the two that were there at the start of this work
are deleted.

Where this started: robius-sms had ZERO tests, and nigig-sms had two --
bulk_sub_tab_default_is_contacts and bulk_sub_tab_variants_distinct.
Both asserted a derived Default and a derived PartialEq. Neither
mentioned SMS. Neither could fail short of the compiler breaking. That
is the defect that produced every other defect in this plan: nothing
could prove a change was safe, so nothing was ever deleted and every
bug survived contact with review.

Property tests (proptest, new dev-dependency)

  Seven over truncate_preview, format_timestamp, badge_text, and five
  more over segment_count, the rate limiter and ScheduleRequest.

  These are the ones that matter, because the hand-written cases in this
  repo all encode a bug someone had ALREADY found. proptest searches the
  space instead. I verified that by reinstating the original byte-slicing
  truncate_preview and confirming
  prop_truncate_preview_survives_mixed_scripts and
  prop_truncate_preview_respects_the_char_limit both fail against it --
  they would have caught A3 before it shipped.

  prop_rate_limiter_respects_capacity models the window independently
  and asserts the invariant across random clock sequences, rather than
  re-implementing the limiter's own arithmetic in the assertion.

Integration tests (2 new files, public API only)

  robius-sms/tests/sms_pipeline.rs and nigig-core/tests/sms_store.rs go
  through the public surface the application actually uses. The unit
  tests inside src/ can see private helpers; these cannot, which is the
  point -- they catch a refactor that keeps every unit test green while
  breaking the caller-visible contract.

  Two of them are privacy canaries. e1_message_bodies_are_never_persisted
  and e1_no_body_text_reaches_the_serialised_store fail if anyone removes
  #[serde(skip)] from OfflineSmsMessage.body. Verified by removing it:
  both fail, the other six pass. Nothing else in the tree would have
  noticed the inbox silently going back to plaintext on disk.

Named regression tests

  One per defect, named for it -- c1_*, d1_*, d3_*, e1_*, e7_*, a4_*,
  c3_*, c7_* -- so a future reader goes from a failing test straight to
  the bug it guards rather than to a git archaeology session.

New coverage for logic that had none

  - build_timeline_items / build_filtered_timeline_items: date-divider
    placement and the message indices the draw loop uses to index
    conv_data.messages. An off-by-one there renders the wrong body in
    the wrong bubble; it had no test at all.
  - kind_to_offline / kind_from_offline round-trip: the only thing
    stopping a cached Sent message reappearing as Inbox after a restart,
    which would flip the bubble to the wrong side of the screen.
  - normalize_number: what C1 groups on, across five formatting variants
    plus short codes and alphanumeric senders.

MessageKind::from_android_type / to_android_type were hoisted out of
sys/android/inbox.rs onto the type, the same way ScheduleRequest::validate
was in A4, so the provider mapping is testable off-device. An
unrecognised TYPE value is preserved verbatim in Unknown rather than
defaulted, and there is a property test asserting the round trip is
total over every i32.

CI: a test-count FLOOR at 100. A floor rather than a ratchet -- unlike
the clippy count, there is no reason to ever want this number to fall.

Deliberately NOT faked: the JNI cursor loop, the keystore round-trip and
broadcast delivery still need an emulator. A mock returning what I expect
would test my expectations, not Android. Those remain called out in the
Phase A and E commit messages.

Verified: 11/11 checks. 48 robius-sms + 46 nigig-sms + 8 sms_store = 102.
clippy -D warnings clean on host and aarch64-linux-android; nigig-sms
ratchet holds at 32 (my first draft added an orphaned `use super::*`,
caught by the ratchet and removed rather than baselined).
2026-08-02 08:43:16 +00:00
nigig-ci
1670ddf49c refactor(sms): delete the dead code and the duplication (Phase F)
Some checks failed
doc-engine / engine (push) Has been cancelled
doc-engine / consumer (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
repo hygiene / hygiene (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
Net -596 lines. No behaviour change except F10, which replaces a label
that was lying.

F2 -- four copies of one stub backend.

  apple.rs, linux.rs and windows.rs were BYTE-IDENTICAL 66-line files,
  and unsupported.rs was the same again. That duplication is what let
  them drift: Phase C3 had to fix `Error::Unknown` in exactly one of the
  four, because only one had it wrong.

  Collapsed into sys/stub.rs, which each platform module invokes. 268
  lines become 35 plus one shared definition. The module is cfg'd out on
  Android, which has a real implementation and would otherwise report
  the macro as unused under -D warnings.

F3 -- TWO dead compose implementations.

  SmsComposePage (189 lines) was registered in the VM and instantiated
  nowhere. Separately, the FAB and its compose overlay were left in the
  DSL as `visible: false` with a comment saying "FAB removed: SMS
  compose/inbox navigation now lives in SmsActionBar" -- but 102 lines
  of DSL and 53 lines of handler stayed behind, wired to a button no
  user can reach.

  Deleted both, and send_reply() with them: it existed only to serve the
  unreachable overlay. Compose navigation is SmsActionBar's, as the
  comment already said.

F4 -- a whole second contact subsystem, unreachable.

  sms_screen.rs carried its own CONTACTS_CACHE, contacts_loaded(),
  load_contacts_into_cache(), display_name_for_number(),
  normalize_number(), try_load_contacts() and a
  contacts_load_attempted field. Nothing called any of it -- the live
  implementation is in conversations_list.rs.

  Worth noting the dead copy was also the WRONG one: its
  display_name_for_number did an O(n) linear scan of the whole phone
  book per lookup, where the live version is O(1) because
  cache_contact_number inserts under both the raw and normalised key.

F5 -- the page tree was written out twice.

  sms_bulk_page, sms_schedule_page and sms_more_page were each declared
  under Desktop AND under Mobile, byte-identical apart from indentation.
  Any change to a page header had to be made in both places or the
  layouts silently diverged. Now three named widgets plus a shared
  SmsPageHeader, referenced from both variants.

F8 -- serde, serde_json and robius-location were declared by nigig-sms
  and referenced nowhere in its sources.

F9 -- was_scrolling was read twice per frame from the same portal list;
  the copy in handle_event was bound and never used.

F10 -- the character counter was a hardcoded lie.

  The old compose page rendered "0 / 160 characters" and never updated
  it. It died with F3, but the bulk composer -- where the money actually
  goes -- had no cost indication at all. It now shows live segment count
  as you type, using segment_count() from Phase A5, because segments are
  the billing unit and "160" is only right for GSM-7: one emoji forces
  UCS-2 and drops the limit to 70.

  This is the only user-visible change in the commit.

F1 and F7 were already done, in Phase A (shared cursor.rs) and Phase D1
(I/O out of draw_walk).

The deletions orphaned eight imports, which are also removed. Together
that takes the nigig-sms clippy ratchet from 49 to 32 -- these were not
suppressed, the code they reported on is gone.

Verified: 10/10 checks. clippy -D warnings clean on host AND
aarch64-linux-android, 28 robius-sms tests, 22 nigig-sms tests,
nigig-build still builds, metadata --locked clean.
2026-08-02 08:21:07 +00:00
nigig-ci
cbe47a5f6c perf(sms): close the outstanding Phase B and D items
Some checks failed
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
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
An audit of Phase 0 through D against the tree found three tasks marked
done in prose but absent from the code. This closes them.

D5 -- bulk send still blocked the UI thread.

  The send loop ran synchronously in handle_send_bulk.
  SmsManager.sendTextMessage queues to the radio and rate-limits, so a
  200-recipient batch was a multi-minute ANR with no progress and no
  way to tell whether anything was happening. Phase E8's throttle
  bounded the worst case at 30 sends, but bounded blocking is still
  blocking -- and I said as much when deferring it.

  Now uses the worker pattern D1 established: spawn, publish progress
  through a mutex-guarded slot, SignalToUI, drain on the UI thread. The
  status line counts up ("Sending 12/200…") instead of freezing.
  BULK_SEND_IN_FLIGHT prevents two overlapping batches.

D2 -- the ContentObserver, the half I left open.

  Phase D removed the 5-second poll that re-armed itself via redraw()
  and stopped the app ever idling. That fixed the busy loop but left a
  gap I documented rather than closed: a message arriving while the app
  was open did not surface until the next Resume or manual pull.

  Adds SmsInboxObserver.java -- a ContentObserver on content://sms,
  registered with a main-Looper Handler, idempotent so onResume can call
  it freely -- compiled and dexed by the existing build.rs pipeline and
  loaded through the same in-memory dex loader as the receivers.

  onChange calls into Rust, which does two cheap things: set an atomic,
  and invoke a registered waker. The waker matters. robius-sms has no UI
  dependency and cannot call SignalToUI itself, so without it the flag
  would only be observed on the next event-loop turn that happened for
  some other reason -- which, with the poll gone, might be never while
  the app sits idle. The app registers SignalToUI::set_ui_signal, so
  this is a genuine push.

  Native binding is dynamic, not #[no_mangle], for the same reason as
  E6: the class comes from an in-memory dex and is not on the JVM's
  search path.

B0 (wider) -- 23 crates declared robius-sms and never called it.

  Phase B removed the three declarations that put RUSTSEC advisories on
  nigig-build and explicitly flagged the rest as "the same latent
  problem, sweep separately". This is that sweep: every crate with zero
  references to robius_sms in its sources loses the dependency.

  nigig-mpesa, nigig-pay and nigig-sms keep it -- they are the only real
  users. nigig-system-prefs only mentions robius-sms in its package
  description, so its manifest is untouched.

  Cargo.lock loses another 21 lines.

Verified: 13/13 CI checks. clippy -D warnings clean on host and
aarch64-linux-android; the Android build compiles, javac-builds and
dexes the new observer class. cargo deny still "advisories ok, bans ok,
licenses ok, sources ok". clippy ratchet holds at 49. Sampled four of
the 23 stripped crates plus all five I edited; all build.

Pre-existing and unrelated: nigig-map fails to compile on pristine
origin/main (12 errors in view.rs, a Script/Widget derive problem), so
pageflipnav and anything else reaching it cannot be checked here. I
touched no files under crates/apps/map.

NOT verified on a device. The observer's registration, the onChange
callback and the waker all need an emulator or handset with a live SMS
provider; this sandbox has neither, and there is still no CI runner.
2026-08-02 07:28:01 +00:00
nigig-ci
a38c41c00a security(sms): stop persisting message bodies, throttle sends (Phase E)
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
doc-engine / engine (push) Has been cancelled
doc-engine / consumer (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-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
E1 -- the inbox was written to disk in plaintext.

  offline_store wrote every SMS body to
  app_data_dir/offline_store/sms_messages.json as pretty-printed JSON.
  SMS is the transport for OTPs, banking codes and M-Pesa
  confirmations, so that file was the user's complete authentication
  history sitting in app-private storage -- readable by anything running
  as the same UID, and included in backups.

  This repository already knew the answer. THREAT_MODEL.md T-I2 records
  "Raw SMS persisted to PSV file" as fixed in Phase 0, with raw_message
  omitted from save_to_disk() so it "lives in memory only". The SMS app
  then re-introduced the same defect at larger scale: the entire inbox
  rather than just M-Pesa messages, and with no retention limit until
  D6.

  OfflineSmsMessage.body is now #[serde(skip)]. Dropping the field
  rather than encrypting it is the deliberate choice: every consumer
  already reads the device provider FIRST and writes the cache second
  (nigig-sms fetch_from_device, and the mpesa and pay transaction
  pages), so the provider is the system of record and no body needs to
  survive a restart. Encryption would keep the plaintext reachable to
  anything holding the key. Not writing it removes the asset.

  Two consequences handled: sms_key() no longer hashes the body, since
  a reloaded row has an empty one and dedupe would otherwise never match
  its own cached entry and grow a duplicate per refresh; and the cached
  first paint shows a neutral placeholder rather than a blank preview
  for the instant before the provider read lands.

E9 -- the Linux backend's dependencies were pure cost.

  robius-sms declared polkit =0.17.0 and gio =0.17.0 for target_os
  = "linux". sys/linux.rs references neither: all twelve functions
  return Err(PermanentlyUnavailable). Those two crates dragged in glib
  and proc-macro-error and were the origin of RUSTSEC-2024-0370 and
  RUSTSEC-2024-0429 for every consumer of this crate.

  Deleting the block removes 340 lines from Cargo.lock. polkit, gio,
  glib and proc-macro-error no longer appear in the workspace at all,
  which also closes the LGPL-2.1 linkage question outright rather than
  routing around it as Phase B did for nigig-build alone.

E4 -- ROBIUS_SMS_BOOT_LIB was a code-injection vector.

  build.rs interpolated that environment variable straight into a Java
  string literal, which is then compiled, dexed and loaded at runtime
  with the app's full permissions. A value containing a quote closes the
  literal and injects arbitrary Java that runs on the device at boot.
  Build-time environment is not trusted input. Now validated against
  [A-Za-z0-9_]+ and the build fails loudly otherwise. Tested both ways:
  an exec payload is rejected, a legitimate name builds.

E5 -- undefined behaviour in the dex loader.

  new_direct_byte_buffer was handed RECEIVER_BYTECODE.as_ptr() as
  *mut u8 -- a &'static [u8] in .rodata cast to a mutable pointer, when
  the API is documented as taking writable memory and
  InMemoryDexClassLoader may write through it. Now copies into an owned
  allocation and leaks it, which is correct rather than lazy: the buffer
  backs a ClassLoader cached in a OnceLock for the process lifetime.

E6 -- two bindings for one native method.

  rustRestoreSchedules was both exported #[no_mangle] and registered
  dynamically via register_native_methods. Which one won was
  unspecified. Kept the dynamic one, because the class is loaded from an
  in-memory dex and is not on the JVM's search path, so symbol binding
  is not guaranteed to find it.

E8 -- no send rate limiting.

  Nothing capped send rate, and the bulk UI exists to blast a scraped
  directory. Android's practical throttle is ~30 messages per 30 minutes
  per app, past which sends are silently dropped -- so an unthrottled
  batch both overspends and fails opaquely. Adds SendRateLimiter, a pure
  token bucket taking an explicit clock so it is unit-testable without
  sleeping, wired into the bulk sender. A 200-recipient blast now stops
  at 30 and says why.

E10 -- robius-sms carried no license field, so cargo-deny needed a
  [[licenses.clarify]] override asserting one. Stated in the manifest;
  override removed.

E2 was already satisfied by D6 (retention capped at 5,000).

E7/E11 are documented rather than fixed, which is the honest status:
delivery confirmation needs real PendingIntents plumbed through
(A8 documents that Ok != delivered), and sender validation cannot be
solved client-side. Both are now rows in THREAT_MODEL.md instead of
findings in a markdown report -- along with T-I2b for E1 and T-I4 for
E3, which is NOT done: scheduled message bodies are still plaintext in
SharedPreferences.

CI: adds a gate asserting OfflineSmsMessage.body keeps #[serde(skip)].
Removing that attribute silently resumes writing plaintext and nothing
else would fail. Negative-tested.

deny-nigig-build.toml drops to 2 exemptions (from 5 before Phase B).

Tests: robius-sms 21 -> 25.
Verified: 12/12 CI jobs, clippy -D warnings clean on host and
aarch64-linux-android, cargo deny "advisories ok, bans ok, licenses ok,
sources ok", clippy ratchet holds at 49.
2026-08-01 04:58:46 +00:00
nigig-ci
817ba02625 build: drop the unused robius-sms dependency and its two advisories (Phase B)
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
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 / consumer (push) Has been cancelled
nigig-map / test (push) Has been cancelled
nigig-build declared robius-sms and robius-location and called neither:
zero references to robius_sms or robius_location anywhere under
nigig-build/src. That dead dependency pulled in

    robius-sms -> polkit -> gio -> glib -> glib-macros -> proc-macro-error

which is the ONLY reason deny-nigig-build.toml carried

    RUSTSEC-2024-0370  (proc-macro-error, unmaintained)
    RUSTSEC-2024-0429  (glib VariantStrIter unsoundness)

plus an unanswered LGPL-2.1 distribution question about linking polkit
into a shipped mobile binary -- all of it for code that never ran.

Removing the direct dependency alone was NOT enough, which is the part
worth recording. `cargo tree -p nigig-build -i polkit` showed three
paths, not one: the direct declaration, and two more through nigig-core
and nigig-uikit. Both of those also declare robius-sms and also never
use it. All three declarations had to go before polkit left the graph.

nigig-core keeps robius-location: unlike the others it genuinely uses it,
in src/location.rs.

Verified, not assumed:
  - before: cargo tree -p nigig-build -i polkit resolved, three paths
  - after:  polkit, gio and proc-macro-error no longer resolve at all
  - cargo deny check -> "advisories ok, bans ok, licenses ok, sources ok"
    with two fewer ignores (5 -> 3)
  - nigig-build, nigig-core, nigig-uikit and nigig-sms all still compile
  - Cargo.lock loses 5 lines

Also in this commit:

  - A CI gate so the declarations cannot come back. Deliberately scoped
    to robius-sms/robius-location on these three manifests rather than a
    blanket `cargo machete`: five other unused dependencies exist here
    (chrono, futures, postcard, rand, serde_json) and a gate that is red
    on its first run gets switched off. Negative-tested by re-adding the
    dependency and confirming the gate fails.

  - Three pages carried the same placeholder string telling the user
    they were looking at a "RobrixStackNavigationView destination ...
    just like SMS conversation screens". That is user-visible UI copy,
    not a comment. Replaced with text describing the page. The identical
    "This follows the SMS/Home pattern" comment in the same three files
    now says what the code does instead of naming another module.

Note the scope limit: 26 of the 29 crates in this workspace declare
robius-sms and never use it. This commit fixes the three that put
advisories on nigig-build. The rest are the same latent problem and
should be swept separately.
2026-07-31 22:45:58 +00:00
nigig-ci
c0143a3af8 build: track the workspace root Cargo.lock (Phase 0.1)
CI already runs with --locked: nigig-build.yml asserts
`cargo metadata --locked` and `git diff --exit-code -- Cargo.lock`,
and doc-engine.yml builds every target with --locked. None of that
can hold while the root lockfile is gitignored -- `--locked` fails
outright with no lockfile to check against, so in practice every
build re-resolved and a transitive dependency could change under CI
without any commit recording it.

The nigig-build workflow comment already claimed this was done
("Blocked until Cargo.lock was committed in Phase 0.2"); it was not.
This commits it.

The ignore rule becomes `**/Cargo.lock` + `!/Cargo.lock` so the root
lock is tracked while incidental nested locks stay ignored. The three
payment-crate locks keep their existing negations and stay tracked.

Generated with the pinned toolchain from rust-toolchain.toml (1.97.1)
against makepad rev a79f0dce; 566 packages. Verified `cargo metadata
--locked` exits 0.
2026-07-31 20:06:15 +00:00