You are right that my first pass at R1 fell short. It deferred an item on a
judgement call, and it fixed three defects without regression tests naming
them. Four gaps, all closed here.
## 1. S8 was deferred; it is now done as far as the platform allows
I skipped certificate pinning as "wasted work if the endpoints get dropped".
That was my call to make about effort, not an external blocker.
Investigated properly: Makepad's HttpRequest exposes no pinning API. Its
only TLS control is set_ignore_ssl_cert, which weakens verification. Pinning
is not implementable at this layer without patching the platform crate.
What *is* enforceable is the property pinning mostly buys — that a mistyped,
injected or attacker-supplied URL cannot be dialled. check_transport gates
every request on HTTPS plus a four-host allowlist, at all three dial sites
in both copies of the client.
7 tests: lookalike hosts (api.coingecko.com.evil.example), embedded
credentials (https://evil@real/), explicit ports, plain HTTP, malformed
URLs, and an assertion that TLS is never disabled. Verified by disabling the
allowlist: 3 tests fail.
## 2. The 13-digit phone defect had no test naming it
I fixed it and moved on. It now has a regression test quoting the original
duplicated branches, plus a property test that normalisation output is
either empty or exactly a valid 10-digit 07/01 number — no third outcome.
## 3. The fee-policy UI wiring was untested
The domain guard had 11 tests; the wiring that connects it to the pay sheet
had none, so nothing proved the sheet actually consults it. Four tests now
cover the shipped policy: it identifies the bundled tariff, refuses once
stale, still quotes while current, and keeps "unknown band" distinct from
"stale table".
## 4. The exchange client had no tests at all
It does now, via the transport module above.
## A test that failed against itself
tls_verification_is_never_disabled_in_this_module asserts the module never
calls set_ignore_ssl_cert — and the literal in the assertion put the string
in the file, so it failed on first run. The needle is now assembled at
runtime. Recorded because it is exactly the kind of thing that gets
"fixed" by deleting the test.
## Completion standard, now written into the plan
A phase is done when: no item is deferred on a judgement call; no capability
is removed to satisfy a review item; defects found while implementing are
fixed in the same phase even if absent from the review; every fix carries a
test that fails without it; and CI enforces it.
## Validation
domain 148 / storage 41 / platform 64 / mpesa 29 pass
nigig-pay-ui 72 (was 66) / nigig-mpesa 20 pass
clippy -p nigig-pay-ui --no-deps -D warnings 0 errors
builds: pay-ui, pay, mpesa, core pass
allowlist injection: 3 tests fail when disabled pass
pin-capture guard pass
Pre-existing and untouched: `cargo test -p nigig-pay --lib` fails to build
on clean HEAD (ClassifiedTransaction not in scope in transact.rs). Verified
by stashing. The transport tests are exercised through the nigig-mpesa copy.
All four R1 items. Two of them uncovered defects that were not in the
review, and R1.4's corpus found a live bug.
## R1.1 versioned fee policy (U9)
The band table is a static "effective Jan 2024" snapshot. When Safaricom
revises a tariff, nothing notices: the old number is quoted and the user
authorises a total they are not charged.
FeePolicy attaches provenance and a 400-day trust horizon. Past it,
fee_for returns FeeError::PolicyOutOfDate rather than a number, and the
sheet refuses to quote exactly as it already does for an unknown band —
"no band for this amount" and "our table is old" stay distinguishable
because they need different messages. Verified by disabling the check:
4 tests fail. Domain tests 137 -> 148.
## R1.2 quality gate for nigig-pay-ui (Q2)
Correcting my own earlier count: 9 of the 10 unwraps were in tests. The one
production case, on the dispatch path inside the biometric branch, is now a
fail-closed path — no request, no prompt, no dispatch.
nigig-pay-ui now denies unwrap_used/expect_used outside tests and CI runs
clippy --no-deps -D warnings. Scoped with --no-deps because matrix_client
and robius-ussd carry pre-existing warnings that are not this crate's to
fix, and a gate that fails on someone else's code gets disabled.
Turning the lint on surfaced 13 more issues, one a real defect:
normalise_phone had two identical branches, and the 13-digit "254…" arm
produced an 11-digit result — not a valid MSISDN, but non-empty, so it
flowed on as a recipient. The duplication was hiding it.
## R1.3 exchange API (S7/S8/S10)
The client forged origin/referer for api2.bybit.com and p2p.binance.com,
impersonating those exchanges' own web clients against internal endpoints.
Removed from both copies (nigig-pay and nigig-mpesa — item A5 again), along
with the framework-identifying User-Agent. CI rejects either regrowing.
Requests are still made, now honestly identified. If those endpoints reject
an honest client the P2P panes fall back to their offline cache, which is
the true state of the integration rather than a disguised one.
Not done, deliberately: certificate pinning. Pinning an endpoint the
product may drop is wasted work, and whether to keep these endpoints is a
product call recorded in the plan.
## R1.4 adversarial CSV corpus (7.5)
parse_csv turns an untrusted file into a payment list. Corpus covers empty
input, injection-shaped fields, overflow, NUL, RTL override, full-width
digits, a 5,000-row file and malformed numbers. The bar is not "parses
correctly" but "never silently produces a payment nobody intended".
Verified it can fail. UI tests 61 -> 66.
## Validation
domain 148 / storage 41 / platform 64 / mpesa 29 / pay-ui 66 pass
clippy -p nigig-pay-ui --no-deps -D warnings 0 errors
builds: pay-ui, pay, mpesa, core; default and --no-default pass
pin-capture guard pass
fee-policy injection: 4 tests fail with the check removed pass
corpus injection: catches a fabricating normaliser pass
Option A, as requested. Plus an audit of the whole review against the code.
## The default flips
nigig-pay-ui default = [] (leaf stays off; see below)
nigig-pay default = ["demo"]
nigig-mpesa default = ["demo"]
pageflipnav default = ["native", "demo"]
`cargo run -p pageflipnav` now drives *334#, shows the PIN field and
dispatches. That is the app's primary function and it works out of the box.
A release build opts out:
cargo build -p pageflipnav --no-default-features --features native
This was not one line. A first attempt flipped the four `default =` lines
and the opt-out still leaked: the app crates depended on nigig-pay-ui with
its own defaults, so `--no-default-features` on pageflipnav was silently
re-enabled one level down. Verified with a compile probe rather than
cargo tree, which truncates. The inner deps now carry
`default-features = false`, and the probe confirms both directions:
default -> demo ON, --no-default-features -> demo OFF.
ADR 0007's Play-policy note is untouched. The containment requirement of
review item 0.1 is not dropped — the flag exists, CI exercises both
directions, and a shipped build still cannot dispatch. What changed is
which way it points by default, so development and device testing are not
fighting it.
CI guards inverted to match: they now assert automation is on by default
*and* that the packaging opt-out still works. check-no-pin-capture.sh now
probes the packaging build, since the default legitimately captures a PIN.
## REVIEWS/IMPLEMENTATION_AUDIT.md
Every phase checked against the code, not against the tranche notes. Where
they disagreed the code won. Summary: phases 0-5 and 7 done bar 5.2 and
Keystore provisioning; phase 6 substantially done with the thread_local
session ownership outstanding; phase 8 partly.
## B7 found live while auditing
Month navigation had never been examined. Both copies of the transactions
widget still stepped months with Duration::days(31) and years with
Duration::days(366). Reproduced before touching it:
2025-12-28 -1 month => 2025-11-27 (drifts a day)
2026-03-30 -1 month => 2026-02-27 (drifts; repeated steps skip a month)
2027-06-15 +1 year => 2028-06-15 (366d wrong on a non-leap year)
Now uses checked_add_months/checked_sub_months, which clamp to the end of
the target month, and checked_add_signed on the day path. An
unrepresentable date leaves the view where it was.
Neither claimed done nor flagged open — simply never looked at. That is the
argument for auditing code rather than notes.
## Validation
domain 137 / storage 41 / platform 64 / mpesa 29 / pay-ui 61 pass
cargo check: pay-ui, pay, mpesa, core (default and opt-out) pass
demo-on-by-default probe, both directions pass
no-PIN-capture guard against the packaging build pass
Unrelated and still blocking a full APK: nigig-map fails to compile on
clean HEAD (12 errors, no field center_lat on ViewportState).
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.
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.
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.
- 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
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.
Reported twice as a bug: "Payment dispatch is unavailable in this build;
no M-Pesa PIN was used".
The gate itself is correct — review items 0.1/0.3 require that a default
build cannot move money. Two things around it were not.
## 1. The obvious command did not work
$ cargo check -p nigig-pay --features demo
error: the package 'nigig-pay' does not contain this feature: demo
Only nigig-pay-ui declared the feature. Neither app crate had a [features]
section at all, so the only way in was --features nigig-pay-ui/demo, which
nobody would guess. Anyone following the natural path hit a hard error,
concluded the build was broken, and had no way forward.
nigig-pay and nigig-mpesa now forward it: demo = ["nigig-pay-ui/demo"].
A CI guard asserts both crates forward it and that both compile with it,
tested against a reverted manifest to confirm it fails.
## 2. The message read like a build error
"unavailable in this build" describes the binary, not the product, and
offers no next step. It now says what the app does and what to do:
This app tracks payments — it doesn't send them. Send in the M-Pesa app
or dial *334#, and it will appear here automatically. Your PIN was not
requested or stored.
The internal log strings that say "compile with --features demo" are left
alone: those are for developers reading logs, not users reading a sheet.
## 3. No build instructions existed
Review item 1.1 asks for them and the README had none. Added: default vs
demo build, the two caveats that matter (USSD has an Android backend only,
so demo does nothing useful on desktop; and demo must not ship because of
the unresolved Play-policy risk in ADR 0007), native library setup, and
the test commands.
## Validation
domain / storage / platform / mpesa harness pass
nigig-pay-ui: cargo test --lib pass (61)
cargo check -p {nigig-pay,nigig-mpesa} --features demo pass
cargo check -p {nigig-pay,nigig-mpesa} (default) pass
demo-forwarding guard: verified to fail on a reverted manifest pass
No change to what the gate permits. Enabling demo is still a deliberate
act with the Play-policy exposure recorded in ADR 0007.
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.
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.
Phase 3 (secure repository) is now complete.
3.3 retention and erasure — new retention.rs:
- Data minimisation and financial record keeping pull opposite ways, so the
policy states both: evidence expires 90 days after settlement, settled
records are held for a five-year floor, never-dispatched intents expire in
30 days, and anything awaiting reconciliation is never swept. Deleting an
unresolved payment would destroy the only record of money that may have
left the account.
- erase_payment_on_request honours a user deletion request without
destroying the accounting record: it normally erases the parsed copy of
the user's inbox and retains the financial record, erases fully only past
the floor, and refuses outright while in flight. Refusals carry a reason.
3.4 backup, restore and corruption reporting:
- integrity_check and foreign_key_check surface damage as StorageError
::Corrupt rather than letting a damaged ledger be trusted.
- backup_to uses SQLite's online backup API instead of copying the file, so
a concurrent writer cannot produce a torn copy, and refuses to back up a
database that fails its integrity check.
- restore_from validates the candidate in a separate connection before
touching the live database, so a failed restore cannot destroy a working
ledger. Tested with both a missing and a corrupt backup.
Storage tests 20 -> 36.
Phase 4 (blocking I/O) — 4.1 to 4.3 implemented:
- 4.1 expenses/mod.rs called reload(), a full parse of the store from disk,
as the first statement of draw_walk; scrolling re-read the whole ledger
every frame. Now loaded once, with invalidate()/reload_if_stale() on the
event path.
- 4.2 the SMS scan was a wall-clock check inside draw_walk, so the branch ran
every frame and the scan landed mid-paint. Now a cx.start_interval timer on
the event path, with the interval named rather than inline.
- 4.3 update_summary walked the filtered list and rewrote labels every frame.
Now invalidated only in refresh_filtered and applied in handle_event, fully
out of the draw path rather than guarded inside it.
- 4.4 already satisfied by PaymentStorageWorker since tranche 1.
Supporting domain work: view_state.rs adds PeriodSummary and
TransactionsViewState so the summary arithmetic is testable outside a widget.
The tests immediately pin two things the inlined code got wrong: totals now
report overflow instead of wrapping, and a net outflow returns None rather
than a clamped zero. Money gains Default (zero).
Domain tests 66 -> 75.
Validated on rustc 1.97.1: domain and storage each pass test --locked, fmt,
clippy -D warnings and cargo-deny; storage also --features sqlcipher; domain
benches run; 7 M-Pesa store tests pass; 49 manifests parse.
NOT VERIFIED: the nigig-pay widget edits for 4.1-4.3 are not compiled here.
That crate needs the git-hosted Makepad fork. cargo check -p nigig-pay on a
full developer checkout is required before Phase 4 can be called complete.
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.