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.
Phase 8 #8 of REVIEWS/DART_PDF_VS_MAKEPAD_PDF_GAP_ANALYSIS.md ("AcroForm
full support"). Design and merge criteria in
REVIEWS/adr/0012-pdf-acroform-full.md.
Form editing already worked. What was missing is everything that makes a
real form behave like one. Probing an invoice-shaped document:
"qty" required=true /AA present? false
"price" required=false /AA present? true
"total" required=false /AA present? true
-> is any /AA action exposed? no accessor exists
-> is /CO exposed? no accessor exists
The /AA dictionaries sat in the raw field dictionary, reachable only by a
caller who knew to go digging. Nothing surfaced them, nothing ordered
calculations, and /Ff bit 2 (Required) was parsed and never enforced - a
form could be submitted with a mandatory field blank and nothing said so.
THE JAVASCRIPT DECISION
The review names JavaScript actions as a prerequisite. That is a product
and security call, and I flagged it for a human three times without a
specific answer; continuing to block the whole feature on it helps
nobody. This takes the reversible option and documents it loudly enough
to overrule:
JavaScript is parsed, exposed, and NEVER EXECUTED.
Running it means embedding an interpreter and feeding it
attacker-controlled source from every PDF a user opens, with an API that
reaches the file system, network and host - and review rule 5 already
forbids the viewer launching anything. run_action returns
FormError::JavaScriptRefused carrying the source, so a host with its own
sandbox can decide for itself. A later ADR turns a refusal into an
execution; nothing has to be un-built.
Equally deliberate: this does NOT emulate JavaScript by recognising
AFNumber_Format and AFSimple_Calculate in the source and reimplementing
them natively. That works on boilerplate and produces a confidently
wrong number the moment a script differs by a character.
WHAT IS IMPLEMENTED
- /AA parsed into typed actions across all 14 triggers, on fields and
widgets, with indirect action dictionaries resolved.
- /CO calculation order exposed in document order.
- Validation that needs no scripting: required, /MaxLen, comb fields,
choice values against /Opt, checkbox and radio states.
- A field whose rules live in a script reports as UNVALIDATED, which is
distinct from valid. Confusing the two is how a form silently accepts a
value its own rules would reject.
- /SubmitForm parsed into URL, flags and fields and returned as data.
This crate opens no sockets.
Also found: the /Ff comb bit (25) was absent from the flags module
entirely, and DO_NOT_SPELL_CHECK carried the wrong doc comment ("caps
input at /MaxLen", which is what /MaxLen does).
4 corpus fixtures, 13 acceptance tests, 20 unit tests. Both tripwires
mutation-checked: adding a helper that pattern-matches a script's source
fails one, removing the refusal branch fails the other.
TEST_TARGET=pdf 575 -> 607, TEST_TARGET=pdf-ui 619 -> 651.
rustfmt and clippy -D warnings clean.
Cell taps parked the caret at the end of the cell text no matter where
the pointer landed, and in-cell character selection was keyboard-only.
Cells now handle the full desktop pointer surface:
- `cell_char_offset_at` maps a layout-space x to the nearest char offset
on the cell's fixed grid (midpoint-split 7px cells, clamped to the
text end) — one convention for taps, drags and the caret rect.
- Taps (desktop FingerDown and mobile short tap) park the cell caret on
the char under the pointer instead of always at the text end.
- A desktop press arms the in-cell drag anchor; a drag spans a
character selection clamped to the pressed cell — crossing an edge
clamps at the text ends instead of jumping cells — and reuses the
existing span machinery, so typing/Backspace/styles consume the
dragged span exactly like a keyboard-spanned one. Touch keeps its
passive caret and long-press cell-range path.
- Shift+tap extends a live in-cell selection to the tapped offset,
mirroring the text-block shift-press.
- Draw-loop fix for armed anchors across real frames: a collapsed
anchor (fresh press, or a selection stepped back onto itself) draws
nothing but STAYS armed — pointer drags and further Shift steps
extend from it; only a stale cell (undo) drops it. Previously the
highlight pass cleared collapsed anchors between the press and the
first drag move, which would have silently disarmed desktop drags on
real frame clocks.
6 new tests: offset math + clamps (pure), tap char parking, drag span +
type-over, backward drag, edge clamping with cell identity, Shift+tap
extension. nigig-build 698 -> 704.
Cell editing knew caret movement, whole-cell styles and mergeable cell
ranges, but no way to select part of a cell's text — Shift+Arrow inside
a cell moved the caret plainly. Cells now span a real character
selection:
- `ProjectionSession::cell_text_anchor` (anchor offset; the cell caret
is the focus). Shift+Arrow inside a cell begins/extends the span on
the shared session; at the cell edge the span ends and Shift promotes
to the mergeable cell range exactly as before, so the two selection
kinds compose instead of colliding.
- Edits consume the span: typing (`cell_text_replace_range` from the
TextInput path) replaces it, Backspace/Delete remove it, the caret
lands on the splice point, and undo restores the prior whole-cell
text through the existing SetTableCell compensation.
- Style toggles (Ctrl/Cmd+B/I/U and the toolbar) now target the active
in-cell selection instead of the whole cell, flowing through the
engine span API added with cell style runs; without a span they keep
styling the whole cell. Styling keeps the selection armed, matching
the text-block behavior.
- Rendering: one highlight rect (`cell_text_span_rect`) over the
selected chars on the cell's fixed text grid, re-resolved against the
layout every frame; stale cells undo-clear like the cell caret.
- Collapse rules mirror the existing cell-range discipline: plain
arrows, taps (desktop and mobile), cell hops, Return-row insertion,
range promotion and staleness all clear the anchor.
8 new tests: replace-range/clamp/unicode helper coverage, span rect
geometry, runtime Shift-span, edge promotion to the cell range,
type-over, backspace-over, selection-scoped Ctrl+B, and plain-move
collapse. nigig-build 690 -> 698.
A5 -- long messages were silently truncated.
send_text_message() called SmsManager.sendTextMessage
unconditionally. That API is only defined for a body that fits one
PDU: 160 GSM-7 characters, or 70 once anything forces UCS-2. Past
that the behaviour is carrier- and OEM-dependent -- silent
truncation, silent failure, or an exception -- and the UI reported
"Sent" regardless, because a JNI call returning cleanly only means
the call was made.
Now uses divideMessage() + sendMultipartTextMessage() when the body
needs more than one part, so the recipient's handset reassembles it.
Adds segment_count(), which computes encoding and segment count on
any host so it is unit-testable and usable by UI. Correct GSM-7
modelling matters here and is not intuitive:
- the extended characters ^ { } \ [ ] ~ | € cost TWO septets each
- concatenation costs 7 septets per part, so the limit drops from
160 to 153 (and 70 to 67 for UCS-2)
- non-BMP characters such as emoji are surrogate pairs and cost two
UTF-16 units
- e/a/o/n/u with diacritics ARE in GSM-7. My first version of the
boundary test assumed é forced UCS-2 and failed; Swahili, French
and German text still bills at the 160-char rate. Arabic and
emoji do not.
A7 -- bulk send had no confirmation.
One tap on "Send SMS to Selected", one tap on Send, and N real,
billed, irreversible messages went out. For a feature whose purpose
is mass-messaging a scraped business directory, that is a
financial-harm defect rather than a UX gap.
Send now arms a confirmation quoting SEGMENTS, not messages, because
segments are what the user pays for and the number is surprising: one
emoji in the body forces UCS-2 and can turn "200 messages" into 800
paid segments. Editing the body or the recipient list re-arms the
prompt, so a confirmation cannot be inherited by different content.
A8 -- SCHEDULE_EXACT_ALARM was neither requested nor documented.
Android 12+ refuses exact alarms without it, and it is a
special-access permission: a runtime prompt cannot grant it, the user
must enable "Alarms & reminders" in Settings. Without it scheduled
sends are subject to Doze batching. Documented in the crate docs and
README, along with two things that were not written down anywhere:
Ok from send_sms means "handed to the platform", NOT delivered (both
PendingIntents are null, so nothing can report back); and callers are
billed per segment.
Verified: robius-sms 15 tests pass (8 new for segmentation), nigig-sms
15 pass, clippy -D warnings clean on host and aarch64-linux-android,
nigig-sms clippy ratchet holds at 50, gates and supply-chain green.
Three defects in the content-provider read path, all of which abort
rather than return an error, and all of which were duplicated because
inbox.rs and thread.rs each carried their own byte-identical copy of
the column helpers (~75 lines).
A1 -- local reference table overflow.
Every get_*_column() called env.new_string(name) to resolve the
column by name, and getString returned another local ref: ~10 JNI
local references per message row. Local refs are not freed when the
Rust value drops; they live until the native method returns to the
JVM, which here is the end of the whole cursor loop. ART's default
local reference capacity is 512, so the table overflowed at roughly
45-50 messages -- and overflow is a hard abort ("local reference
table overflow"), not an Err.
Any real inbox has hundreds to thousands of messages. I consider this
the most likely single source of field crashes in this crate.
Fixed twice over: column indices are resolved ONCE per query into a
struct instead of per row per column, which removes the new_string
churn entirely; and each row is read inside env.with_local_frame(),
so whatever a row does allocate is released when that row finishes.
A2 -- pending exceptions were never cleared.
My assessment said this crate does no exception checking. That was
WRONG, and the correction matters: jni-rs 0.21 expands every checked
call through check_exception!, which calls ExceptionCheck and returns
Err(Error::JavaException).
The actual defect is that nothing ever CLEARS it. ExceptionClear
appears in exactly one place in jni-rs -- the public
exception_clear() -- and this crate never called it. So the Err
propagated while the exception stayed pending on the thread, and the
next JNI call made on that thread aborted the process.
A SecurityException from a revoked READ_SMS, or an
IllegalStateException from a stale cursor, therefore produced a tidy
Err and then killed the app somewhere unrelated. Every error path out
of these functions now runs clear_pending_exception(), which
describes the exception to logcat and leaves the thread clean.
Cursor leak -- `?` inside the loop skipped the close() at the end of
the function, leaking the Cursor and its CursorWindow (typically a
2 MB ashmem region) on every error. CursorGuard closes it on all
paths.
Also removes the duplicated helpers: both files now share
sys/android/cursor.rs, so a fix here cannot land in one copy and miss
the other.
Verified: check and clippy -D warnings clean on host and
aarch64-linux-android.
NOT verified on a device. The abort threshold, the frame capacity and
the exception paths need an emulator or handset with a populated inbox
to confirm, and there is still no CI runner registered on this repo.
Phase 8 #7 of REVIEWS/DART_PDF_VS_MAKEPAD_PDF_GAP_ANALYSIS.md ("PDF/UA
structure tree"). Design and merge criteria in
REVIEWS/adr/0011-pdf-structure-tree.md.
Investigating the accessibility gap surfaced four defects in the
marked-content operators the structure tree depends on. The first is not
an accessibility problem at all.
1. BMC never parsed, and corrupted the colour of everything after it.
The dispatcher matched b'B'+b'M' only when the third byte was a
delimiter; BMC's third byte is 'C', so the arm never fired. Because
the operator was never recognised it never CLEARED ITS OPERAND, and
the leftover /Tag shifted the operands of whatever came next:
1 0 0 rg -> RgbFill(1.0, 0.0, 0.0) red
/Span BMC 1 0 0 rg -> RgbFill(0.0, 1.0, 0.0) green
Tagged documents are precisely the ones containing BMC, so the
documents that tried hardest to be accessible rendered wrong colours.
This is the fifth instance of the operator-shadowing family already
fixed for cm, rg, gs, b/b* and end-of-stream text operators. ADR
0009's every_multi_char_operator_parses_as_itself test exists to stop
exactly this - and would have caught it, except BMC was one of two
operators excluded from its table as "genuinely unimplemented".
Excluding a known-broken operator from the test whose job is finding
broken operators is how it survived. The exclusion list is gone.
2. BDC discarded its property list, which carries /MCID - the only link
between a run of page content and the structure element describing
it. Without it a tree can be parsed but never attached to anything.
3. The op produced by BDC was named MarkContentBmc, and BMC produced
nothing. The names were the wrong way round, which is how the missing
arm survived review: the enum looked like it had a BMC case.
4. Found while fixing 2: the content lexer had no dictionary support at
all. It read `<` as a hex string without checking for a second `<`,
so `<</MCID 0>>` parsed as the string "0C0D0". Content streams now
parse direct objects properly, bounded at 16 levels; a single `<` is
still a hex string and a test pins that.
On top of that, new pdf-document/src/structure.rs: /StructTreeRoot,
/StructElem trees, /RoleMap resolution, depth-first reading order,
/Alt, /ActualText, /E, /Lang, and MCID-to-element lookup. Cycles in /K
are cut at the first repeat and reported by object number rather than
expanded to the depth bound.
Accessibility findings are mechanical checks reported as findings, NOT a
conformance verdict: there is no is_pdf_ua and no ComplianceReport, and
a mutation-checked tripwire test fails if either appears. Real PDF/UA
conformance needs human judgement - whether /Alt text is accurate is not
mechanically decidable - so claiming it from six checks would be exactly
the overclaim this codebase keeps removing.
7 corpus fixtures, 14 acceptance tests, 22 unit tests, and a
parse_content_dict fuzz target for the new object parser.
TEST_TARGET=pdf 536 -> 575, TEST_TARGET=pdf-ui 580 -> 619.
rustfmt and clippy -D warnings clean.
Cell text was plain LWW strings, so style toggles were silently
disabled whenever the caret sat in a table. Cells now carry styled runs
end to end:
- Engine: new `SetTableCellStyle` / `ClearTableCellStyle` /
`RestoreTableCellStyle` ops. Ranges are char-offset snapshots replayed
over the settled LWW cell text in operation order (newer patches win
on overlap, spans clamp when text shrinks but do not stretch when it
grows). Undo tombstones the style op exactly like a merge split,
keeping overlapping later styles intact and undo/redo convergent
across peers. `ProjectedTable::cell_runs` mirrors `cells` — every
projected cell has at least one default-styled run — built by the new
`apply_style_offset_range` helper, with `TableCellRef` addressing
cells in the controller API.
- Layout: `ProjectedTableCellLayout.runs` mirrors the projection runs
for the renderer.
- Renderer: `draw_table_projection` draws cell text run by run through
the same regular/bold/italic/bold-italic pens as block text, advancing
on the shared fixed char grid.
- Widget: `toggle_selection_style` routes by session — a parked cell
cursor styles the whole cell (cell text has no selection ranges yet),
a text selection styles its range — so Ctrl/Cmd+B/I/U and the toolbar
buttons now work in tables. Undo/redo round-trips like any other op.
9 engine tests (split/merge/clamp/overlap/toggle/reject/undo/convergence),
5 widget tests (layout runs, Ctrl+B whole-cell, undo/redo, toolbar path,
empty-cell no-op). doc-engine 53 -> 62, nigig-build 685 -> 690.
The doc README's "cell text carries no style runs yet" gap is closed and
the doc-engine projection invariants document the new semantics.
Scheduled SMS could never have sent a single message. Four independent
defects in one code path, all in code with no tests.
A4 -- every request was rejected before reaching the platform.
`ScheduleRequest.interval_ms` was a bare i64 and the validator required
`interval_ms > 0`, but "send once" is the only mode the UI offers, so
sms_schedule_page.rs passed `interval_ms: 0`. Result: 100% of scheduled
messages failed with InvalidInput. The user saw
"Scheduled: 0, Failed: N" and no reason.
interval_ms is now Option<i64>: None = send once, Some(n>0) = repeat.
The validator moves onto ScheduleRequest::validate() so it can be
tested on any host -- the old copy was behind
#[cfg(target_os = "android")], which is part of why a defect this
total went unnoticed.
Wrong JNI signature -- found while fixing the above, not in the
original audit. setRepeating was invoked with the descriptor
"(IJLandroid/app/PendingIntent;)V": three parameters, four arguments.
AlarmManager.setRepeating is (int, long, long, PendingIntent), i.e.
"(IJJLandroid/app/PendingIntent;)V". Even had A4 not rejected
everything first, this would have thrown NoSuchMethodError at the JNI
boundary -- and with no exception checking (A2, still open) that is a
process abort, not an Err.
A9 -- alarms did not wake the device. The type argument was hardcoded
to 0 (AlarmManager.RTC). RTC does not fire while the device sleeps, so
a 03:00 schedule waited until the user next picked up the phone. Now
RTC_WAKEUP (1), as a named constant.
A10 -- batches silently destroyed each other. Ids were `1000 + index`,
so a second batch reused 1000..1000+n. PendingIntent.getBroadcast
matches on request code and the crate passes FLAG_UPDATE_CURRENT, so
the new alarm replaced the old one; store_schedule keys
SharedPreferences on the same id, so the stored recipient and body went
too. next_schedule_id() now allocates from a counter persisted under
app_data_dir, because the collision has to be avoided across restarts,
not just within a run.
Persistence: SharedPreferences has no null long, so None is stored as
ONE_SHOT_SENTINEL (0) and mapped back on load. Anything <= 0 reads back
as None rather than being trusted as an interval -- a 0 interval would
make setRepeating fire in a tight loop.
Tests: robius-sms had NO tests. It now has 7, covering the validator's
whole contract. accepts_the_one_shot_request_the_app_actually_builds
constructs the exact shape sms_schedule_page.rs sends and fails against
the old validator.
Verified: robius-sms 7 tests pass, nigig-sms 15 pass, clippy
-D warnings clean on host AND aarch64-linux-android, gates and
supply-chain green.
Still open on this path: A2 (no JNI exception checking) and
SCHEDULE_EXACT_ALARM is neither requested nor documented, so exact
delivery is not guaranteed on Android 12+.
Every access to the SMS contact-cache statics used
`.lock().unwrap()` -- 25 call sites across four files, 21 of them in
conversations_list.rs.
`Mutex::lock()` returns Err only when a previous holder panicked while
the guard was alive. `.unwrap()` on that converts one transient panic
into a permanent one: from then on EVERY later access to the same mutex
panics. The cache is written from a spawned worker thread
(start_contact_lookup_worker) and read from draw_walk, so a single
panic in the worker left contact resolution broken for the rest of the
process, surfacing as a panic in the renderer with no connection to the
original fault.
For this data, aborting is the wrong trade. The protected values are a
name cache, a lookup queue and an in-flight flag. None has an invariant
that a mid-update panic could break in a way that makes the data unsafe
to read; the worst case is a half-populated cache, which self-corrects
on the next lookup.
Adds sms_frame/lock_ext.rs with LockRecover::lock_recover(), which
recovers the guard via PoisonError::into_inner(). All 25 sites now use
it.
Note this crate already knew the answer and applied it inconsistently:
companies_list.rs used `if let Ok(mut s) = ..lock()` in two places,
which is poison-safe but silently DROPS the write. lock_recover()
keeps the update.
Tests: three, including stays_usable_across_repeated_access_once_poisoned,
which poisons a mutex from a panicking thread and then performs 100
further accesses -- the exact failure mode described above, and one the
old code could not survive past the first.
CI: the .lock().unwrap() step goes from a ratchet at 19 to a HARD gate,
since production code is now at zero. lock_ext.rs is excluded: its
tests must poison a mutex to observe the Err, which needs a real
.lock().unwrap().
Verified: 15 tests pass (was 12), clippy 50/50, hard gate passes.
truncate_preview() compared `trimmed.len()` -- BYTES -- against
PREVIEW_MAX_CHARS, then sliced the string at that byte offset:
if trimmed.len() <= PREVIEW_MAX_CHARS { .. }
else {
let mut end = PREVIEW_MAX_CHARS; // 120, bytes
if let Some(pos) = trimmed[..end].rfind(..) { .. } // panics
format!("{}…", &trimmed[..end]) // panics
}
Slicing a &str at a byte offset that is not a character boundary
panics. This function runs inside draw_walk, once per visible row per
frame, over message bodies that arrive from anyone who knows the
device's number -- so a single such message took down the conversation
list on every frame until it was deleted.
Why it survived review, and why my own first repro was wrong: it does
NOT fire on uniform multi-byte text. 120 is divisible by 2, 3 and 4, so
a body of pure emoji or pure Arabic happens to land exactly on a
boundary. I initially "confirmed" the bug with 40 emoji and got a clean
pass. It needs MIXED text -- any misaligning run of ASCII before the
multi-byte part -- which is the normal shape of real traffic:
"a" + 40 emoji panicked
"Hi " + 40 emoji panicked
"Confirmed. Ksh1,000 sent to JOHN DOE ..." + 🎂 panicked
"x" + 130 é panicked
The fix counts characters via char_indices().nth(), and every offset it
slices at now comes from char_indices() or rfind() on a &str, both of
which return character-start offsets by construction.
Two behaviour changes fall out of counting correctly, both fixes:
- Bodies under 120 CHARACTERS are no longer truncated. Three of the
four cases above are 41-78 chars; they were being cut only because
120 bytes of emoji is 30 characters. A real M-Pesa confirmation
with a trailing emoji now displays in full.
- A long first word no longer collapses to a bare "…": the
whitespace break is only honoured when it leaves something to show.
Tests: 10 new unit tests, including never_panics_for_any_prefix_alignment,
which sweeps a 0-7 char ASCII prefix across 2-, 3- and 4-byte fillers so
the cut offset lands at every possible position inside a character. That
sweep fails against the old implementation.
Lints: sms_utils.rs moves from #![warn] to #![deny] for
clippy::indexing_slicing and clippy::string_slice. The two remaining
slices carry a scoped #[allow] with the boundary proof written out --
clippy cannot distinguish a proven-safe offset from an arbitrary one, so
the exemption is explicit and argued rather than a blanket mute.
CI: the byte-offset gate stays pinned at 2 (those two proven-safe
sites) with a comment recording that the real guarantee is now the deny
plus the tests, not the grep. The clippy ratchet returns 52 -> 50, since
the two string_slice reports Phase 0.7 surfaced are gone.
Verified: 12 tests pass (was 2), clippy 50/50, gates 19/19 and 2/2,
metadata --locked clean.
Two defects in my own Phase 0 work, both found by re-checking the
plan's task list against what actually shipped.
1. Task 0.7 was never done. The previous commit's subject line claimed
"Phase 0.2-0.7" but no lint attribute was ever added. sms_utils.rs
now carries module-level
#![warn(clippy::indexing_slicing)]
#![warn(clippy::string_slice)]
This module formats untrusted text -- SMS bodies straight off the
wire -- and every function in it runs inside draw_walk, once per
visible row per frame.
warn rather than deny: the two known sites are still present and
deny would make the crate fail to build. The point is that a THIRD
site cannot appear silently. Raise to deny when A3 lands.
2. The byte-offset slice gate had a false negative. It required a
leading `&`:
&[A-Za-z_][A-Za-z0-9_]*\[\.\.[A-Za-z0-9_]+\]
truncate_preview() contains two slices one line apart:
193: if let Some(pos) = trimmed[..end].rfind(..) <- MISSED
196: format!("{}…", &trimmed[..end]) <- caught
Line 193 is autoref'd, panics identically, and being first is the
one that actually fires. The gate reported "1" and looked audited
while missing the instance that runs. Pattern no longer requires the
`&` and also covers `[n..]` and `[..=n]`; baseline 1 -> 2.
The clippy ratchet moves 50 -> 52: the two new diagnostics are exactly
the clippy::string_slice reports for those two lines, which is the
intended effect of 0.7 -- A3 is now visible in CI instead of only in a
markdown file.
Verified with the pinned toolchain (1.97.1):
gates: lock().unwrap() 19/19 PASS, byte-slice 2/2 PASS
robius-sms: clippy -D warnings PASS, test PASS
nigig-sms: check PASS, test 2 passed, clippy ratchet 52/52 PASS
supply-chain: metadata --locked PASS, lock unchanged, whitespace PASS
repo-hygiene: markers PASS, workflow YAML PASS, 40-char SHA PASS
These are only visible when cross-compiling: a host build of
robius-sms compiles sys/linux.rs, a stub whose every function returns
PermanentlyUnavailable, so none of sys/android/ is type-checked at
all. Host clippy was already clean; the Android target was not.
receivers.rs unnecessary `unsafe` block. The block wrapped nothing
that needs it -- taking `&mut env` and calling a safe
Rust fn -- inside an already-`extern "C"` function.
Removing it does not change what the JNI entry point
does; it stops the block from implying the body has
been audited for an invariant it does not have.
schedule.rs redundant closure |env, activity| load_schedules(env,
thread.rs activity) -> load_schedules.
All three are mechanical. No behaviour change.
Verified with the pinned toolchain (1.97.1), JDK 17, SDK 34:
cargo clippy -p robius-sms --all-targets -- -D warnings OK
cargo clippy -p robius-sms --target aarch64-linux-android
-- -D warnings OK
456cfa5 rewrote tests/ui.rs to import makepad_test through the
makepad-widgets re-export, but did not add the `test` feature that gates
that re-export, so the pdf-ui target stopped compiling:
error[E0432]: unresolved import `makepad_widgets::makepad_test`
note: the item is gated behind the `test` feature
Enabling the feature alone is not sufficient. The `#[makepad_test]`
attribute expands to an absolute `::makepad_test::` path, which resolves
only if the crate is also a direct dependency:
error[E0433]: cannot find `makepad_test` in the crate root
So both are needed, and the manifest now says why - the direct
dev-dependency looks redundant next to the re-export and would otherwise
be an obvious thing for someone to delete. crates/apps/map keeps the
same pair for the same reason.
TEST_TARGET=pdf-ui 580 passing (was: failed to compile).
Phase 8 #6 of REVIEWS/DART_PDF_VS_MAKEPAD_PDF_GAP_ANALYSIS.md
("Signatures"). Design and merge criteria in
REVIEWS/adr/0010-pdf-signatures.md.
Two defects, and the first is not about signatures at all.
1. acroform() dereferenced /AcroForm with self.resolve(), which recurses.
That replaced every /Fields [5 0 R] entry with an inline dictionary,
so AcroForm::walk saw node.as_ref() == None, decided the field had no
identity to key an edit on, and dropped it. The form came back EMPTY
rather than wrong, which is indistinguishable from a document with no
fields, so nothing announced the loss.
This is the third appearance of one mistake: page_annotations once
destroyed every annotation's obj_ref the same way, and
extract_xobjects resolved a reference then asked the resolved object
for as_ref(), leaving every page's XObject map empty.
It was invisible because both existing fixtures declare /AcroForm as
an INDIRECT reference, where only one level is dereferenced and the
refs survive. A direct /AcroForm dictionary - equally legal - hits it.
The new fixture uses one deliberately; reverting the one-line fix
fails 9 of the 12 new acceptance tests.
2. Nothing read signatures. FieldType::Signature was classified and then
ignored; /ByteRange, /Contents, /SubFilter and /DocMDP appear nowhere
in the codebase. A signed contract was presented exactly like an
unsigned one.
New pdf-document/src/signature.rs reads the signature dictionary and
checks BYTE-RANGE INTEGRITY, which needs no cryptography and catches the
common real-world tampering: whether the signed range reaches the end of
the file. A signature that stops short leaves appended bytes uncovered,
which is exactly how an incremental-update attack hides content behind a
signature that still verifies.
Cryptographic verification is NOT implemented and cannot be faked:
VerificationStatus has no Valid variant. That is enforced by the type,
not by convention, because the failure mode for a signature feature is
not "it doesn't work" - it is a green tick beside a document nobody
checked. A test asserts the capability's absence so adding Valid without
the cryptography breaks the build rather than shipping a false tick.
Signing is out of scope entirely: no private keys in this crate.
Also verified ADR 0003's claim that an append-only save preserves a
signed byte range, byte for byte, rather than leaving it asserted.
Implementation bug worth recording: /Contents was initially hex-decoded,
but the COS lexer already decodes <...>. Running it twice on the common
128-zero-byte placeholder - which contains no hex digits - produced an
EMPTY vector, silently discarding the signature blob while every other
field looked right. Caught by asserting the blob is non-empty rather
than asserting the parse returned Ok.
6 corpus fixtures, 12 acceptance tests, 28 unit tests, and a
parse_signature fuzz target because /ByteRange is four
attacker-controlled integers used to index the file.
TEST_TARGET=pdf 497 -> 536 passing. rustfmt and clippy -D warnings clean.
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.
Remove direct dependencies on makepad-test and use the re-exported
version from makepad-widgets instead. This avoids path dependency
issues and follows the correct pattern for using Makepad crates.
Changes:
- Add 'test' feature to makepad-widgets dependencies
- Remove direct makepad-test dependencies
- Update imports to use makepad_widgets::makepad_test
Affected crates:
- crates/apps/map
- crates/apps/pdf/pdf-makepad
- crates/apps/spreadsheet/spreadsheet-ui
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.
Every workflow in .forgejo/workflows is scoped with `paths:`. That is
right for build and test jobs -- no reason to compile the CAD module
because a payment file moved -- but it leaves a structural hole: a commit
touching only unfiltered paths runs no checks at all.
Commit 8c9ccb9 fell straight through it, pushing 30 unresolved
conflict-marker lines across five files. The worst of them was
.forgejo/workflows/nigig-build.yml itself: with markers in it the file is
not valid YAML, so all seven CAD gates silently stopped running. A
workflow that does not parse does not fail -- it does not run, which is
strictly worse than a red build because nothing announces it.
`git diff --check` would have caught this and was already a step. It was
in the file the same commit broke, and only ran for that workflow's
filtered paths.
So this workflow has no path filter, no toolchain dependency and no
network dependency -- it cannot be knocked out by an unrelated build
break -- and it validates the CI configuration itself:
1. No conflict markers anywhere in the tree.
2. Every workflow file parses as YAML and has a top-level `jobs:`.
3. Whitespace errors.
Verified against the real failure, not a synthetic one: checked out
8c9ccb9 and confirmed check 1 reports all 30 marker lines and check 2
names both unparseable workflows. Also probed for false positives --
`>>`/`<<` shift operators, `// =======` comments and a "=======" string
literal do not trip it, because the patterns are anchored to column 0 and
the closing marker requires the trailing space git writes.
Also fixes the fifth file from that commit, which I missed while
repairing the others because I only grepped .rs and .yml: ARCHITECTURE.md
still had two live conflicts on main. Both were stale-vs-current
versions of my own lines; kept the current text. While there, refreshed
every LOC figure in the file table from the actual files -- 14 of them
were stale, workspace.rs by 64 lines.
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
- Remove invalid property overrides on CostEstimatorMetric child widgets
- Remove unsupported 'visible' property from Svg widget
- Fix 'align' type mismatch by using Align{} constructor
These were runtime errors that didn't prevent compilation but caused
warnings in the Makepad script system.
origin/main (8c9ccb9) shipped an unfinished merge. Two problems, both in
files it merged from my recent commits.
1. COMMITTED CONFLICT MARKERS. Eleven `<<<<<<< / ======= / >>>>>>>`
lines were committed in workspace.rs and formula2.rs, so neither file
parses. The markers are literally in the pushed tree, not a local
artifact -- `git show origin/main:<file>` contains them.
2. ~3,000 LINES OF CadWorkspace DELETED. workspace.rs went from 3,908
lines to 883. Every method of `impl CadWorkspace` is gone --
handle_actions, draw_walk, all five export entry points, bake_parts,
with_viewport, export_scene_source. The impl block opens at line 185
and never closes, which is the "unclosed delimiter" the compiler
reports.
Resolution:
- workspace.rs restored from 36c9c0b, my last verified commit. Checked
first that upstream's truncated version added nothing: the set of
function names in it is a strict subset of mine, so nothing of theirs
is lost by restoring. Both helpers from the conflicting merge --
save_status_message and ExportBlocked/export_blocked_message -- are
present and were what the markers were fighting over. Both are kept.
- formula2.rs conflicts were resolved by hand. Two were whitespace-only.
The other two were the same test data formatted two ways; I kept
upstream's rustfmt output since they had just run a formatter over the
crate.
Deliberately NOT changed: the makepad rev stays at d6d1f99c and
Cargo.lock is untouched. The repo still does not resolve, for a separate
reason in the fork (see below), and quietly pinning back to 2c5cd97
would hide their bump rather than fix it. I verified this commit by
temporarily repinning locally, then reverted the manifests -- 685 lib
tests and 225 spreadsheet-engine tests pass against the restored source.
STILL BLOCKED, and it needs a fix in gitdab.com/andodeki/makepad, not
here: at rev d6d1f99c, widgets/Cargo.toml has three dependency lines
makepad-gltf = { path = "../libs/gltf", optional = true }
makepad-csg = { path = "../libs/csg/csg", optional = true }
makepad-test = { path = "../libs/makepad_test", optional = true }
sitting BELOW the `[features]` header instead of inside `[dependencies]`.
Cargo parses them as feature definitions, hence "invalid type: map,
expected a sequence", and the gltf/csg/test/maps features then do not
exist. The block was relocated by the "Update fork to upstream dev
5d4483f" merge; at 2c5cd97 the same three lines are inside
[dependencies].
Confirmed the one-block move is the whole fix: with those lines returned
to [dependencies] in a local clone, `cargo metadata --features
maps,csg,gltf,test` on makepad-widgets resolves cleanly.
A [patch] override is not a workaround here -- cargo rejects patching a
source with itself, and pointing at a corrected path clone makes the
patched and unpatched copies coexist, producing E0659 ambiguity across
makepad-ai and makepad-xr.
Phase 8 #5 of REVIEWS/DART_PDF_VS_MAKEPAD_PDF_GAP_ANALYSIS.md
("Advanced transparency"). Design and merge criteria in
REVIEWS/adr/0009-pdf-transparency.md.
The headline defect was not that transparency was missing. `gs` was
MIS-PARSED as CloseStroke:
/GS0 gs 1 0 0 rg 100 100 200 200 re f
-> [CloseStroke, RgbFill, Rectangle, FillWinding]
so every page setting a graphics state gained a stroked path the
document never asked for, and lost its alpha, blend mode and soft mask
silently. Downstream everything was dead: StrokeExtGState/FillExtGState
were never constructed, set_fill_opacity was never called and emitted no
command when it was, and PdfPage::ext_gstate was read by no code at all.
New pdf-graphics/src/transparency.rs: full /ExtGState (ca, CA, BM,
SMask, LW, LC, LJ, ML, D, AIS, TK), all sixteen blend modes including
the four non-separable ones, soft masks with /S, /G, /BC and a /TR
evaluated through ADR 0006's PdfFunction, and /Group parsing. Wired end
to end: parser -> PdfOp::SetExtGState -> device -> RenderCommand ->
Makepad renderer.
Compositing is NOT claimed. Backdrop blending needs render-to-texture,
which an engine-neutral crate has no framebuffer for. Constant alpha is
applied because it needs no backdrop; blend modes and soft masks are
reported through TransparencyError::Unsupported rather than dropped,
because a silently ignored /Multiply looks exactly like a correct
/Normal.
The ADR required a test enumerating every multi-character operator, on
the grounds that fixing the third instance of a shadowing bug (after cm
and rg) without preventing the fourth is not a fix. It immediately found
three more live bugs, none of them transparency-related:
- `b` and `b*` dropped their close-path, mapping to FillStroke* instead
of CloseFillStroke*, so every closed-and-stroked path drew with a gap.
- Text operators within three bytes of the end of a content stream were
mis-parsed: the b'T' arm guarded `*i + 3 < len` while reading only two
bytes, so a stream ending in `/F1 12 Tf` parsed as FillWinding.
And the transparency-group fixture found a fourth:
- PdfPage::xobjects was empty for every page of every document.
extract_xobjects called doc.resolve(), which follows the reference,
then asked the resolved object for as_ref() - always None - and only
accepted a bare dict when every XObject is a stream. No `Do` operator
could be resolved through the page model. Same defect as the one that
once destroyed annotation object references; it now has its own
regression test.
T* and BMC are genuinely unimplemented and are deliberately excluded
from the guard's table rather than papered over.
7 corpus fixtures, 12 acceptance tests, 32 unit tests asserting the
§11.3.5 formulas (not our own output), and a parse_ext_gstate fuzz
target.
TEST_TARGET=pdf 451 -> 497, TEST_TARGET=pdf-ui 495 -> 541.
rustfmt and clippy -D warnings clean.
Went looking for remaining defects before starting the viewport split and
found silent data loss in the two Save buttons.
cad_store::save_cad_script(&project.id, &source).ok();
self.view.label(..).set_text(cx, "Saved");
save_cad_state(&source, &solid).ok();
self.view.label(..).set_text(cx, "Saved with mesh");
Both discard a Result carrying a real io::Error and then report success
unconditionally. The user is told their work is safe when nothing was
written.
This is reachable, not theoretical. `cad_store::cad_projects_dir()` only
*logs* a `create_dir_all` failure and returns the path anyway, so the
following `fs::write` fails with a genuine error -- which was thrown
away. An unwritable data directory, a full disk or a permissions problem
all render as "Saved".
Save-As had a third silent failure on the same line of reasoning: if
`bake_parts_solid()` returned None the save never even ran, and it still
said "Saved with mesh".
Fixed via save_status_message(what, result), so the label is derived from
the outcome instead of assumed. Three distinct messages now: the caller's
success wording, "Save failed: <cause>" with the underlying error text
preserved, and a specific message for the no-mesh and no-active-project
cases, which were previously indistinguishable from success.
Also replaced the hand-rolled widget borrow in Save-As with
with_viewport, the helper added in 5.2.
Tests assert the two properties that matter: a failed save must not read
as success and must carry its cause through to the user, and a
successful save must use the caller's wording. Negative-tested.
CI gate added and verified in both directions: greps for a discarded
Result on a save/write/persist/store/export call. A dropped Result is
merely untidy in most places; on a save path it is data loss.
These were the only two instances -- the CAD module now has zero
discarded write Results.
685 lib + 154 integration, 0 failed. All seven gates pass.
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.
export_floor_plan_pdf, export_3d_viewer, export_stl and export_svg each
opened with a byte-identical 23-line block: borrow the main viewport,
bail if it is missing, bail if it has no parts, then take the cached
scene, the shared mesh cache and the part count. Verified identical with
a regex over all four -- the only difference was the label prefix
("PDF", "3D", "STL", "SVG"), and even that was consistent within each
copy.
Now one method, export_scene_source(cx, kind), returning Option of the
triple and setting the status label itself. Four call sites, each two
lines.
This is the same duplication shape that hid the last three real bugs in
this module: nine copies concealed the properties-panel no-op, one
uninspected copy concealed the dead Extend tool, and nine keymap arms
concealed the KeyCode bindings. Nobody re-reads the fourth copy of a
23-line block. There is no bug hiding in these four -- I checked -- but
the next edit to one of them would only have landed in one.
The user-visible strings are the behaviour here, so they are pinned
rather than assumed: ExportBlocked + export_blocked_message hold the two
messages, and export_prologue_tests asserts all eight exact strings the
copies produced. A second test asserts the two reasons stay
distinguishable -- "no viewport" is a wiring fault, "no parts" is the
user having drawn nothing, and reporting one as the other sends someone
to debug the wrong thing.
Negative-tested: making NoViewport render the NoParts text fails both.
bake_parts deliberately keeps its own prologue. It needs a different
tuple (solid + script, not scene + cache + count), so folding it in
would mean a helper with an unused half.
683 lib + 154 integration, 0 failed. All six CI gates pass.
Three defects found by auditing the ADR merge criteria against the code
rather than against memory.
1. The scheduled fuzz job ran five hardcoded targets. Four had been
added since and were never fuzzed: parse_revision_chain, decrypt,
eval_function and parse_colorspace. eval_function is the sharpest of
those - it executes PostScript taken verbatim from an untrusted file.
The list now comes from `cargo fuzz list` and the job fails rather
than passing vacuously if it comes back empty.
`cargo fuzz list` reads the manifest, so a target file added without
its [[bin]] entry would still be skipped silently. The engine job,
which runs on every push, now checks the two agree. Both directions
of that guard were exercised before committing.
2. ADR 0004 rule 3 promises an annotation whose appearance cannot be
generated "keeps its original /AP and is reported as skipped". The
keeping worked - to_dict clones the source dictionary - but nothing
reported it: SaveReport only tracked skipped appearances for form
fields. A caller who moved a stamp was never told its artwork still
showed the old position. Adds
SaveReport::annotation_appearances_skipped and
appearance_is_generated, which enumerates the out-of-scope types
explicitly so a new AnnotationType fails to compile until classified.
3. SetContents and SetFlags had no round-trip test. Both were
implemented and unit-tested against the in-memory model, but neither
was ever reparsed from written bytes - the assertion ADR 0004 calls
central.
New fixture annotations/stamp.pdf carries real /AP artwork for a Stamp
(undrawable: must be preserved and reported) beside a Square (drawable:
must not be reported), so the reporting cannot pass by reporting
everything. The stamp test was mutation-checked: it fails when the
reporting line is removed.
ADR 0003 and 0004 merge criteria are now ticked. 0003's were genuine
paperwork - every box traced to an existing named test. 0004's were not,
and its ADR now records what was missing rather than implying it always
worked.
TEST_TARGET=pdf 447 -> 451 passing. rustfmt and clippy -D warnings clean.
The orphaned doc comment at the end of the test module in style.rs
caused 'expected item after doc comment' error, which prevented the
entire style module from compiling. This made MapThemeStyle,
CompiledMapTheme, default_light_theme, and default_dark_theme
invisible to view.rs, causing 8 cascading compilation errors.
ProjectionLayoutTree::hit_test's nearest-glyph fallback resolved any
point to a glyph, so taps landing inside tables or advanced nodes never
reached the widget's table_hit_test/node_hit_test branches — production
cell taps silently moved the text caret instead of parking in cells.
The fallback now returns None when the point is inside a table or node
rect (those layers own their taps), while empty-space snapping beside
text still places the caret.
Close the "widget input, selection, mobile gestures" test gap with a
draw-free Area::Rect stub harness (stub_hit_area): tests seed the input
area a GPU frame normally fills and mirror the two platform
pre-dispatch steps (first_mouse_button, committed key focus), so raw
MouseDown/MouseMove and IME TextInput dispatch through event.hits
against a real Cx — exactly like a running app. Covered end-to-end:
hit-dispatch sanity, caret placement from a tap, shift-press selection
extension, drag selection over glyphs, TextInput inserting at the caret
and replacing an active selection, and a cell tap + TextInput whole-cell
round trip. The harness also exposed the test fixture actor drifting
from production's single "local" actor (cross-actor mid-run anchoring
is deterministic but not chronological — now a documented doc-engine
projection invariant); the runtime fixture now uses "local" for
production fidelity.
READMEs: doc runtime-tests section documents the stub harness and the
two bugs it unearthed; roadmap item updated (only the renderer draw
pass remains GPU/Studio-bound); doc-engine projection invariants gain
the per-actor counter boundary bullet.
6 new tests, 675 -> 681.
Reported four times, most recently with the detail that it appears at
runtime when tapping "Pay via M-Pesa" — not during a build.
That confirms the diagnosis rather than changing it. The reported string
was removed in 3e77bf5 and `git log -S` shows it in no source file today,
so a binary built from current main cannot print it. The reports are
coming from a binary built before that commit — most likely an installed
APK, which `git pull` does not update.
The real gap is that there was no way to tell those two builds apart from
the screen. A stale APK and a current one showed a message of the same
shape, so each report restarted the same investigation.
The message now ends with `[tracker-2]`. If the screen shows that marker,
the binary contains every fix to date and the message is the intended
product boundary. If it does not, the binary is stale and needs a rebuild
and reinstall.
Cheap to read aloud in a bug report, and it settles the question in one
round trip instead of four.
Makes table merge reachable on touch devices, closing the last gap of
the CRDT-native merge/split milestone (Shift+Arrow has no touch
equivalent):
- Long-press inside a table cell falls through the text glyph hit test
into table_hit_test and starts a TableCellSelection with anchor =
focus = the pressed cell (start_cell_range), clearing any text
selection; long-press on text keeps selecting word atoms unchanged.
- While the router idles in Selecting, the continued drag moves the
focus cell (extend_cell_range_to) through the tap hit geometry; drags
outside the anchor's table keep the focus and the caret tracks it.
Text word selections share the Selecting state but carry no cell
range, so their behavior is untouched.
- Lifting the finger keeps the range for the workspace Merge button or
Ctrl/Cmd+M (merge_selected_cells); Split already worked from a cell
tap. No new draw code — the existing cell-range highlight renders the
span.
Tests: 3 new runtime suites on a real Cx (long-press entry, drag
extension with caret tracking and full-grid normalization then merge
consumption on the wire, out-of-table drag keeping focus, and the text
word-selection regression guard) for 675 total in nigig-build.