678 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 01ebeeb7fe |
feat(makepad-table): real image rendering with a resize anchor, and per-row heights
Two things: attached images are decoded and drawn rather than shown as a placeholder chip, and row heights become genuinely per-row. **Image rendering.** Decoding follows `pageflipnav/src/utils.rs`, the Robrix-derived app in this repo: try PNG, then JPEG, because a header sniff is not reliable enough to choose on its own. The decoded texture is cached per cell, and a cache entry of `None` records a file that could not be read so a broken path is attempted once rather than every frame — `draw_walk` runs at 60Hz and re-decoding a photo there would be the slowest thing in the widget by a wide margin. One `Image` widget repositioned per cell, matching `cell_editor` and `math_cell`, with the texture swapped from the cache. A pool would let several textures live at once but needs runtime template instantiation and a reuse policy; this is the same number of GPU uploads with far less machinery. On first successful decode the real pixel size is written back to the attachment, so the row is sized from the true aspect ratio instead of the placeholder guess. **The resize anchor.** A grab square at the image's bottom-right corner. Dragging it writes `ImageSizing::Fixed`, which pins the height so a later relayout cannot overrule what the user chose, and the row follows because `row_height_for` reads the same value. The floor is asserted at compile time against the anchor size: an image dragged smaller than its own grab handle could not be grabbed again, and the user would have to delete the attachment to recover it. The anchor is hit-tested before the cell, or dragging it would open the editor instead. Sizing lives on the attachment rather than in widget state, so it survives a column reorder along with the image. **Per-row heights.** `TableRow::height` holds a dragged override and `row_height_for` honours it. Item 5's row resize previously assigned `self.row_height`, which is table-wide — dragging one row's handle resized every row at once. `RowGeometry`, added with the attachments, now carries the consequence: rows below a resized one shift down. Tests 86 -> 94 (140 across the tree), all five crates clippy-clean. Verified by reintroducing four defects. One of those guards did not work first time and the gap was mine. Deleting the per-row override branch from `row_height_for` left the whole suite green: the tests checked that `TableRow::height` could be *stored*, and nothing checked it was ever *read*. Storing a value no one consults is exactly the shape of "the handle does nothing". `the_row_override_is_actually_consulted` now asserts the connection at both ends — that `row_height_for` reads `row.height`, and that the resize writes it rather than the table-wide field. |
|||
| 3c751f18bd |
feat(makepad-table): cell attachments and per-row heights (item 6)
The last of the nine. Items 1-5, 7, 8 and 9 shipped in |
|||
| bbdfe823f8 |
feat(makepad-table): row gutter, header select/resize, long-press menus, one input model
Items 1, 2, 3, 4, 5, 7 and 9 of the nine reported. Item 8 shipped separately
in
|
|||
| 1887b54efa |
fix(makepad-table): repaint when the cell editor takes focus (item 8)
The editing cell's text did not appear until the pointer moved. `begin_edit` cannot focus the editor directly. `set_key_focus` takes an `Area`, and the editor only has one once it has been drawn, so focus is deferred: `begin_edit` sets `needs_editor_focus` and the next `handle_event` calls `set_key_focus` on the now-valid area. That deferred step changed how the editor draws — the caret starts blinking and the focused colour states apply — but it never asked for another frame. The editor therefore kept painting its unfocused appearance until something unrelated triggered a redraw. Moving the mouse was that something, which is why the text appeared only after moving the pointer away. One `self.redraw(cx)` after focus is taken. Tests 62 -> 63; verified by removing the call, which fails the new test. This is item 8 of nine reported together. The other eight are new capability — a row-number gutter, header editing, resize handles, a cell context menu with attachments, and a touch path — and are being scoped separately rather than bundled into a bug fix. |
|||
| 374af5ccad |
feat(pdf): the five Phase 6 bullets the status line omitted
ADR 0027. Asked whether Phase 6 was 100% complete, I checked the plan's bullets against the code instead of answering from the status line. Five were not implemented and the status line named none of them: detached/ATTACHED signatures /SubFilter hardcoded to adbe.pkcs7.detached PAdES basics ETSI.CAdES.detached was a string in a match external_signing_test.dart absent; SigningIdentity needs an in-memory key OCSP/CRL lookup CRL only; OCSP counted, never parsed Fulcio identity absent (optional in the plan) This is the second time. ADR 0021 recorded the same failure in Phase 4 and wrote the rule meant to prevent it — enumerate criteria from the plan text first, then mark each done or explicitly deferred. I wrote that rule and then produced another prose summary of what I had built. A summary written from the work cannot show what the work omitted. PAdES is a real profile, not a label. CAdES signs a set of signed attributes, one carrying the document digest, and the signature is over those attributes re-tagged as a SET (RFC 5652 5.4) rather than over the [0] IMPLICIT SEQUENCE they are carried in. Verification checks the messageDigest attribute against the document as well as verifying the attribute signature; without that, a signature over somebody else's digest would be accepted. /SubFilter now comes from the profile, so a document cannot claim CAdES while carrying plain PKCS#7. ExternalSigner is a trait: bytes in, signature out. A smartcard or KMS never hands out its key, so SigningIdentity could not represent one. SigningIdentity implements the trait rather than sitting beside it, so there is one signing path — a second path for hardware keys would be a second place the byte range could be computed differently. OCSP is decoded with the der crate already present rather than adding the ocsp crate for two fields. Revoked from any response beats Good from any other. Attached signatures are REFUSED, not deferred. Both attached profiles (adbe.pkcs7.sha1, adbe.x509.rsa_sha1) are SHA-1 based, and SHA-1 is broken for signatures. They are parsed so such documents can be read; they cannot be written, enforced by the absence of a SignatureProfile variant. Same decision as RC4 in ADR 0024. Recorded as refused rather than not-done, because "not done" invites someone to finish it. Four mutations, all killed first attempt: messageDigest not compared, CAdES verified against the wrong bytes, /SubFilter hardcoded again, OCSP revoked read as good. The status line is now the plan's own bullets in a table, one row per spec item, not prose. Two wrong status lines in the same direction is a pattern, and the fix is structural: a missing row is visible, a missing sentence is not. Four rows are left unticked — Fulcio, independent review, Acrobat interoperability, and signing a document that already has an AcroForm. qpdf accepts documents under both profiles. pdf: 1289 passed (was 1276). Coverage 87.96%. |
|||
| 780e323674 |
refactor(spreadsheet-ui): headless formula-bar state machine, wired to the workspace
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (push) Has been cancelled
spreadsheet / engine-coverage (push) Has been cancelled
spreadsheet / ui-controller-coverage (push) Has been cancelled
The formula bar's state — formula text, target cell, dirty flag, and the "ignore the next change" latch — lived as five scattered fields plus inline logic inside workspace.rs, a `script_mod!` DSL file that no unit test can construct. That made the formula bar the one piece of the UI whose behaviour was untestable headlessly. Extract a `formula_bar` controller module (the same pattern as the earlier geometry.rs extraction) and wire the workspace to it: - `FormulaBar` owns text/target/dirty/latch with `sync_to_cell`, `on_user_input`, `insert_reference`, `take_commit`, and `sync_after_commit`. The workspace now only bridges it to the Makepad text input; the change/return/focus-loss/undo-redo handlers route through the controller unchanged in behaviour. - Point-and-click formula building (new, Excel-style): while the formula bar is focused, selecting a cell appends its reference instead of replacing the formula being edited. `reference_for(sheet, row, col)` and `quote_sheet_name` build `A1`, `Sheet2!A1`, or `'My Sheet'!A1` with Excel's quoting rules (bare identifier unquoted; spaces, punctuation or a leading digit quoted; embedded quotes doubled) — the machinery that makes the cross-sheet reference syntax buildable from the UI. - The coverage script now measures formula_bar.rs alongside the other controller modules. Tests: 11 new headless unit tests (quoting, reference construction, sync/latch/commit round trip, and insertion into empty/value/formula bars). UI lib tests 62 -> 73, all pass; ui-controllers coverage 98.74% (floor 96) with formula_bar.rs at ~98%. |
|||
| d0e7ede0c1 |
feat(spreadsheet): cross-sheet named ranges (Data!Total)
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
spreadsheet / engine-coverage (push) Has been cancelled
spreadsheet / ui-controller-coverage (push) Has been cancelled
Completes the cross-sheet reference feature with `Sheet!Name`, the one
shape the previous tranche explicitly left out.
Parser and AST:
- `Sheet2!Total` and `'My Sheet'!Sales` parse as a new
`Expr::SheetNamedRange { sheet, name }` node. In
`parse_sheet_qualified_ref`, an identifier after `!` that is not a
valid cell reference is treated as a named range; a cell reference
wins when both readings are possible (`Sheet2!A1`), matching Excel.
Evaluation:
- `EvalContext` gains `get_sheet_named_range` (default `None`).
`evaluate` resolves a `SheetNamedRange` to its range and returns the
first cell in expression context, while `resolve_arg` expands it to
every cell for aggregates — so `Data!Total` reads one cell and
`SUM(Data!Total)` sums the whole range. An unknown name on a real
sheet is `#NAME?`, and `DataEvalContext` looks the range up on the
sibling sheet case-insensitively (or on the sheet itself, through
the intra-sheet path).
Dependencies:
- A named-range reference is a coarse sheet-level dependency, because
its bounds live on the target sheet. `SpreadsheetData` tracks
`cross_sheet_named_refs` (cell -> sheet names), rebuilt by
`rebuild_dependency_graphs` / `update_dependency_graph` alongside the
cell-level `cross_sheet_refs`, and the workbook folds both into its
sheet-index dependency map. Editing a cell inside the named range
therefore recalculates the reading sheet.
Tests: parser/evaluator units (incl. quoted names, cell-vs-named
precedence, dependency extraction) and workbook end-to-end (SUM over a
cross-sheet named range with propagation on edit, quoted names, and the
#NAME? case). Engine unit tests 390 -> 395; UI lib tests still pass.
Engine coverage 97.56% (floor 96).
|
|||
| 593cd8fee8 |
feat(spreadsheet): cross-sheet references (Sheet2!A1)
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
spreadsheet / engine-coverage (push) Has been cancelled
spreadsheet / ui-controller-coverage (push) Has been cancelled
The engine's dependency graph is per-sheet by design, so this was the
structural change: a formula can now read a cell on another sheet, and
edits to the source sheet recalculate the readers.
Parser and AST:
- New tokens: `Bang` (`!`) and `SheetName` (`'My Sheet'`, with `''` as
an escaped quote). `!=` still tokenizes as not-equal.
- New AST nodes: `SheetCellRef { sheet, cell }` and
`SheetRange { sheet, range }`, parsed from `Sheet2!A1`,
`Sheet2!A1:B2`, `'My Sheet'!B3`, before the cell-ref fallback (a bare
`Sheet2` would otherwise parse as a bogus cell reference).
- `evaluate` and `resolve_arg` resolve them through two new `EvalContext`
hooks, `get_sheet_cell_value` / `get_sheet_range_values`, which default
to `#REF!` in contexts without sibling sheets.
Evaluation:
- `DataEvalContext` gains a sibling-sheet view plus a shared cross-sheet
cycle guard keyed by `(sheet, row, col)`. Reading a sibling routes to a
child context pointed at that sibling; re-entering the same cell on the
same sheet mid-evaluation reports `#CYCLE!`, and a reference back to the
sheet being recalculated is caught through the guard.
- `recalculate_all_with` / `evaluate_formula_with` accept the sibling
view; the plain per-sheet entry points are unchanged.
Recalculation and dependencies:
- `SpreadsheetData` tracks `cross_sheet_refs` per cell (rebuilt by
`rebuild_dependency_graphs` / `update_dependency_graph`), and the
workbook flattens it into sheet-index edges. Editing a sheet
recalculates — with sibling access — the transitive closure of sheets
that read it, plus the sheet itself (its own formulas may read other
sheets). Single-sheet edits with no cross-sheet references keep the
fast incremental path.
- `evaluate_all` clears computed values across sheets first, then runs
one extra pass per sheet, so acyclic chains propagate and mutual
cross-sheet cycles terminate with `#CYCLE!`.
- Cross-sheet formulas survive save/load: deserialization runs a
sibling-aware pass, and a missing sheet renders `#REF!`.
Tests: parser/evaluator units (incl. quoted names, dependency
extraction) and workbook end-to-end (read + propagation, a three-sheet
chain, quoted names, missing sheet, save/load round trip, cycle
termination). Engine unit tests 382 -> 390; UI lib tests still pass.
Engine coverage 97.55% (floor 96).
|
|||
| 99aebc202a |
fix(pdf): security review of the signing code — a forgery verified as valid
ADR 0026. Both ADR 0024 and ADR 0025 said this code needed a security review before shipping. This is that review, done adversarially: for each way a signature could be defeated, a test that attempts it. It found a critical vulnerability in the code as shipped last turn. FINDING 1, critical, exploitable with no special access. Verification recovered the certificate and the signature by *scanning* the blob for DER-shaped bytes rather than decoding it. The signature was checked against certificates[0]; trust was checked against ANY certificate present. Two questions, two different certificates. So: the attacker signs a forgery with their own key the attacker appends the victim's trusted certificate to the blob signature_valid = true (their signature over their own content is real) chain_trusted = true (the victim's certificate is present) is_valid() = true Demonstrated before the fix, with the message "I hereby transfer everything to the attacker" verifying as valid. Fixed by decoding the ContentInfo/SignedData structure and finding the certificate the SignerInfo actually names, by issuer AND serial, then evaluating both the signature and the trust path against that one certificate. Trailing data now fails the decode instead of being ignored. The scanning functions are deleted, not left unused: dead code that once returned the wrong answer is an invitation to call it again. FINDING 2, moderate. signer_certificate() returned chain[0] unconditionally, so a chain whose first entry was not the signing key's certificate made the SignerInfo name the wrong one. Not a forgery route — the signature fails — but a UI showing "signed by <somebody trustworthy>" beside a failed check is its own kind of dangerous. Now it finds the entry whose public key matches the key doing the signing. FINDING 3, informational. digest_matches was hardcoded true under a comment claiming it was computed. Not exploitable, because is_valid() also requires signature_valid and the signature covers the bytes — but a field asserting an unperformed check is ADR 0017's pattern exactly. The four items ADR 0025 left unticked are closed: PKIX chain building, with each link's issuer signature verified. A name match alone is not a chain; anyone can put any name in a certificate. Pinning still short-circuits first. Stapled revocation from /DSS, offline only. Unknown is the default and a first-class answer: treating "no information" as "not revoked" is a claim a verifier cannot support. Signature appearances, with the claimed time labelled "Time claimed" because a self-declared /M carries no authority. One-call sign_document. Three things were wrong first: the /ByteRange placeholder was too narrow for real offsets so patching them moved every later byte; /Contents must be a hex string because a literal full of NULs needs escaping and changes length; and a signature dictionary nothing points at is invisible — the first version wrote one and the reader reported zero signatures over a correctly signed document. Four mutations, all killed — two only after strengthening the tests. My first smuggling test put the attacker's certificate first, where certificates[0] finds it anyway, so it passed with or without the issuer/serial match. Putting the TRUSTED certificate first is what distinguishes them, and writing that test is what exposed Finding 2. qpdf --check accepts the signed documents. pdf: 1276 passed. Coverage 87.98%. Left unticked, deliberately: an independent review by someone who did not write the code. This is a self-review; it found two real vulnerabilities, which is evidence the method works and not evidence that nothing remains. Also untested against Acrobat, which is stricter than the spec, and sign_document replaces rather than merges an existing AcroForm. |
|||
| 2d7b48b72c |
feat(spreadsheet): lookup functions — MATCH, INDEX, VLOOKUP, HLOOKUP, XLOOKUP
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
The formula engine could compute over ranges but could not look a value up in one. This adds the Excel lookup family, plus the #N/A error they need to report "not found". New `FormulaError::Na`: - Renders as `#N/A`, parses back in `from_display`, and — a bonus fix — the recalc fast path now propagates a stored `#N/A` as an error instead of turning it into text. Functions (5): - MATCH(lookup_value, lookup_array, [match_type]) — 1-based position. Type 0 exact (case-insensitive text), 1 largest ≤ lookup, -1 smallest ≥ lookup. - INDEX(array, row_num, [col_num]) — cell at a 1-based position; a single index walks the array flat in row-major order. Out of range is #REF!. - VLOOKUP / HLOOKUP — exact or approximate (approximate takes the largest first-column/first-row value ≤ lookup, the sorted-table convention), col/row index out of range is #REF!. - XLOOKUP(lookup_value, lookup_array, return_array, [if_not_found], [match_mode], [search_mode]) — match modes 0 exact, -1 next-smaller, 1 next-larger, 2 wildcard (`*`/`?`, case-insensitive); search modes 1 first-to-last and -1 last-to-first; the `if_not_found` fallback is evaluated lazily, only when nothing matches. Binary search modes are rejected with a clear error rather than silently mishandled. Lookups index ranges by position, so they keep their arguments as AST nodes (a new `range_from_arg` resolves Range and NamedRange expressions) instead of flattening through the aggregate path. Tests: 9 new unit tests (exact/approximate/wildcard/search-direction/ error arms) + an end-to-end test proving the dependency graph tracks the lookup table and recalculates dependents when a table cell is edited. Engine unit tests 373 -> 382. |
|||
| 383533da36 |
feat(email): email the trip report to the finance department
build_report_email (pure, tested) attaches the report PDF as application/pdf in a multipart message to a comma-separated finance recipient list. spawn_email_trip_report fetches the inbox, extracts trip receipts, builds the report, and emails it via the signed-in SMTP account; the proxy backend reports 'attachments unsupported' honestly rather than failing silently. The Finance card gains a recipients field and an 'Email report to finance' button. An end-to-end test sends the attached PDF through the SMTP sink and asserts the recipient, application/pdf type and filename land in DATA. Domain tests 234 -> 237. |
|||
| 26f6d431fb |
feat(spreadsheet): date and time functions, with a date display format
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
The formula engine had no notion of dates. This adds an Excel-style date/time model and the functions to work with it. Date model (new `dates` module): - A date/time is a single f64: the integer part is a day serial, the fraction is the time of day. Serial 25569 = 1970-01-01, so every modern date agrees with Excel exactly. The calendar is the proleptic Gregorian (Howard Hinnant's days_from_civil/civil_from_days), so Excel's phantom 1900-02-29 (serial 60) reads back as 1900-02-28 and serial 61 = 1900-03-01 — documented rather than reproduced. - Serial <-> calendar conversion, day-of-week, leap-year and days-in-month helpers, date/time string parsing, EDATE/EOMONTH, DATEDIF(Y/M/D), and ISO formatting. Functions (17): - DATE, DATEVALUE, YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, TIME, TIMEVALUE, WEEKDAY (return types 1/2/3), DAYS, EDATE, EOMONTH, DATEDIF — with Excel's month/day rollover in DATE, day clamping in EDATE, and #VALUE! for malformed strings and unknown units. - TODAY and NOW read a wall clock. The engine stays deterministic: `EvalContext` gains a default-none `now_serial` hook, and `SpreadsheetData` carries an optional `now_serial` (never serialized). Without a clock they report "requires a wall clock"; `SpreadsheetData::set_system_now` / `Workbook::set_system_now` supply the system clock, and the UI workspace model wires it on creation. Display: - `NumberFormat::Date` and `NumberFormat::DateTime` render a serial as `YYYY-MM-DD` / `YYYY-MM-DD HH:MM:SS` through write_display_value, with codes that round-trip through the existing serialization. Tests: 18 new unit tests (calendar round-trips across centuries, known serials, leap-year rules, parsing, weekday schemes, EDATE/EOMONTH clamping, DATEDIF, ISO formatting) + 6 evaluator tests + an end-to-end test covering format rendering and recalc through the dependency graph. Engine unit tests 352 -> 373. UI lib tests still 62 pass with the new clock wiring. |
|||
| 9a5ce9c0e6 |
feat(pdf): signing and verification — Valid becomes reachable, with a policy
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
email / gates (push) Has been cancelled
email / email-domain (push) Has been cancelled
email / nigig-email (push) Has been cancelled
email / supply-chain (push) Has been cancelled
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (push) Has been cancelled
doc-engine / engine (push) Has been cancelled
doc-engine / coverage (push) Has been cancelled
doc-engine / consumer (push) Has been cancelled
nigig-map / test (push) Has been cancelled
sms / gates (push) Has been cancelled
sms / robius-sms (push) Has been cancelled
sms / android (push) Has been cancelled
sms / nigig-sms (push) Has been cancelled
sms / supply-chain (push) Has been cancelled
ADR 0025, completing Phase 6's functional core. This partially reverses ADR 0010, which refused signing and cryptographic verification outright, and it reverses only the half whose justification expired. ADR 0010 gave two reasons. The first — a parsing library has no business signing — stopped being true when Phase 4 began creating documents and Phase 5 editing them. The second is still true and is preserved intact: deciding which certificate authorities to trust is a policy decision that belongs to the host, not to a parsing library So VerificationStatus::Valid is still not reachable by default. Verification returns three independent booleans and is_valid() needs all three; the third, chain_trusted, can only become true through a caller-supplied TrustAnchors. There is no TrustAnchors::system(), no bundled root store, no Default that trusts anything. A caller with no policy is told "cryptographically intact, signed by somebody you have not said you trust" — a different fact from "forged", and a host that cannot tell them apart shows the wrong thing to a user. RSA PKCS#1 v1.5, ECDSA P-256 and Ed25519, all with SHA-256. PSS is stronger and not universally accepted by PDF verifiers, so v1.5 is what is written. Ed25519 carries an interoperability caveat in the doc comment on the variant itself, because that is where someone choosing it will read it: ISO 32000-2 does not list it and most desktop viewers will reject it. No network. Revocation is not implemented rather than smuggled in: the engine crates are CI-gated against reaching outward, and that gate is a rule about layering, not an obstacle to work around. Every test generates a real key and a real certificate at run time. Nothing asserts against a checked-in blob — a fixed expectation only proves the code still does what it did, which is the wrong question for a signature. The tampering tests assert the signature verifies FIRST, then flip a bit; without that half they could pass by never verifying anything. Four mutations, all killed. The one that matters is the first: making an empty anchor set confer trust is exactly the regression that would turn this back into the thing ADR 0010 refused, and it fails immediately. Two bugs the tests found: UTCTime cannot encode a year past 2049 (RFC 5280 4.1.2.5.1). The first fixture used a 2096 expiry and every certificate failed to encode. The certificate scanner assumed a two-byte DER length. RSA certificates are large enough to use that form, so RSA and P-256 passed while Ed25519 found no certificate at all — its certificate is small enough for the short form. A scanner tested only against the largest input fails silently on the smallest. 72 dependency packages pulled in, zero non-compliant licences, no C. Stated plainly and left unticked in the ADR: chain_trusted is anchor identity matching, not PKIX path building. Correct for certificate pinning, a false negative for a real CA hierarchy. Also outstanding: revocation, signature appearance generation, and one-call incremental signing. pdf: 1247 passed. Coverage 88.08%, floors met. |
|||
| b5ff5dcf40 |
feat(spreadsheet): text manipulation and IS* info functions
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
The formula engine could count, sum, compare and test conditions, but had almost no way to work with text (only CONCAT/LEN/UPPER/LOWER) and no way to ask about a value's type. This adds both. Text functions (operate on the display form of their arguments): - TRIM — collapse internal space runs and drop leading/trailing spaces - LEFT / RIGHT — first/last n characters (default 1) - MID — n characters from a 1-based start - SUBSTITUTE — replace all occurrences, or just the nth instance - FIND / SEARCH — 1-based position, case-sensitive vs case-insensitive, #VALUE! when not found or start is out of range - REPT — repeat n times, capped at Excel's 32767-character result - PROPER — title-case each word Info functions (never propagate their argument's error, like Excel — ISERROR reports it, the others treat it as FALSE): - ISNUMBER / ISTEXT / ISNONTEXT / ISLOGICAL / ISERROR - ISBLANK — TRUE only for an absent cell. This needs the distinction between "missing" and "holds 0", so EvalContext gains a `cell_is_blank` method; DataEvalContext overrides it with a direct cells lookup (an absent cell reads as Number(0.0) through get_cell_value, but is blank, whereas a real 0 is not). Dependencies flow through the existing AST walk, so a TRIM/LEFT/etc. reference is tracked and recalculates when its source cell is edited — pinned by an end-to-end test through SpreadsheetData. Engine unit tests 345 -> 352. |
|||
| b93dc485b9 |
test(spreadsheet): cover public workbook serialization round trip
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
|
|||
| b7eacb2a94 |
test(spreadsheet): cover public multi-sheet evaluation
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
|
|||
| 4deefabd0a |
test(spreadsheet): cover public style commands
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
|
|||
| a099ab9980 |
test(spreadsheet): cover public workbook lifecycle API
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
|
|||
| 2824b49f0e |
test(spreadsheet): cover public workbook deserialization API
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
|
|||
| 1629994de7 |
test(spreadsheet): add public persistence integration test
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
|
|||
| d4e3e9a443 |
feat(pdf): encryption on save — AES-128 and AES-256 (Phase 6, part one)
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
doc-engine / engine (push) Successful in 21s
doc-engine / coverage (push) Successful in 31s
doc-engine / consumer (push) Failing after 16m57s
email / gates (push) Has been cancelled
email / email-domain (push) Has been cancelled
email / nigig-email (push) Has been cancelled
email / supply-chain (push) Has been cancelled
nigig-map / test (push) Has been cancelled
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (push) Has been cancelled
sms / gates (push) Has been cancelled
sms / robius-sms (push) Has been cancelled
sms / android (push) Has been cancelled
sms / nigig-sms (push) Has been cancelled
sms / supply-chain (push) Has been cancelled
ADR 0024. This reverses ADR 0005's "never write encryption", and the reason it is safe to reverse is that the facts changed underneath it. A crate that only reads cannot produce weak ciphertext, so refusing to write any was free. Now that Phase 4 creates documents and Phase 5 edits them, the refusal does something worse than protect nobody: open a password-protected file, change one annotation, save, and the output is plaintext. No error, no warning — the protection is silently dropped. That is this project's recurring failure mode in the one place where the consequence is a breach. The principle survives in a narrower form: no hand-rolled crypto, and no weak cipher offered as an option. RC4 stays readable because files use it and is not writable — EncryptionAlgorithm has no RC4 variant, so the refusal is a type, not a runtime check someone can route around. The encryptor is the literal inverse of the decryptor and imports its primitives rather than restating them; two implementations of one algorithm drift, and here they drift towards "decrypts to garbage". Every unit test round-trips through the existing Decryptor. Encryption sits at one choke point: PdfWriter holds the Encryptor and write_object_at encrypts everything passing through. Not per call site — there are twenty-two of those in PdfDocBuilder, and one stream written in the clear inside an encrypted document is not a partial failure, it is a leak that no reader will report because the file is otherwise valid. The /Encrypt dictionary is the single deliberate exemption: it holds the salts a reader needs before it has a key, so encrypting it bricks the file. Verified against implementations we share no code with, now gated in CI: ok qpdf opens it with the password ok it really is AES-256 ok the wrong password is refused ok poppler decrypts the content ok no plaintext in the encrypted file Four mutations, all killed — two only after the tests were strengthened, and both misses are the interesting part: A fixed IV survived two_saves_of_one_document_are_not_byte_identical, because the AES-256 file key is fresh per save and that alone makes the output differ. The property actually needed is narrower: one encryptor, identical plaintext, different bytes. In CBC a repeated IV under one key leaks that two plaintexts are equal. A wrong /Length survived because our own reader recovers by scanning for endstream — a robustness fix from ADR 0023. An independent reader that trusts /Length reads a truncated stream and decrypts garbage. A lenient reader hides a broken writer, which is why the external gate exists. The /Length test itself had a bug first: it searched a from_utf8_lossy view and reported a stream declaring 80 bytes holding 156. Ciphertext is not UTF-8; the replacement characters shifted every offset. Unencrypted output stays byte-reproducible; encrypted output cannot be, and a test asserts that loss rather than leaving it implicit. pdf: 1220 passed (was 1187). pdf-ui: green. Coverage 88.21%, encrypt_write.rs at 96.5%. Signing is NOT started. It needs the trust-anchor decision ADR 0010 deferred: VerificationStatus::Valid is unreachable by construction, and making sign -> verify pass is a policy change, not an implementation detail. The plan's Phase 6 status now says so. |
|||
| 118fbefe9a |
test(spreadsheet): move format detection test to integration suite
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
|
|||
|
|
ee8d10d978 |
fix(pay): the M-Pesa PIN field has been visible by default since 69f6fb2
The first thing the newly registered runner found. tools/check-no-pin-capture.sh
fails on main:
FAIL: pin_input is not hidden by default in the DSL
(review item 0.3: hiding must survive a DSL reload)
Reproduced locally, so this is a real defect and not runner flakiness.
|
||
| 676b47087a |
refactor(spreadsheet-ui): centralize dirty region state
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
|
|||
| f75c1cc966 |
feat(spreadsheet-ui): model dirty render regions
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
|
|||
| 2ba1837055 |
fix(pdf): main was red — three clippy errors broke the engine build
The PDF engine did not compile under `-D warnings`, which is what CI's
engine job runs, so that job could not have passed on any of the last
eleven PDF commits. The tests themselves were fine (1187 passing); the
build was not.
Two lints in the new jpx.rs, one in tests/filters.rs. One root cause each:
jpx.rs:1279,1290 needless_range_loop on the inverse component
transform. The lint's suggestion does not work here:
each pass reads and writes three component planes at
the same index, and `components.iter_mut()` cannot
express three simultaneous mutable borrows of one Vec.
Allowed locally with the reason written down, rather
than restructuring correct code to satisfy a lint that
has misread it.
filters.rs:383 vec_init_then_push, where the lint is simply right.
No behaviour change. Verified after the fix, on a clean checkout of
|
|||
| ee81d292ed |
test(spreadsheet): cover literal and comparison formula edges
Some checks failed
email.yml / test(spreadsheet): cover literal and comparison formula edges (push) Failing after 0s
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
|
|||
| 0fcb2d3fae |
test(spreadsheet): cover sum boolean text error branches
Some checks failed
email.yml / test(spreadsheet): cover sum boolean text error branches (push) Failing after 0s
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
|
|||
| dffa140123 |
fix(makepad-table): measure text with the layouter instead of estimating it
Some checks failed
email.yml / fix(makepad-table): measure text with the layouter instead of estimating it (push) Failing after 0s
repo hygiene / hygiene (push) Has been cancelled
makepad-table / model (push) Successful in 20s
makepad-table / widget (push) Successful in 1m41s
makepad-table / hygiene (push) Successful in 6s
Right-aligned text still overflowed its column after the previous fix, because that fix kept the 7px-per-character estimate and only clamped the result. Real glyphs at this size average wider than 7px, so the estimate came out short, `pos.x + width - pad - estimated_width` placed the run too far right, and it ran past the cell edge. The clamp only guards the left side. No fixed per-character figure can work here: under-measuring overflows and over-measuring leaves a visible gap. `draw_cells` now measures with the same layouter that draws the run — `DrawText::layout(..).size_in_lpxs.width`, the pattern `glass_panel.rs` uses — and both the truncation and the alignment use that measurement, so the two cannot disagree. `align_text_x` takes a width rather than a string. `fit_text_to_cell` becomes `fit_text_measured`, taking a measuring closure and bisecting for the longest prefix that fits; laying out a string is not free, and a long value in a wide column would otherwise be measured once per character every frame. Tests 59 -> 62, and the existing ones were rewritten around an injected measurer. `varied()` gives different characters different widths, as a real font does, so the suite now fails on anything that assumes a constant width. Verified against four defects. Three of the guards did not work on the first attempt, and all three failures were mine: - **Estimating the alignment width — the actual reported bug — passed.** The source check asserted `size_in_lpxs` appeared *somewhere* in `draw_cells`, and the fitting call still mentioned it after the aligning call had been replaced with an estimate. It now counts both measurements and rejects any `chars().count() as f64 *` in the draw path. - **Removing the clamp passed.** Every test reached `align_text_x` through `fit_text_measured`, and once text has been shortened to fit, the clamp never fires. `alignment_clamps_a_run_wider_than_its_cell` calls it directly with an over-wide value. The clamp still earns its place: the fitted width and the aligned width are separate measurements of separate strings, and any disagreement is what it catches. - **An off-by-one in the bisection hung instead of failing.** `lo = mid - 1` stops the interval shrinking and the loop spins forever — a frozen frame, not a wrong pixel, and no assertion catches it without a timeout. The loop is now a bounded `for` capped at the number of steps a correct bisection can need, so the same mistake produces a wrong answer that a test can see. `the_fit_search_terminates_on_hostile_input` covers degenerate measurers. |
|||
| b870dc4c69 |
feat(pdf): outline, page label and struct-tree editing — Phase 5 complete
Some checks failed
email.yml / feat(pdf): outline, page label and struct-tree editing — Phase 5 complete (push) Failing after 0s
repo hygiene / hygiene (push) Has been cancelled
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (push) Has been cancelled
The last functional item. `catalog.rs` read all three; nothing could write them into an existing document. `PdfDocBuilder` can emit an outline when *creating* a file, but a document already on disk could not have its bookmarks changed. An outline is a doubly-linked tree — `/First`, `/Last`, `/Next`, `/Prev`, `/Parent` and a signed `/Count` — and every pointer has to agree. A viewer walking `/Next` and one walking `/First`..`/Last` must see the same list, or bookmarks vanish in one reader and not another with no error anywhere. Object numbers are reserved before any dictionary is built, because each item names its parent, its siblings and its children. `/Count` is signed and that matters: positive means open and counts *visible* descendants, negative means closed. A closed child contributes itself but hides its own children. Writing the total unconditionally makes every node render expanded. Page labels are a number tree, so the keys are sorted before writing and two rules starting on the same page are refused — that page's label would be undefined, and picking one arbitrarily is worse than saying so. Struct-tree editing is deliberately **removal only**. Editing the tree in place means rewriting `/K` arrays whose entries are marked-content ids inside page content streams; the tree and the content must stay in step, and changing one without the other produces a document whose accessibility information describes content that is no longer there. Removal is honest — the document stops claiming to be tagged — and `/MarkInfo` goes with it, because `/Marked true` with no tree tells a screen reader there is structure to find. Verified by mutation, seven defects, all caught: /Prev never written 1 fail /Next never written 5 fail /Count always positive 1 fail page validation skipped 2 fail /MarkInfo left behind 1 fail children not linked via /First 2 fail label rules not sorted 1 fail The last one needed a new test. Our reader walks `/Nums` linearly, so it tolerates any order and the round-trip passed unsorted — but a conforming reader binary-searches it and would label pages arbitrarily. Only reading the raw array catches that, which is the same lesson as the stale `/Count` in the page-ops tranche: our parser's tolerance hides defects that harm other readers. Engine suite 1158 -> 1187. External readers still pass. **Phase 5 is complete** but for the `ui.rs` interaction tests, blocked on the same missing Makepad headless backend as Phase 4's. The plan records the item-by-item status and, separately, the six defects the round-trip tests found in code that already existed — an unordered dictionary writer that made every generated PDF differ run to run, a short /Length that silently truncated streams, two readers disagreeing by a byte, a nested paren that truncated a string and desynchronised the stream, undecoded # escapes in names, and unknown operators being dropped outright. |
|||
| 41c43df0e5 |
feat(pdf): redaction and object compaction — and three reader defects
Some checks failed
email.yml / feat(pdf): redaction and object compaction — and three reader defects (push) Failing after 0s
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
Phase 5's last two functional items. Together they are what makes a redaction real, which is why they are one commit: redaction removes the content, compaction removes the revision that still holds it. **Redaction removes operators; it does not draw rectangles.** The famous failure is painting black over text and shipping it — the text is still there, and `pdftotext` prints it. This module draws nothing. It removes the text-showing operators whose position falls inside a rectangle, and the test that matters is that the text can no longer be extracted. Positioning needs the text matrix, so the module tracks `Tm`/`Td`/`TD`/`T*` and the CTM through `q`/`Q`/`cm`. It cannot reach the graphics layer — the crate boundary again — so it treats a showing operator's origin as its position and removes the whole run. That is coarse in the *safe* direction: removing more than asked loses content the user can see is missing; removing less leaves the secret in the file. What it refuses to claim is as important. Images are removed entirely rather than cropped. Metadata and attachments are untouched. And an incremental redaction leaves the original text in the earlier revision — the report says so via `earlier_revisions_retain_content` rather than implying the job is done. **Compaction finishes it.** The output is built from the object graph reachable from `/Root`, so dead objects, superseded revisions and the bytes behind a redaction are not copied — they are simply never written. A signed document is refused unless `allow_signed` is set, because compaction destroys the revision a signature covers and would leave every signature unverifiable with no warning. The end-to-end test is the point: redact, compact, then search the output bytes for the secret. It is gone. **Three reader defects, all found by writing the tests.** - **`PdfWriter` wrote dictionary keys unordered.** `PdfDict` is a HashMap and Rust seeds its hasher per process, so *every generated PDF differed run to run*. Found by compaction's idempotence test — compacting an already-compact file produced the same objects at the same offsets with their keys shuffled. Verified fixed by running four separate processes and getting a byte-identical file. Same defect as the one fixed in `content_edit::write_dict`; this one affected every file this codebase has ever written. - **A short `/Length` silently truncated a stream.** The reader guarded against a `/Length` running past the buffer but trusted one that was too small, cutting the stream at the wrong place and losing the rest with no error. Short lengths are common in hand-edited files. `endstream` is now the authority when the two disagree — but only when it is *further* on, so binary data containing the word `endstream` is still bounded by its declared length. - **Two stream readers disagreed by one byte.** `read_object_at` did not trim the EOL before `endstream` while `find_endstream` did, so a write-read-write cycle grew every stream by a newline. A test fixture had encoded the bug: it declared `/Length 9` for eight bytes of content and asserted the newline came back as data. Both corrected — the newline is syntax (§7.3.8.1), not content. Verified by mutation. Ten defects across the two modules, all caught: redaction covers instead of removes 16 fail CTM ignored 1 fail Q does not restore the CTM 1 fail operands kept when operator removed 12 fail revision warning always false 1 fail signature guard removed 1 fail reachability keeps everything 2 fail dropped reference left dangling 1 fail unresolvable object kept as reachable 1 fail writer dictionary order unsorted 1 fail short-/Length fix reverted 1 fail /Length not rewritten on compaction 1 fail One mutation survived and deleted code rather than adding a test: a `continue` skipping `/Length` in the compaction loop was dead, because the `set` after the loop overwrites it either way. Removed rather than left as untested defence with a reassuring comment — the same call ADR 0017 made about the visited-set guard. A second mutation moved a test rather than a fixture: a stale `/Length` can no longer reach `renumber` through a file, because the reader now repairs it first, so that branch is tested directly instead. Engine suite 1108 -> 1158. External readers still pass. Phase 5 remaining: outline, page label and struct-tree editing. |
|||
| b7eb271a0d |
feat(spreadsheet): logical and conditional formula functions
Some checks failed
email.yml / feat(spreadsheet): logical and conditional formula functions (push) Failing after 0s
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
The formula engine could sum, average and do single-cell math, but had no way to combine conditions or aggregate selectively — no `=IF(AND(...))`, no `=SUMIF(...)`. This closes that gap with seven Excel-compatible functions. Logical (evaluated lazily, like IF, so they short-circuit): - `AND(a, b, ...)` — TRUE iff every argument coerces to TRUE; ranges flatten to their cells. Stops at the first FALSE, so `AND(FALSE, 1/0)` is FALSE rather than `#DIV/0!`. - `OR(a, b, ...)` — TRUE iff any argument is TRUE, short-circuiting on the first TRUE. - `NOT(x)` — logical negation of the single argument. - `IFERROR(value, fallback)` — `value` unless it errors, in which case the fallback is evaluated and returned (lazily). Conditional aggregates (criteria-matched by position): - `COUNTIF(range, criteria)` — count of matching cells. - `SUMIF(range, criteria[, sum_range])` — sum of `sum_range` (or `range`) cells whose position matches; text in the summed region is skipped, errors propagate. - `AVERAGEIF(range, criteria[, average_range])` — mean of the matched cells, `#DIV/0!` when nothing matches. Criteria accept numbers, comparison operators (`>5`, `>=5`, `<5`, `<=5`, `<>5`, `=5`), case-insensitive text (`"apple"`, `=apple`, `<>apple`), and cell references holding any of those. Wildcards are not supported. Dependencies flow through the existing AST walk, so a SUMIF's range and criteria cell are tracked by the dependency graph and the formula recalculates when an input is edited — pinned by an end-to-end test through SpreadsheetData. Engine unit tests 331 -> 343. |
|||
| e1d1346b27 |
fix(mpesa): parse SMS from in-memory messages, not serde-skipped cache
OfflineSmsMessage.body has #[serde(skip)] so it round-trips as empty. scan_sms was upserting to the offline store then loading back and parsing from the empty body, which always fails. Parse directly from the in-memory list_messages() result instead. |
|||
| 70bbff7ecb |
fix(cad): the nav pad's zoom buttons were a pixel from their own hit zone
Some checks failed
email.yml / fix(cad): the nav pad's zoom buttons were a pixel from their own hit zone (push) Failing after 0s
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
First test written against the widget layer, and it found something on the way in. The viewport's navigation pad — fit, four pans, zoom in, zoom out — had its layout written twice. `viewport_render.rs` drew each button at `col * (BTN_W + GAP)`. `viewport_input.rs` decided what a click hit with hand-written arithmetic inline in a 630-line event handler: `BTN_W * 3.5 + GAP * 3`. Those agree for whole-numbered columns and disagree at the half column the zoom pair sits in: 3.5 * 24 = 84 drawn, 22 * 3.5 + 2 * 3 = 83 hit-tested. The zoom buttons' clickable area sat one pixel left of the buttons, so their right-hand pixel column did nothing and a pixel of empty space beside them zoomed. `PanRight` was 2px short at its right edge for the same reason. One pixel is not much on its own. The mechanism is what matters, and it is the third instance of it in this module: two copies of one piece of geometry, free to drift, with nothing able to notice. The camera-to-world pair was the first, the scene-cache benchmarks the second. Both callers now read `nav_pad::LAYOUT`. The renderer iterates it; the hit test tests against it; the offsets come from one function. That also turns 88 lines of inline conditionals in the event handler into 32 lines of match, which is a readability win I would not have bothered with on its own. The bounds are now the drawn rectangle exactly — half-open, BTN_W by BTN_H, gaps dead. The old hit zones were 2px larger than the buttons in several places. Being strict is deliberate: a hit area larger than its button is indistinguishable from a misaligned one the next time something looks wrong. 7 tests, 100% of the new module. The one that matters is `drawn_and_hit_zones_agree`: every drawn rectangle must hit-test to its own button at all four corners and the centre. That test fails on the old code, which is the only reason to trust it. Verified with a real compiler, which this environment turns out to have: 1051 lib tests pass (7 new), cad_integration 154 pass, cargo fmt clean, engine coverage 97.16% with every floor met including nav_pad at 100%. |
|||
| 074066a382 |
feat(email): export the trip report from the inbox
spawn_export_trip_report fetches the inbox (shared fetch_inbox_messages helper), extracts trip receipts, builds the report, and writes trip-expense-report.pdf into app-data; it posts TripReportExported with the path and a summary. The More page gains a Finance card with an 'Export trip report (PDF)' button and a status line. |
|||
| ee61546c2c |
feat(email): trip-expense report PDF for the finance department
finance_report.rs renders TripReceipts into a self-contained PDF with the nigig PDF stack (nigig-pdf-graphics, base-14 Helvetica): a summary table of dates and amounts with a total row, then one receipt block per trip, A4 with pagination and a bookmark outline. build_trip_report is pure and its tests read the output back through PdfDocument, asserting page sizes, dates, amounts and the total landed. Native-only (gated behind not(wasm32)), so the wasm build stays free of the PDF dependency. 6 tests, plus a runnable example. |
|||
| a935133bb4 |
feat(email): trip-receipt extraction (Bolt ride receipts)
email_receipts.rs turns inbox messages into a TripReceipt — date, amount, currency, route, receipt id — for the finance department's expense report. Sender detection (bolt/uber/taxify), a tolerant currency+amount finder (total > fare > amount priority, comma/space grouping, KSh→KES), a date extractor (ISO, d/m/y, '17 Aug 2026', 'Aug 17, 2026', with the email timestamp as fallback), and label-prefix value extraction for the route and receipt id. A message without an amount is not a receipt. 12 host tests over realistic fixtures. |
|||
| e0d86274d8 |
feat(pdf): flatten annotations and form fields into page content
Some checks failed
email.yml / feat(pdf): flatten annotations and form fields into page content (push) Failing after 0s
repo hygiene / hygiene (push) Has been cancelled
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (push) Has been cancelled
Phase 5's `flatten_test.dart`. An annotation draws from its /AP /N stream, which lives beside the page rather than in it; flattening moves that appearance into the page's own content and removes the annotation, so what is drawn is part of the page and cannot be turned off, edited or extracted as a field. That is what "finalise this form" and "make these comments permanent" mean, and it is irreversible by design. The placement transform is the whole problem, and it is §12.5.5: transform the /BBox by /Matrix, take the bounding box of the *result*, then fit that onto /Rect. Skip a step and the stamp lands at the origin, or in the right place at the wrong size, and the page still renders. A rotated appearance is the case that exposes it — rotation swaps the transformed box's width and height, so fitting the untransformed box squashes it. What is refused matters as much as what is done: - **No appearance stream**: left in place and reported. Dropping it loses it; inventing an appearance draws something the producer never specified. - **Hidden or /NoView**: not drawn on screen, so burning it in would *add* ink the user never saw. - **A /Popup**: the pop-up window of another annotation, never drawn on the page itself. Appearances are painted as XObjects rather than having their operators spliced in. Splicing needs the stream's resources merged into the page's with every name collision renamed, and it loses the /BBox clip an XObject applies for free. `pdf-document` cannot depend on `pdf-graphics` — the crate boundary is cos -> document -> graphics and inverting it to reuse `write_ops` would be a far worse trade than emitting the four operators (`q`, `cm`, `Do`, `Q`) directly. The number formatting follows the same shortest-exact rule as `content_edit::write_real`, and for the same reason. Verified by mutation, five defects, each confirmed red: placement matrix ignored 1 fail /BBox not transformed first 2 fail hidden check removed 1 fail annotation kept after flattening 7 fail existing page content dropped 1 fail **Externally verified, and it found a real gap.** Flattening the sample's seven annotations passed `qpdf --check` and kept every text run — but poppler still reported `Form: AcroForm` on a document with no fields left, because the catalogue entry survived. A viewer may still offer to fill in a form that no longer exists. `remove_acroform_if_empty` drops it, but only when *no* widget survives anywhere: flattening one page of a three-page form must not strip the fields still live on the others. before: Form: AcroForm after: Form: none `flatten_document` is the whole-document entry point — every page, then the form entry — and re-parses between pages because each flatten appends a revision the next must read. 22 round-trip tests through the saved file, including that flattening twice is idempotent (a Do count that grows on every save is how a "flatten" button pressed twice doubles every stamp), that existing page resources survive, and that a /AP /N state dictionary resolves through /AS. Engine suite 1075 -> 1108. Remaining in Phase 5: object compaction, redaction, and outline, page label and struct-tree editing. |
|||
| 8ca5070b26 |
test(spreadsheet): cover the remaining serialization and border branches
Some checks failed
email.yml / test(spreadsheet): cover the remaining serialization and border branches (push) Failing after 0s
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
The last reachable lines in data.rs, all in the persistence layer and one style command: - serialize/deserialize round-trip for named ranges (the NAME block line), text colour, and an unknown block tag (ignored for forward compatibility), plus a malformed NAME line inserting nothing. - The legacy CSV deserializer's `|B` bold and `|F` formula metadata segments. - apply_command with BorderTarget::None clearing all four edges (the "remove borders" action), the complement of the per-edge arm the existing test covers. - A large-range self-reference reporting CycleDetected (pins the fix in the previous commit). data.rs 97.91% -> 99.49%; engine total 98.50% -> 99.13%. Unit tests 325 -> 331. Deliberately uncovered, as documented: the B17 invariant's debug_assert/#ERROR! patch (only reachable on a dep-graph bug), the topological-sort underflow guard, the let-else continue after a cell disappears mid-iteration, and the `}` attribution regions after unconditional returns. |
|||
| e1dcbefda3 |
fix(spreadsheet): large-range cycle detection must report, not zero
The large-range fast path in DataEvalContext::get_range_values (used for ranges over 64 cells) handled a re-entrant formula cell — a reference cycle — by silently pushing 0.0. The small-range path in get_cell_value reports FormulaError::CycleDetected for the same situation. So a cyclic formula inside a large range contributed zero to the sum instead of surfacing #CYCLE!. The inconsistency is invisible in normal recalculation: the topological sort in recalculate_incremental detects cycles before any evaluation, so the branch only fires through the legacy recursive get_cell_value API. The fix makes the two range paths agree. |
|||
| 2ba06f34c5 |
fix(makepad-table): stop overlong cell text spilling into the next column
Some checks failed
email.yml / fix(makepad-table): stop overlong cell text spilling into the next column (push) Failing after 0s
repo hygiene / hygiene (push) Has been cancelled
makepad-table / model (push) Has been cancelled
makepad-table / widget (push) Has been cancelled
makepad-table / hygiene (push) Has been cancelled
A right-aligned cell whose text is wider than its column drew over the left-aligned text in the column beside it. Two separate causes, one on each side of the cell. **Spilling left.** `align_text_x` computed `pos.x + width - pad.right - text_width` for right alignment and `pos.x + (width - text_width) * 0.5` for centre. Once `text_width` exceeds the column width both go *negative relative to the cell*, so the run started outside its own cell and reached backwards over the previous column. That is the reported collision: the right-aligned column's text sitting on top of its left-aligned neighbour. The result is now clamped to the padded left edge, so an overlong string starts where a left-aligned one would and runs forwards. **Spilling right.** Clamping alone only fixes the near side — the run would still continue past the cell's right edge into the following column, which is the same overlap seen from the other direction. `fit_text_to_cell` shortens anything wider than the padded width and appends an ellipsis. `DrawText` can do this itself via `text_overflow: Ellipsis`, but only through `draw_walk`, which needs a turtle; these cells are drawn absolutely with `draw_abs`. The truncation therefore uses the same 7px-per-character estimate the positioning already used, so one approximation is applied consistently rather than two that can disagree. When a real text measurer replaces it, both callers change together. Order matters in `draw_cells`: the text is truncated first and the *truncated* string is aligned. Aligning the original and drawing a shorter one positions the run by a width it no longer has, which puts the overlap back. Tests 51 -> 59. Verified by reintroducing four defects: removing the clamp fails the left-spill test, removing truncation fails three, aligning the full text fails the ordering test, and measuring bytes fails the multi-byte test. Two of those needed the tests fixed first, and both were mine: - The draw-path ordering had no coverage at all, because `draw_cells` needs a live `Cx`. It now reads the source for the order of the two calls. Blunt, but an accidental reordering is exactly the regression this invites. - `truncation_counts_characters_not_bytes` passed with `len()` substituted for `chars().count()`. The byte count only gates *whether* to truncate, and the case I wrote was one that should be truncated either way. The test now uses a multi-byte string that comfortably fits — which `len()` would wrongly shorten — so the assertion turns on the difference rather than merely being near it. |
|||
| 306504bc0b |
fix(makepad-table): pin the pressed-state fill, and put the caret after the text
Some checks failed
email.yml / fix(makepad-table): pin the pressed-state fill, and put the caret after the text (push) Failing after 0s
makepad-table / model (push) Has been cancelled
makepad-table / widget (push) Has been cancelled
makepad-table / hygiene (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
Two bugs in the table demo's cell editor, both reported after the previous
colour pass.
**White on click.** `TextInput`'s `draw_bg` blends through six fill states,
and the previous change pinned five of them. It missed `color_down`. The
widget's animator holds `down: 1.0` for as long as the pointer is pressed on
it, so clicking into a cell blended toward the theme's light fill and stayed
there until the pointer moved off and the state decayed — which is exactly
the reported "goes white when I click, correct once I move the mouse away".
It read like a hover state and was the press state.
`color_down` is now pinned, along with the six `color_2*` gradient partners.
The shader only mixes those when `color_2.x > -0.5` and the default is a
-1.0 sentinel, so they were inert — but the theme sets them, and anything
that later turned the gradient on would have pulled theme colours back in.
**Caret at the start.** `begin_edit` called `set_text` and nothing else.
`set_text` loads the value and leaves the cursor at index 0, so typing into a
cell that already had content inserted at the front. `move_cursor_text_end`
after the load puts it where every spreadsheet puts it, and where
`commit_edit` reading the whole buffer back already assumed the user was
working.
Tests 49 -> 51, and the existing state test was rewritten. It had been
checking a hand-written subset of states and passed the whole time
`color_down` was missing — a test that only covers the states someone
remembered is a test that misses the one they forgot. The fill states are now
their own exhaustive check.
Verified by reintroducing each defect: removing `color_down` fails the fill
test, removing the caret call fails the caret test, and swapping the caret
call before `set_text` fails it too.
That last case needed the test fixed first. The ordering guard compared
`body.find("set_text")` against `body.find("move_cursor_text_end")`, and
`find` returns the first match anywhere — including inside the doc comment
above the code, which mentions `set_text`. Swapping the two statements left
the comment in place, so the naive check still passed. It now compares the
first non-comment line containing each call.
|
|||
| 691b868264 |
fix(spreadsheet): make an editing cell readable — no doubled border, cell's own ink and fill
Some checks failed
email.yml / fix(spreadsheet): make an editing cell readable — no doubled border, cell's own ink and fill (push) Failing after 0s
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
The same three symptoms were reported twice. The first fix went to `makepad_table`, which is not in the APK — `pageflipnav` does not depend on it. The editor actually being clicked is `SpreadsheetGrid::draw_edit_overlay`, reached through `nigig-build -> spreadsheet-ui`, and it is a custom-drawn overlay rather than a `TextInput`. This fixes that one. 1. **No border of its own.** `draw_walk` calls `draw_selection_overlay` immediately before `draw_edit_overlay`, and the selection rectangle is already stroked around this exact cell in `selected_border_color`. The overlay then drew four more 2px rects inside it, which is the doubled frame: an outer selection edge and an inner editor edge a pixel apart. The four strokes are gone. Checked before removing them that this cannot leave a cell unframed. All three paths into `request_begin_edit` are on an already-selected cell: the keystroke path uses `self.selected_cell()` by definition, and both double-tap paths record `InputIntent::Select` for the same cell on the first tap. The caret remains the signal that the cell is in edit mode. 2. **The cell's own ink, not a forced one.** The overlay did `self.draw_text.color = self.text_color;` with no branching, so a cell with a user text colour, a bold cell, or a formula all changed colour the moment the caret landed in them. It now resolves the same way the resting draw path does: per-cell colour, then formula green, then bold, then the default. 3. **The surface it rests on.** The fill was always `edit_bg_color`, which equals `cell_bg_color` — so an odd row, which rests on `cell_alt_bg_color`, visibly changed shade when editing began, and a cell the user had given a background lost it entirely. That last case is the "background goes a different colour and the text disappears" report: the fill came from `edit_bg_color` while the ink came from the cell's own style, and nothing kept the two in agreement. It also covers the selection fill. A cell being edited is by definition selected, so `draw_cell_bg` had already painted `selected_bg_color` (#x2d4a63, a blue-grey) underneath — text was sitting on a wash it was never coloured for. That is the grey. `grid.rs` had no tests and is excluded from the coverage report, because the widget needs a live `Cx`. The background choice does not, so it moved into `editing_bg_source` and is tested there; the border and ink changes are pinned by reading this file, which is blunt but is the only thing short of a GPU that can catch "compiles, runs, wrong on screen". 6 tests, verified against all three defects: restoring the border stroke fails 1, forcing `text_color` again fails 1, and ignoring row striping and cell backgrounds fails 3. The 12 clippy warnings in this crate tree are pre-existing and unchanged — checked by running the same command on a stashed tree. None are in the hunks here. |
|||
| 60117099ae |
fix(makepad-table): make an editing cell readable — white fill, dark ink, one border
Some checks failed
email.yml / fix(makepad-table): make an editing cell readable — white fill, dark ink, one border (push) Failing after 0s
repo hygiene / hygiene (push) Has been cancelled
makepad-table / model (push) Has been cancelled
makepad-table / widget (push) Has been cancelled
makepad-table / hygiene (push) Has been cancelled
Three reported problems, one cause. `TextInput` does not have a colour, it has a set of them, and `get_color` blends between them: `color_focus` on focus, `color_empty*` when the field is empty, `color_hover`, `color_down`, plus `border_color_2*` gradient partners for every border state. The cell editor set four properties — `color`, `color_focus`, `border_color`, `border_color_focus` — and left the rest to fall back to the active theme. The active theme is the dark one: `theme_desktop_dark::script_mod` runs after the light one in `widgets/src/lib.rs` and wins. So the moment a cell took focus the editor drew `theme.color_text` (near white) on `theme.color_inset_hover` (dark grey), and an emptied cell hit the `color_empty` path, which was also unset. Fixed by pinning every state rather than the ones that happened to be visible in one configuration: 1. **No border of its own.** `border_size: 0.0` and every border state transparent, including the six `border_color_2*` partners, which render as a faint grey edge even at zero width on some backends. The 2px green frame is `draw_select`, drawn over the same rect in `draw_walk` — the editor's border sat inside it and read as a doubled frame. The green selection is unchanged. 2. **Editing ink equals resting ink.** `#x1f2937` in all eight text states, the same colour a non-editing cell uses. An editing cell should look like a resting cell with a caret in it; the caret is the affordance and a colour change only costs contrast. `draw_cursor` is pinned too — `theme.color_text_cursor` is chosen for a dark inset and nearly vanishes on white, which would have left no cue at all. 3. **Plain white fill**, in all five fill states rather than just two. This is also the "background goes white and the text disappears" case: that was the fill switching to the pinned white while the text switched to the unpinned theme colour, so the two moved independently. `draw_selection` is pinned to a pale green that matches the frame, instead of the theme's blue, so selected text stays dark-on-light. The same latent bug was in all six of the invoicer's inputs — the five header fields and the search box — which set the same four properties. They are pinned the same way. Their placeholders stay grey deliberately: a placeholder is a prompt, not content. Tests 44 -> 49. The colours live in `script_mod!`, which is data this crate does not parse, and no test can ask a widget what it drew without a GPU — so these read the DSL source and assert the states are present. That is blunt, and worth having anyway, because the failure mode is precisely "compiles, runs, looks wrong only on screen". Verified by restoring the original four-property block: all five fail. |
|||
| e0aab74452 |
feat(pdf): page operations — insert, reorder, duplicate, delete, import
Some checks failed
email.yml / feat(pdf): page operations — insert, reorder, duplicate, delete, import (push) Failing after 0s
repo hygiene / hygiene (push) Has been cancelled
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (push) Has been cancelled
Phase 5's page management, matching dart-pdf's `page_ops_test.dart`, `page_index_map_test.dart` and `import_source_test.dart`. A reader sees "page 3"; the file holds a tree of /Pages nodes with /Kids and /Count and /Parent back-pointers, any of which can be left stale. So the writer **flattens to a single level**: a one-level /Pages node with every page as a direct kid is valid, is what most producers emit, and removes the entire class of bug where an intermediate node's /Count no longer matches what is under it. Preserving an arbitrary tree shape through arbitrary reordering is far more code for nothing a reader can see. `PagePlan` accumulates operations and applies them together, so intermediate states never have to be valid — delete page 0 and insert a new one at 0 without the document momentarily having no first page. `PageIndexMap` reports where every page went, which is the only way to fix an outline entry, named destination or link annotation afterwards. What each operation carries matters and differs: - Reorder and delete rewrite only the kid array, so page objects and their resources are untouched. - Duplicate writes a new page dictionary that **shares** the original's resource references. Two pages naming one font object is normal; deep-copying would double the file and change nothing visible. - Import must deep-copy the page and everything it reaches, renumbered, because source object numbers mean nothing in the destination. /Parent is deliberately not followed — it leads back to the source's page tree and from there to every other page in that file. Inheritable attributes are resolved *before* a page is imported. /Resources, /MediaBox, /CropBox and /Rotate may live on an ancestor (Table 30) that is not coming with it, so a page imported without them renders at the wrong size with no fonts, and nothing reports an error. **Round-trip tested through the saved file**, which is Phase 5's exit criterion: 20 tests that save, re-parse, and assert on what a reader actually gets. Pages are identified by /MediaBox width rather than object number, because object numbers are exactly what a page-tree bug scrambles. Mutation testing changed two things. Seven defects injected: /Count left stale 1 fail /Count omitted entirely 1 fail imported /Parent not rewritten 1 fail inherited attributes not resolved 1 fail import does not deep-copy 3 fail duplicate loses /Contents 1 fail re-parenting skipped 1 fail The last two only fail because of tests the mutations forced: - **A stale /Count passed everything.** Our own parser walks /Kids and never reads /Count, so it cannot see the disagreement — but other readers trust /Count, and a document where the two differ opens with a different page count in different viewers. The test now reads the raw page-tree node instead of asking the document. - **Re-parenting could be deleted with every test still green**, because the flat fixture's pages already parent to the root. Added a nested fixture with an intermediate /Pages node supplying an inherited /MediaBox — the case where leaving /Parent stale means a page keeps inheriting from a node it is no longer under. Externally verified: a generated sample with pages swapped and duplicated passes `qpdf --check` with no warnings, and poppler reads 4 pages with the reordering visible in extracted text. Engine suite 1039 -> 1075. Remaining in Phase 5: flatten, object compaction, redaction, and outline and struct-tree editing. |
|||
| b8bd71852f |
feat(pdf): content-stream serialiser and editor — the Phase 5 foundation
Some checks failed
email.yml / feat(pdf): content-stream serialiser and editor — the Phase 5 foundation (push) Failing after 0s
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
Phase 5 needs to write operators back, and `content.rs` has only ever
parsed them. Every editing feature the phase asks for — insert, delete,
replace, rewrite a text run, flatten an annotation — rests on that, and a
serialiser that is subtly wrong does not throw: it writes a valid content
stream that draws something else.
So this is the serialiser plus the gate, and nothing built on top yet.
The contract is a property, run over the whole corpus:
parse(write(parse(bytes))) == parse(bytes)
Operators, not bytes. Byte equality would be the wrong test — `1.0` may
legally be written `1`, whitespace is free, and a writer that reproduced
its input byte for byte would only prove it had copied it.
It passes: **16,466 operators across 154 streams in 109 files**, plus
stability, idempotence, and the same property after an edit.
**Then mutation testing showed the corpus gate was not enough.** Six
injected defects, and *five passed*: dropping name escaping, unescaping
string parens, un-sorting dictionary keys, discarding unknown operators,
and a fixed six-decimal number format. Real files are written by
well-behaved producers, so 16,000 corpus operators contain no name with a
space, no nested parenthesis, no seven-key inline dictionary and no
vendor operator. A gate that only sees well-formed input cannot catch a
writer that mishandles the rest.
The adversarial set fixes that — eighteen streams, each a legal shape the
corpus lacks, each chosen because a specific defect survives without it.
Writing it found **three live bugs in the parser**, none of which the
round trip could see on its own:
- **Nested parentheses truncated a string to nothing.** `((nested))`
parsed as the empty string, and worse, left the reader mid-string so
every operator after it was parsed from the wrong offset. §7.3.4.2 says
balanced parens nest and need no escaping.
- **`#` escapes in names were never decoded.** `/My#20Font` — how every
producer writes a font whose name contains a space — parsed as the
literal `My#20Font` and never matched the page's resource.
- **`PdfOp::Unknown` was declared and never constructed.** An operator
the parser did not recognise vanished. Survivable for a renderer, fatal
for an editor: parse, change one operator, write back, and every vendor
extension in the page is silently gone from the saved file.
And two in my own serialiser, both found the same way:
- A fixed `{:.6}` flushed 1e-7 to zero — a scale factor silently becoming
zero collapses whatever it transforms — and rounded `1.234567891` to a
different number. Precision is now the shortest that parses back to the
identical f64, exact by construction rather than by choosing a number.
- Sorted dictionary keys turned out to be load-bearing. `PdfDict` is a
HashMap and Rust seeds its hasher per process, so an unsorted writer is
stable within a run and different on every new one: rebuild the same
document twice, get two different files. Neither the round trip nor a
within-process stability check can see it — both sides are equally
unordered. Verified by running five separate processes and getting five
different key orders.
Two of those needed tests the round trip structurally cannot provide, so
they assert on the parser directly: what `((nested))` must produce, and
that operators after it are still read at the right offset.
Final mutation run, eight defects, all caught:
fixed 6-decimal precision 1 fail
name escaping dropped (writer) 1 fail
name unescaping dropped (parser) 2 fail
nested-paren fix reverted 1 fail
unknown operators discarded 1 fail
string parens unescaped 1 fail
close-paren unescaped 1 fail
dictionary keys unsorted 2 fail
`ContentEditor` sits on top: insert, append, prepend, delete, replace,
isolate, and text-run rewriting that preserves the operator *kind* — a
`'` stays a `'` and keeps its line advance, a `TJ` keeps its kerning
numbers while its strings change. Every mutation is balance-checked, so
an edit that would leave `q` without `Q`, or `BT` without `ET`, is
refused at the edit rather than discovered at save time. `PdfOp` gained
`PartialEq`, which is what makes the property expressible at all.
Engine suite 1025 -> 1039.
Phase 5's remaining items — page ops, import/merge, flatten, compaction,
redaction — build on this and are not started.
|
|||
|
|
b478945c34 |
ci(doc): gate the doc-workspace coverage floor on every push
Some checks failed
email.yml / ci(doc): gate the doc-workspace coverage floor on every push (push) Failing after 0s
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
Adds the doc-workspace-coverage job to the nigig-build workflow, mirroring the CAD gate: checkout, then ./tools/test-doc-workspace-coverage.sh, which installs its own instrumented toolchain into a shell-trap-cleaned temp dir and fails if the total floor (92%) or any per-file floor is not met. The script joins the workflow's push/PR path filters next to tools/test-cad-coverage.sh so edits to the harness itself re-run the gate. The doc README gains the milestone section recording the 28.55% -> 96.76% line measurement, the honest exclusions (widget layer, persistence write-path wrappers, defensive traversal guards) and the behavior pins and defect fixes the drive surfaced. |
||
|
|
949cf24189 |
test(doc): doc-workspace coverage harness with enforced floors
tools/test-doc-workspace-coverage.sh measures line+region coverage of the doc module's pure layer the same way the CAD gate does: it copies the dependency-free sources (advanced_json, crdt_bridge, mobile_gesture, persistence, projection_layout, projection_session, and the collaboration/, editing/, layout/, model/, plugins/ trees) plus tests_pure.rs into a temporary host-only crate with the real module path, satisfies the five makepad-math symbols the pure layer uses through a 30-line makepad-widgets shim, runs the suite under -C instrument-coverage with a toolchain it installs itself, and enforces a total floor plus a per-file floor for every instrumented file. The per-file floors are the point: a lone total waves through the silent loss of one whole file's tests. Measurement moved from a 28.55% line baseline to 96.76% (6170 lines) with the tranche in the parent commit; floors sit a few points under per file, except persistence.rs (55%), whose three write-path entry points write into the host's real application-data directory and are covered through their path-injected seams instead -- the honest exclusions, the exact table, and the two defect fixes this drive surfaced (ReplaceBlockRange validation order, dead RgaText::visit_children) are written down in the module's new COVERAGE.md. Everything the script touches -- pinned toolchain, cargo home, target dir, fetched Makepad tree, profraw data -- lives under one mktemp dir removed by a shell trap on every exit path; nothing lands in the repo or $HOME unless KEEP_COVERAGE=1 is set for a debugging run. DOC_WS_COVERAGE_REPORT_ONLY=1 measures without gating. |
||
|
|
71c31cd19f |
test(doc): host-only suite split + pure-layer coverage tranche
Split the doc module's tests in two so the dependency-free majority
can also run under coverage instrumentation on a host-only crate:
* tests_pure.rs (new) holds every test that needs no Cx -- model,
layout, editing, collaboration, advanced JSON, projection
layout/session, CRDT bridge, persistence seams, mobile gestures --
and is the file tools/test-doc-workspace-coverage.sh copies
byte-for-byte into its harness.
* tests.rs keeps the widget-runtime and boot tests; shared helpers
(projection_table_engine, only_table) live in tests_pure so both
suites use them.
On top of the split, this tranche adds ~80 tests covering the pure
layer's real gaps: every Command apply arm and its inverse (text,
atoms, blocks by stable id, image properties, block ranges, table
cells/rows/columns, merges/splits/restores, node insert/delete/
replace), the DocumentController's CRDT typing lifecycle
(insert/replace/delete ranges, backspace/forward delete, position
sync), remote operation classification
(Applied/Duplicate/Deferred/Rejected), tombstone compaction at a
peer-acknowledged frontier, a two-peer MemoryTransport conversation,
typing coalescing and history limits, the cell-text editing and
multi-line geometry helpers in projection_layout, table cell
cursor/range/merge queries, and the small model/session/selection
behaviour surface.
Two defects found while writing the tests, fixed with pins:
* Command::ReplaceBlockRange validated a caller-supplied block_ids
length AFTER draining blocks and legacy ids out of the document, so
a malformed (remote) command destroyed content before reporting
failure. Validation now runs before any mutation; a test proves a
rejected replace leaves blocks, ids and order untouched.
* model::crdt::RgaText::visit_children was dead code -- a
String-collecting duplicate of visit_atoms with no callers. Removed.
Two semantics that were folklore are now pinned with inline
reasoning: a multi-peer conversation only converges when each peer
owns a distinct document.crdt.local_actor (the two-controller sync
test assigns alice/bob), and a mid-range replace_range_crdt renders
its replacement after the tombstoned subtree it replaced, because
RGA sibling order walks by atom id ("hello" -> "hloY" is asserted,
not assumed).
cargo test -p nigig-build --lib: 1031 passed, 0 failed.
|
||
| ac145bfaab |
test(cad): measure tools.rs, which a false comment had ruled out — 0% to 97.07%
Some checks failed
email.yml / test(cad): measure tools.rs, which a false comment had ruled out — 0% to 97.07% (push) Failing after 0s
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
tools.rs opens with:
Struct definitions stay in mod.rs (they use #[derive] macros that
need the script_mod! context). Only the impl blocks are here.
That is not true, and it is the reason 250 lines of pure tool-state
logic — tool cycling, work-plane mapping, the inclined-UCS maths — had
never been measured or tested. `CadTool`, `WorkPlane`, `AxisLock`,
`DrawingState`, `SnapSettings` and `InclinedPlane` each derive some
combination of Clone/Copy/Debug/PartialEq/Eq and nothing else, and none
is inside the `script_mod!` block.
I found it by asking the compiler instead of grepping. Adding each of the
eleven unmeasured CAD files to the harness one at a time and reading the
errors gives the real dependency, not a guess: tools.rs needed six plain
types; viewport_2d.rs needs `CadViewport` and `Cx`; code_editor.rs needs
`Cx2d` and `DrawStep`. Only the first of those is a documentation
problem rather than a real one. My previous two triage passes used a
grep heuristic and were wrong twice, including about this file.
The harness now mirrors those six declarations, extracted from mod.rs at
run time so they cannot drift, exactly as it already did for ViewMode and
SelectionMode. **No production code moved.** Moving the declarations for
real is a smaller job than the comment implies but not a free one:
DrawingState, SnapSettings and InclinedPlane have private fields that
mod.rs and viewport.rs read directly, so their fields need widening
first. CadTool and WorkPlane are fieldless and could move today. That is
now written in the file for whoever has a compiler for the widget layer.
13 tests, on the properties that break quietly:
- Cycling forward visits all 17 tools exactly once and closes the
ring. `cycle_next` is a hand-written 17-arm match; a duplicated or
skipped arm makes a tool unreachable from the keyboard and nothing
else would notice.
- Backwards is asserted to be the exact inverse, per tool. Shift-Tab
that does not undo Tab reads as "the tool picker jumps".
- Labels must be unique — two buttons reading the same is a UI bug
with no test otherwise — and every tool needs a description.
- `to_kind` maps only the drawing tools; Select, Delete and Measure
must return None or they would create geometry on click.
- `InclinedPlane::from_3_points` produces a unit normal perpendicular
to both edges, and rejects collinear or coincident picks rather than
returning a NaN basis from a zero-length cross product.
- The plane basis is orthonormal in both branches, including the
vertical-normal case that exists because the usual "up" reference is
parallel to the normal there.
Also fixes a real bug in this script's own drift detector: it tested
membership with `case " ${ENGINE_FILES[*]} "`, and `[*]` joins on the
first character of IFS, which this script sets to a newline. The pattern
could never match, so the note fired for every non-widget file. It was
right about tools.rs by accident.
Floor: tools.rs 95. Total unchanged at 97.14% over a larger denominator.
Verified: 568 tests green, every floor met, hermetic run clean.
|