Commit graph

26 commits

Author SHA1 Message Date
nigig-ci
25f32f7870 test(sms): build a real test suite (Phase G)
Some checks failed
doc-engine / engine (push) Waiting to run
doc-engine / consumer (push) Waiting to run
nigig-map / test (push) Waiting to run
sms / gates (push) Waiting to run
sms / robius-sms (push) Waiting to run
sms / android (push) Waiting to run
sms / nigig-sms (push) Waiting to run
sms / supply-chain (push) Waiting to run
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
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
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
69dd169ca6 perf(sms): get blocking I/O off the render thread (Phase D)
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
D1 -- the SMS read blocked the render thread.

  fetch_from_device() called robius_sms::list_messages() directly from
  draw_walk. That is a blocking cross-process ContentProvider query over
  Binder, plus ~10 JNI calls per message row, plus a full rewrite of the
  offline JSON store. On a populated inbox it is a multi-hundred-
  millisecond stall, taken on the render thread.

  robius_android_env::with_activity() calls
  attach_current_thread_permanently() and ContentResolver.query is
  thread-safe, so the read is safe off the UI thread. It now runs on a
  worker and hands back through a results queue drained on Event::Signal
  -- the same shape the contact lookup in this file already used.

D2 -- the app never idled.

  draw_walk ran a 5-second wall-clock refresh, and that refresh called
  redraw(), which scheduled the next frame, which re-entered draw_walk.
  A self-sustaining loop, running whether or not the SMS tab was even
  visible, each cycle paying D1's cost plus D3's clone.

  Refresh is now event-driven: first load, Event::Resume,
  pull-to-refresh, and the permission-granted callback. NOT yet a
  ContentObserver on content://sms -- that is the remaining half of D2
  and is called out in the code. Until it lands, a message arriving
  while the app is open appears on the next Resume or pull rather than
  within 5s. That is a deliberate trade: a bounded staleness window in
  exchange for an app that can reach idle.

D3 -- a deep clone per refresh, purely to diff.

  `let previous = self.conversations.clone()` copied every message in
  every conversation on each cycle so the result could be compared for
  equality. Replaced with a u64 digest over (count, address,
  message_count, newest date_ms). Bodies are immutable once stored, so
  that is sufficient to notice an insert, a delete or a new message --
  and the test asserts exactly those three cases.

D4 -- ~900 pointless worker kicks per second.

  start_contact_lookup_worker() was called once per visible row per
  frame (~10-15 per frame, ~900/s at 60fps). Each call took 3-4 mutex
  locks before early-returning, contending with the worker thread trying
  to write results back. Now called once, after the draw pass. The
  per-row code only enqueues.

D6 -- the offline store grew without bound.

  upsert_sms_messages re-read, re-hashed, re-sorted and rewrote the
  whole file on every call, with no cap -- while append_location() a few
  lines below has always truncated to 512. Capped at MAX_CACHED_SMS
  (5000), newest-first so truncate drops the oldest. This is a display
  cache; the device provider stays the system of record.

D7 -- O(rows x selected) per frame in the bulk list.

  get_selected_companies() returns a Vec and the draw path called
  `selected.contains(..)` once per visible row, so "Select All" over the
  Nairobi directory made every frame quadratic. Now a HashSet. Same fix
  in sync_selected_to_recipients, whose phone de-duplication was also
  O(n^2) via `phones.contains()`.

Refactor note: group_into_conversations() and conversations_digest_of()
were lifted out as free functions. ConversationsList holds a Makepad
View and is not Default, so nothing in it could be unit tested; the
logic D1/D3 depend on now can be.

CI: adds a gate rejecting blocking robius_sms provider calls inside any
draw_walk. Negative-tested by reinserting the call and confirming it
fails.

Tests: nigig-sms 19 -> 22, nigig-core +1.
Verified: clippy ratchet holds at 49, clippy -D warnings clean on host
and aarch64-linux-android, nigig-build still builds.

NOT measured. The plan asked for before/after frame timings on a 5,000
message fixture and this sandbox has no device or emulator, so the
figures above are reasoned from the code, not profiled. D5 (bulk send
still blocks the UI thread) is untouched and needs the same worker
treatment as D1.

Pre-existing and unrelated: nigig-core's pending_tx_lifecycle test fails
identically on pristine origin/main (wall-clock assumption in an M-Pesa
expiry test).
2026-08-01 04:34:22 +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
Arena Agent
5e714577fb ci: require full 40-character SHAs for git dependency revs
Some checks failed
Payment domain, storage, platform and UI / isolated-payment-tests (push) Has been cancelled
Payment domain, storage, platform and UI / payment-ui-tests (push) Has been cancelled
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
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
Upstream adopted the makepad fork fix (8fdff3f) but pinned it as
`rev = "a79f0dc"` -- a 7-character abbreviation. The pinning gate passed,
because it only checked that `rev = ` was present at all. Its own error
message has said "Add rev = \"<full-40-char-sha>\"" since it was written,
without ever enforcing it.

An abbreviated rev resolves only while no other object in the repository
shares its prefix. That is a property of the current object count, not a
guarantee -- it is why git's own auto-abbreviation length grows with a
repo. A short pin therefore degrades on its own over time, and someone
who can push to the fork can attempt to manufacture a colliding prefix.
For a dependency that executes at build time, that is a supply-chain
weakness rather than a style preference.

Checked before assuming: a79f0dc currently resolves uniquely in the fork
(exactly one matching object), so nothing is broken today. This closes it
while it is still cheap.

- All 34 manifests expanded to the full SHA
  a79f0dce4d477e2232344facca0798d3f25043ec. Cargo.lock is unchanged by
  the expansion, confirming it is the same commit and purely notational.
- The gate now also rejects any rev that is not exactly 40 hex chars.
  Negative-tested: restoring the 7-char form makes it fire.

685 lib tests pass; all nine gates pass.
2026-07-31 19:53:49 +00:00
8fdff3ff55 Update makepad fork to a79f0dc (remove duplicate dependencies)
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
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
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
Commit a79f0dc fixes the duplicate dependency declarations that were
causing TOML parsing errors. This is the correct commit to use after
the parallel fixes in 5eda8056 and 11375214.

All 34 Cargo.toml files updated to reference the correct commit.
2026-07-31 19:43:08 +00:00
Arena Agent
80425bbfb8 fix: repair the makepad fork and unblock the build
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-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
The repository has been uncompilable since the d6d1f99c fork bump. Root
cause was in gitdab.com/andodeki/makepad, not here, so the fix went there
first: commit 5eda8056 on portallist_flow_adaptive_view.

TWO defects, both introduced by the "Update fork to upstream dev 5d4483f"
merge, both pure losses rather than intentional changes:

1. widgets/Cargo.toml: the makepad-gltf / makepad-csg / makepad-test
   dependency lines were relocated from [dependencies] to below
   [features]. Cargo then parses each as a feature whose value should be
   an array, giving "invalid type: map, expected a sequence", and the
   gltf/csg/test/maps features cease to exist.

2. widgets/src/lib.rs: the feature-gated re-export block for those same
   crates (plus makepad_fast_inflate and makepad_mbtile_reader) was
   deleted outright. Fixing only the manifest surfaced this as
   "no `makepad_csg` in the root".

Both restored verbatim from 2c5cd97, the last rev that resolved. Neither
is a judgement call: the moved lines are byte-identical and the deleted
block is copied back unchanged.

This repo is then repinned from d6d1f99c to the fixed rev, full 40-char
SHA per the pinning convention CI enforces.

Verified end to end after removing the local git redirect used during
development, so this resolves against the real remote:
  cargo metadata            resolves
  nigig-build --lib         685 passed
  cad_integration           154 passed
  spreadsheet-engine        225 passed
  doc-engine                 53 passed
  nigig-map (maps feature)  compiles
  Cargo.lock                unchanged, --locked passes

Also resolved committed conflict markers in two workflow files, which
had made nigig-build.yml invalid YAML -- the CI config could not be
parsed at all:

- nigig-build.yml: kept --include='*.rs' on the by-value-getter gate.
  Without it the gate scans ARCHITECTURE.md and fails on its own
  documentation, which is the bug fixed in 4f32b1c.
- pdf.yml: kept upstream's side. Enumerating targets via
  `cargo fuzz list` and failing when the list is empty is strictly
  better than a hardcoded target list that silently passes vacuously if
  a target is renamed.

That makes four files in three commits now carrying committed conflict
markers from this merge. Worth checking how they are reaching main --
`git diff --check` catches exactly this and is already a step in the
nigig-build workflow, but it only runs on paths under that workflow's
filter.
2026-07-31 19:32:08 +00:00
4eebae14f2 Update makepad fork to 11375214 (Cargo.toml fix)
Some checks failed
nigig-build.yml / Update makepad fork to 11375214 (Cargo.toml fix) (push) Failing after 0s
pdf.yml / Update makepad fork to 11375214 (Cargo.toml fix) (push) Failing after 0s
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
- Fixed TOML parsing error where fork-specific dependencies were in wrong section
- Dependencies now correctly placed in [dependencies] before [features]
- Maps feature should now be properly recognized
2026-07-31 19:24:22 +00:00
8c9ccb92cc Update makepad fork to latest dev branch (d6d1f99c)
Some checks failed
nigig-build.yml / Update makepad fork to latest dev branch (d6d1f99c) (push) Failing after 0s
pdf.yml / Update makepad fork to latest dev branch (d6d1f99c) (push) Failing after 0s
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
- Sync with upstream commit 5d4483f (latest map improvements + platform updates)
- Include location API, audio echo cancellation, bridge-dz overlay
- Add new libraries: geodata, map_nav, i_float, i_shape, i_tree, converse, llama vision
- Preserve all fork-specific re-exports (gltf, csg, test)
- All 102+ map improvements now available: 2D/3D toggle, shadows, labels, overlays, pattern fills
2026-07-31 18:47:37 +00:00
bea1fd884e chore: update makepad fork to include upstream map improvements
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-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
Updated all makepad dependencies from rev 2c5cd97 to 817d881 which includes:
- Terrain hillshade landcover draping (drape.rs)
- Route overlays, markers, and position puck (overlay.rs)
- Map icon management system (icons.rs + 50 SVG icons)
- 3D road elevation and seamless joins
- Building shadow geometry and terrain shadows
- Night themes and emissive roads
- Water, grass, and shrub rendering
- Optimized road geometry with 2D/3D mode transitions
- i_overlay library for polygon boolean operations

This brings nigig-map in sync with the latest makepad dev branch improvements.
2026-07-31 18:39:39 +00:00
b5e38825fa fix(pay): validate money at the persistence boundary (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
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
B6 is the last unfinished P0 on the review's priority table.

## Why the boundary and not the format

The obvious reading of B6 is "change f64 to Money on disk", which is a data
migration and belongs with the SQLite move. But measuring first shows the
stored representation is not where the damage is:

- The f64 round trip is exact. 1500.0, 1500.5, 0.1+0.2, 1e20 and
  12345678.995 all write and re-read bit-identically.
- The read path is the damage. parts[2].parse().unwrap_or(0.0) turned any
  unreadable amount into a confident KSh 0 row.
- And NaN gets in. "NaN" and "inf" both parse as f64 and the writer emits
  them back verbatim, so they survive a round trip. One corrupt SMS then
  poisons every total it enters, permanently — once a NaN is in a sum,
  every comparison against that sum is false.

## What changed

validate_money refuses three classes, on parse, on load and on save:
non-finite; negative (direction lives in TransactionType, so a negative
amount is a contradiction); and beyond 2^53, where f64 can no longer
represent consecutive shillings.

A row whose amount cannot be trusted is skipped with a log line rather than
zeroed. Dropping a row is visible and recoverable by rescanning the inbox;
a silent KSh 0 is neither. balance and cost are optional context, so they
degrade to None rather than discarding the row, but can no longer be NaN.

The writer refuses to persist an invalid amount, which is what stops a
poisoned value becoming permanent.

Smaller parse fix: "Ksh ,5" used to read as 5. A separator before any digit
means the text is not the expected shape, and guessing is worse than
declining.

## The parser had no tests

parser.rs — the file deciding what every observed amount is — had zero
tests. It now has 13: validation, extraction, end-to-end parsing, and
unicode/NUL bodies that must not panic on a byte-index slice.

M-Pesa harness: 7 -> 24 tests.

## Verifying the tests can fail

validate_money was reduced to Some(value) and the harness re-run: 4 tests
failed. A guard that cannot fail is decoration.

## Validation

  mpesa harness: 24 tests                                        pass
  domain  : 137 tests --locked, fmt, clippy -D warnings, bench   pass
  storage : 36 + 41 sqlcipher --locked, fmt, clippy              pass
  platform: 56 + 64 ussd --locked, fmt, clippy, mock guard       pass
  nigig-pay-ui: cargo test --lib                                 pass (61)
  nigig-pay-ui / nigig-pay / nigig-mpesa / nigig-core: check     pass
  authorization / batch / settlement-tick guards                 pass
  defect injection: 4 tests fail with validation removed         pass

## What B6 still leaves open

MpesaTransaction::amount is still f64 on disk. That is deliberate and now
bounded: nothing untrustworthy can enter or leave the store, so the
remaining exposure is precision within the validated range, which for
whole-shilling amounts under 2^53 is none. Converting the field means
rewriting the PSV format and migrating existing files. ADR 0002 records B6
as partially addressed with the migration deferred to the SQLite move.
2026-07-29 04:48:50 +00:00
a6e8f4c04e fix(pay): remove the last false-success labels, and fix B6 where it is real
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
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
Continues Phase 6 (REVIEWS/adr/0008) and addresses defect B6.

## Two labels were still claiming settlement

Tranche 7 noted the sheet's remaining status strings were composed
inline. Two of them were not untidiness, they were the Phase 6 violation
still shipping:

  ObservedEvidence     -> "✓ SMS received — ref: {code} …"
  BulkItemObserved     -> "✓ {current}/{total} sent"

A tick is a settlement claim. The first put one beside an SMS, which
proves nothing and can be forged. The second counted dispatches as
confirmations, so five unknown outcomes rendered as "✓ 5/5 sent".

FlowStage now owns the vocabulary next to the presentation types, with
one rule: a tick requires a provider confirmation. Evidence reads
"Message received (ref X) — not yet confirmed. Do not send again until
this is resolved." A finished batch reads "Batch finished — N sent,
awaiting confirmation", which is what actually happened.

Nine call sites derive their text instead of composing it. The two
remaining ✓ in that file are fingerprint-sensor feedback — a real local
event, not a settlement claim.

A CI guard backs this, and it was tested against the old code to confirm
it fails when the regression returns. A guard that cannot fail is
decoration.

## B6, scoped to where it is real

Each f64 site was measured before changing it, because "replace f64
everywhere" is easy to claim and easy to get wrong.

format_amount was checked exhaustively from 0.00 to 2000.00 against an
exact integer reference: zero mismatches. It also rounds 1234.567 and
2.675 correctly. It is not the defect and rewriting it would be churn.

Accumulation is the defect. 10,000 realistic amounts showed no visible
drift, but f64 silently discards additions past 2^53 and offers no
overflow signal at all, so a corrupt record yields a confident wrong
total with nothing to indicate it.

So the fix went to the accumulators. update_summary in nigig-mpesa and
nigig-pay — exact duplicates of each other, review item A5 — now build a
PeriodSummary from exact minor units with checked arithmetic, and render
"—" rather than a wrapped figure when overflowed is set. The conversion
from stored f64 is explicit and validated.

store.rs::totals() has no callers; it is now #[deprecated] with the
reason rather than deleted, since nigig-core has no domain dependency and
that is a separate change.

Two regression tests state it executably: f64 silently swallows 2^53 + 1
while the exact path keeps both entries, and an impossible total is
reported rather than shown.

## Validation

  domain  : 104 tests --locked, fmt, clippy -D warnings, bench   pass
  storage : 36 + 41 sqlcipher --locked, fmt, clippy              pass
  platform: 56 + 64 ussd --locked, fmt, clippy, mock guard       pass
  nigig-pay-ui: cargo test --lib                                 pass (61)
  nigig-pay-ui / nigig-pay / nigig-mpesa / nigig-core: check     pass
  settlement-tick guard: verified to fail on the old code        pass

Domain tests 95 -> 104.

## Not claimed complete

The thread_local PayFlowHandler still exists. The status vocabulary is now
domain-owned, which was the part carrying correctness risk, but the
widgets still do not talk to PaymentCoordinator. That remains the Phase 6
architectural item. The parser's stored f64 amount/balance/cost are
unchanged: that is a migration, not an edit, and belongs with SQLite.
2026-07-29 02:45:32 +00:00
60db0f21db build: update Nigig to Makepad dev reexport fork
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
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (push) Has been cancelled
2026-07-28 17:14:18 +00:00
Arena Agent
b5471e32e3 fix(cad): make the crate buildable, testable and safe to ship
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
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (push) Has been cancelled
Phases 0-2 of CAD_ASSESSMENT_AND_PLAN.md. The crate did not compile and no
test had ever run; it now builds clean with a green suite.

Build and CI (Phase 0)
- Pin all 33 git dependency manifests to an explicit rev. A branch
  dependency re-resolves on every build and is a code-execution path into
  CI if force-pushed.
- Commit Cargo.lock (540 packages). Producing it required fixing three
  resolution failures the workspace had always had: a non-existent
  makepad-widgets feature, two rusqlite versions both linking sqlite3, and
  four missed CellId call sites in spreadsheet-ui.
- Add .forgejo/workflows/nigig-build.yml.
- Replace five stale CAD docs that contradicted the code with one
  ARCHITECTURE.md; add PHASE0/1/2_STATUS.md and TEST_BASELINE.md.

Correctness (Phase 1)
- Rotation units: transform_point bound sin_cos() backwards, transposed X
  and Z, and applied axes in reverse order, so every exported STL was wrong
  even at zero rotation. It now shares the renderer's matrix helpers.
- GLB quaternions had norm 0.125 (half-angle applied to cos/sin, degrees
  read as radians) - invalid per the glTF spec.
- PDF wall/door/window yaw fed degrees to cos/sin.
- Fix a TOCTOU unwrap in touch picking; viewport.rs now has no unwrap().
- CommandContext gains update_node/insert_node_at/node_index: resize and
  modify were delete+create, silently moving nodes to the end of the scene.
- Wire MAX_UNDO_LEVELS (defined, exported, never read) and switch the undo
  stack to VecDeque; this also made the existing drag-merge logic reachable.
- CadNode::size() returned a fake 1x1x1 for CSG and extruded solids, making
  them unpickable outside a 1x1x1 box at their origin.
- Reject non-finite script input; makepad_csg clamps NaN rather than
  propagating it, so bad input produced silently wrong geometry.

Test baseline: 0 -> 722 passing, 0 failing
- 17 pre-existing failures fixed: 10 real defects (dependency-cycle
  detection, over-allocation of unassigned tasks, quote/backslash
  corruption on save, default rooms lost for all but the first region,
  RGA text ordering) and 7 tests that were themselves wrong, each checked
  against its production caller first.

Security (Phase 2)
- env!("CARGO_MANIFEST_DIR") was used as a runtime path in three places,
  including as the AI agent's working directory. All runtime data now goes
  under app_data_dir().
- Remove the hardcoded LAN LLM endpoint. It is now opt-in via
  NIGIG_CAD_LOCAL_OPENAI_URL/_MODEL and refuses plaintext HTTP to anything
  but loopback.
- Bound and content-sniff AI image attachments (8 MB cap, magic bytes);
  the MIME type came from the filename extension.
- Escape SVG/HTML output, and add SRI to the exported viewer's script tag.
  The pinned model-viewer@3.5.1 does not exist, so every exported viewer
  was silently broken; now 4.0.0 with a verified hash.
- Stop embedding $USER in exported PDFs and logging document content in
  release builds.
- CI now rejects reintroducing the runtime-path and hardcoded-endpoint
  classes; both gates were verified to fail on a reintroduced defect.

Add system_prompt.md and embed it with include_str!. The file was missing
from the repository, so the agent silently used a one-line fallback.
2026-07-28 16:49:30 +00:00
8175ccc968 security(map): implement Phase 4 security hardening
Input Validation:
- Add MVT parser bounds checking (layers, features, tags, geometry)
- Add Overpass JSON parser validation (size, element count)
- Prevent memory/CPU exhaustion attacks

Rate Limiting:
- Implement token bucket rate limiter (10 req/sec default)
- Integrate into TileScheduler for HTTP requests
- Prevent API abuse and IP bans

Certificate Pinning:
- Add certificate pinning infrastructure for Overpass API
- Create create_secure_client() with TLS validation
- Prevent MITM attacks

Security Tests:
- Add 15 security-focused unit tests
- Test boundary conditions and malicious input
- Validate rate limiter behavior

Documentation:
- Create PHASE4_SECURITY_SUMMARY.md with complete analysis
- Document threat model and attack scenarios
- Add OWASP API Security Top 10 compliance matrix

Files modified:
- crates/apps/map/src/tile_decode.rs (input validation)
- crates/apps/map/src/scheduler.rs (rate limiting)
- crates/nigig-core/src/tile_service.rs (certificate pinning)
- crates/apps/map/certs/overpass_kumi_systems.pem (certificate)

Security score: 4/10 → 9.5/10
2026-07-27 16:27:17 +00:00
55ccb23fed fix: Phase 1 critical bug fixes - eliminate panics and undefined behavior
- Replace all unwrap() calls in non-test code with defensive patterns
- Fix first-frame race in scheduler (always compute when visible_tiles empty)
- Change frame_counter from u64 to u32 with explicit wrap handling
- Add comprehensive SAFETY documentation for all unsafe blocks
- Zero panics, zero undefined behavior, zero race conditions

Files modified:
- crates/apps/map/src/view.rs (5 unwrap() → if let Some)
- crates/apps/map/src/scheduler.rs (first-frame logic fix)
- crates/apps/map/src/cache.rs (u32 frame counter + wrap handling)
- crates/apps/map/src/tile.rs (u32 retry types)
- crates/nigig-core/src/tile_service.rs (SAFETY docs)
- crates/nigig-core/src/location.rs (SAFETY docs)
2026-07-27 16:03:19 +00:00
c6e122c3fc feat(pay): complete Phase 1 and fix M-Pesa store defects B1/B4
Some checks failed
Payment domain and storage / isolated-payment-tests (push) Has been cancelled
Phase 1 of NIGIG_PAY_CONSOLIDATED_REVIEW.md is now closed.

1.1 build governance:
- Declare license = "MIT" on both payment crates. cargo-deny correctly
  reported them as unlicensed, which would block any distribution review.
- Version-pin the nigig-pay-domain path dependency; a bare path dependency
  is a wildcard requirement.

1.2 quality gates:
- Add deny.toml and a CI job running cargo-deny over both payment crates.
  Advisories, bans, licences and sources all pass. The config bans the
  makepad-* crates outright and restricts sources to crates.io.

1.3 canonical ownership (ADR 0002):
- Record the domain/storage/platform/UI layering and its one-way deps.
- The review's A5 "fork farm" table is stale: one copy each of parser.rs,
  store.rs, pending_store.rs and pay_flow_handler.rs, not three.
- Fix B1: store.rs parsed category, sub_category, status and confidence
  from disk then overwrote them with Default::default(), losing every user
  categorisation on reload. Persistence also wrote display names, which are
  not reversible, so this adds stable storage tokens with a legacy-display
  fallback so existing rows still load.
- Fix B4: clean/restore mapped '|' to '~' and reversed every '~', so
  "JOHN~DOE" loaded as "JOHN|DOE". Replaced with bijective backslash
  escaping covering the separator, newlines and carriage returns.
- B6 (f64 money) deliberately deferred to Phase 6: it is a type change that
  ripples into UI consumers.

These were previously recorded as untestable because nigig-core is not a
workspace member. That was wrong: the three files involved need only serde,
chrono, one log! macro and one app_data_dir() helper. The new
tools/test-mpesa-store-clean.sh supplies those shims in a throwaway crate
and runs 9 tests, two of which reproduced the defects before the fix.

1.4 boundary: enforced twice, by a CI manifest/import check and by the
deny.toml ban list.

1.5 shims: three re-exports in nigig-pay/src/lib.rs had zero callers and are
deleted. The remaining four carry a caller count and a named migration
target so they have a deletion plan rather than an open-ended lifetime.

1.6 scope (ADR 0001): accepted that Nigig Pay is a read-only tracker and
launcher, not a payment processor, until an authorised provider integration
exists. This is the decision the review required before further UI work.

SECURITY: a live Cloudflare API token was found committed in README.md,
present since the initial commit and pushed to a public remote. Removed and
recorded as R-SEC-001 in PAYMENT_RISK_REGISTER.md. Redaction does not revoke
it; it remains in history and must be rotated by the owner.

Validated on rustc 1.97.1: domain and storage each pass test --locked, fmt,
clippy -D warnings and cargo-deny; storage also passes --features sqlcipher;
domain benches run; 9 M-Pesa store tests pass. 49 manifests parse.
2026-07-27 15:23:20 +00:00
62dada264e build: consume Makepad sibling APIs through widgets 2026-07-27 04:22:42 +00:00
e73def6d1b build: align Makepad dependencies with Robrix fork 2026-07-27 03:32:08 +00:00
5af8a20d04 build: use Robrix upstream Robius dependencies 2026-07-27 03:26:49 +00:00
ffa60532ed refactor(pay): name local SMS results as observed evidence 2026-07-26 19:23:53 +00:00
4ffb627dc9 fix(pay): surface legacy pending store persistence errors 2026-07-26 19:06:37 +00:00
062f42ba27 fix(pay): surface ambiguous ussd outcomes for reconciliation 2026-07-26 18:59:20 +00:00
0b21781234 fix(pay): reconcile stale dispatches instead of failing them 2026-07-26 18:56:54 +00:00
f6750c8259 feat(pay): add domain coordinator and sqlite storage foundation 2026-07-26 18:42:02 +00:00
cc05abdc71 Initial commit 2026-07-26 19:38:26 +03:00