Commit graph

15 commits

Author SHA1 Message Date
Arena Agent
cb5def965b fix(cad): exports reported success on a failed write
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
Every file export buffered its output through a BufWriter and never
flushed it. BufWriter flushes on drop and DISCARDS any error it hits
doing so, so a write that fails only when the buffer is pushed to disk --
full disk, revoked permission, a network mount going away mid-export --
returned Ok(()). The status label then said "STL: 11 parts ->
model.stl" while the file on disk was truncated.

Demonstrated before fixing, with a writer that accepts buffered writes
and fails on flush:

    without explicit flush -> Ok(())
    with explicit flush    -> Err("flush failed: disk full")

Three affected paths: PDF (exporters.rs), STL and GLB (workspace.rs).
All now go through exporters::export_to_file, which owns the BufWriter,
flushes it, and maps a failed flush to "<what>: could not write file:
<cause>". Putting the flush in one place is the point -- it was
forgotten three times independently.

The fourth BufWriter, in arch_pdf, writes into a Vec<u8> where flush
cannot fail. Left alone and excluded from the gate with that reason
recorded, rather than churned for uniformity.

Two tests pin the mechanism rather than one exporter: a small write
stays in the buffer, so write_all succeeds and only the flush can report
the failure. One asserts the drop path hides it, the other that an
explicit flush surfaces it. If BufWriter's drop behaviour ever changed,
the first test would fail and tell us the helper is no longer needed.

CI gate added and negative-tested: no BufWriter::new in the CAD module
outside the flushing helper.

Also checked, and found clean: arch_svg still escapes both XML sinks
after the Phase-4 projection rewrite, and all three cargo-deny
exemptions are still live -- removing them makes exactly those three
advisories fire, so none is a stale entry silently widening the policy.

723 lib + 154 integration, 0 failed. All twelve gates pass.
2026-08-02 07:12:23 +00:00
Arena Agent
03dea4d14f fix(cad): remove panics from CAD production paths
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
Continued auditing the code the KeyCode fix (667f565) made reachable, and
followed the panic sites out from there.

Command::merge's default was `panic!("merge() called on a Command that
didn't override it")`. That is reachable, not defensive: any command that
overrides can_merge but forgets merge hits it. UndoRedoStack::execute
runs it on every frame of a drag, and the code five lines away already
declines to panic on a genuinely unreachable branch, with the comment
"never panic on the undo path". A CAD editor holds unsaved work -- losing
the session is far worse than the mis-merged undo entry being reported.

Now a debug_assert!: loud in development where it is actionable, and
survivable in a shipped build. Proven by test in both configurations --
the release behaviour is pinned by a test marked
`#[cfg_attr(debug_assertions, ignore)]`, since the dev build is *supposed*
to fire the assertion. Negative-tested by restoring the panic.

Also replaced three `unreachable!()`s:

- add_part_command and split_selected_part_at both did
  `position(..)` then `get(idx).unwrap_or_else(|| unreachable!())`. The
  index round-trip is what created the Option they then had to discharge
  with a panic; `find(..).cloned()` and `enumerate().find(..)` express
  the same thing without arming one.
- The DDE digit match's `_ => unreachable!()` arm is genuinely dead (the
  outer arm narrows to Key0..Key9), but a keyboard handler is not worth
  a crash to say so. It swallows the keystroke instead.

CAD production code now contains no panicking macros at all, enforced by
a new CI gate that scans each file up to its first #[cfg(test)] -- test
code may panic freely. Negative-tested.

One test-authoring note worth recording: my first version of the merge
test built five MoveNode commands all with `from: 0`, which tripped
MoveNode::execute's debug_assert that the scene's previous position
matches `from`. The assertion was right and my test was wrong; a real
drag chains each frame's `from` to the previous `to`. The corrected test
now also verifies the thing that assertion protects -- five drag frames
collapsing into one undo entry.

710 lib + 154 integration, 0 failed. All ten gates pass.
2026-08-01 04:56:02 +00:00
nigig-ci
817ba02625 build: drop the unused robius-sms dependency and its two advisories (Phase B)
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
sms / gates (push) Has been cancelled
sms / robius-sms (push) Has been cancelled
sms / android (push) Has been cancelled
sms / nigig-sms (push) Has been cancelled
sms / supply-chain (push) Has been cancelled
doc-engine / engine (push) Has been cancelled
doc-engine / consumer (push) Has been cancelled
nigig-map / test (push) Has been cancelled
nigig-build declared robius-sms and robius-location and called neither:
zero references to robius_sms or robius_location anywhere under
nigig-build/src. That dead dependency pulled in

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

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

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

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

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

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

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

Also in this commit:

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

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

Note the scope limit: 26 of the 29 crates in this workspace declare
robius-sms and never use it. This commit fixes the three that put
advisories on nigig-build. The rest are the same latent problem and
should be swept separately.
2026-07-31 22:45:58 +00:00
Arena Agent
5e714577fb ci: require full 40-character SHAs for git dependency revs
Some checks failed
Payment domain, storage, platform and UI / isolated-payment-tests (push) Has been cancelled
Payment domain, storage, platform and UI / payment-ui-tests (push) Has been cancelled
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-map / test (push) Has been cancelled
Upstream adopted the makepad fork fix (8fdff3f) but pinned it as
`rev = "a79f0dc"` -- a 7-character abbreviation. The pinning gate passed,
because it only checked that `rev = ` was present at all. Its own error
message has said "Add rev = \"<full-40-char-sha>\"" since it was written,
without ever enforcing it.

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

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

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

685 lib tests pass; all nine gates pass.
2026-07-31 19:53:49 +00:00
Arena Agent
80425bbfb8 fix: repair the makepad fork and unblock the build
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-map / test (push) Has been cancelled
Payment domain, storage, platform and UI / isolated-payment-tests (push) Has been cancelled
Payment domain, storage, platform and UI / payment-ui-tests (push) Has been cancelled
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (push) Has been cancelled
The repository has been uncompilable since the d6d1f99c fork bump. Root
cause was in gitdab.com/andodeki/makepad, not here, so the fix went there
first: commit 5eda8056 on portallist_flow_adaptive_view.

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

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

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

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

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

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

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

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

That makes four files in three commits now carrying committed conflict
markers from this merge. Worth checking how they are reaching main --
`git diff --check` catches exactly this and is already a step in the
nigig-build workflow, but it only runs on paths under that workflow's
filter.
2026-07-31 19:32:08 +00:00
8c9ccb92cc Update makepad fork to latest dev branch (d6d1f99c)
Some checks failed
nigig-build.yml / Update makepad fork to latest dev branch (d6d1f99c) (push) Failing after 0s
pdf.yml / Update makepad fork to latest dev branch (d6d1f99c) (push) Failing after 0s
nigig-map / test (push) Has been cancelled
Payment domain, storage, platform and UI / isolated-payment-tests (push) Has been cancelled
Payment domain, storage, platform and UI / payment-ui-tests (push) Has been cancelled
- Sync with upstream commit 5d4483f (latest map improvements + platform updates)
- Include location API, audio echo cancellation, bridge-dz overlay
- Add new libraries: geodata, map_nav, i_float, i_shape, i_tree, converse, llama vision
- Preserve all fork-specific re-exports (gltf, csg, test)
- All 102+ map improvements now available: 2D/3D toggle, shadows, labels, overlays, pattern fills
2026-07-31 18:47:37 +00:00
Arena Agent
36c9c0b0d1 fix(cad): both Save buttons reported success when the write failed
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
Went looking for remaining defects before starting the viewport split and
found silent data loss in the two Save buttons.

    cad_store::save_cad_script(&project.id, &source).ok();
    self.view.label(..).set_text(cx, "Saved");

    save_cad_state(&source, &solid).ok();
    self.view.label(..).set_text(cx, "Saved with mesh");

Both discard a Result carrying a real io::Error and then report success
unconditionally. The user is told their work is safe when nothing was
written.

This is reachable, not theoretical. `cad_store::cad_projects_dir()` only
*logs* a `create_dir_all` failure and returns the path anyway, so the
following `fs::write` fails with a genuine error -- which was thrown
away. An unwritable data directory, a full disk or a permissions problem
all render as "Saved".

Save-As had a third silent failure on the same line of reasoning: if
`bake_parts_solid()` returned None the save never even ran, and it still
said "Saved with mesh".

Fixed via save_status_message(what, result), so the label is derived from
the outcome instead of assumed. Three distinct messages now: the caller's
success wording, "Save failed: <cause>" with the underlying error text
preserved, and a specific message for the no-mesh and no-active-project
cases, which were previously indistinguishable from success.

Also replaced the hand-rolled widget borrow in Save-As with
with_viewport, the helper added in 5.2.

Tests assert the two properties that matter: a failed save must not read
as success and must carry its cause through to the user, and a
successful save must use the caller's wording. Negative-tested.

CI gate added and verified in both directions: greps for a discarded
Result on a save/write/persist/store/export call. A dropped Result is
merely untidy in most places; on a save path it is data loss.

These were the only two instances -- the CAD module now has zero
discarded write Results.

685 lib + 154 integration, 0 failed. All seven gates pass.
2026-07-31 18:42:27 +00:00
Arena Agent
e7a201246c ci(cad): scope the source-scanning gates to .rs files
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
The "No build-time paths used at runtime" gate has been failing since the
commit that introduced it, and I did not notice because I verified the
gate's intent rather than running its exact shell pipeline.

It greps the CAD directory for env!("CARGO_MANIFEST_DIR") and filters out
lines starting with a // comment marker. ARCHITECTURE.md documents this
very rule, so it necessarily names the macro -- and a Markdown list item
is not a // comment, so it survived the filter. The gate failed on its
own documentation.

Both files were added in the same commit (b5471e3), so this was broken
from the start rather than regressed later.

Fixed with --include='*.rs' on that gate, and pre-emptively on the other
two source-scanning gates (RFC1918 endpoints, by-value Vec3f getter
writes). Neither matches a non-.rs file today, but both would break the
same way the moment their rule gets documented in the same directory --
which is exactly what a reviewer is likely to do next.

Verified by extracting every grep-based step from the workflow and
running them in a shell with set -euo pipefail, rather than by
re-reasoning about the regexes. All six pass. Negative-tested the fixed
gate: a real env!("CARGO_MANIFEST_DIR") call in persistence.rs still
fires it.

Lesson worth recording: a gate must be executed as CI executes it. This
one asserted something true about the code and still failed, which is the
failure mode that erodes trust in CI fastest.
2026-07-31 18:28:33 +00:00
arena-agent
7143b3e798 feat(doc): render CRDT-native tables in CrdtDocEditor
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
doc-engine / engine (push) Has been cancelled
doc-engine / consumer (push) Has been cancelled
- projection_layout builds ProjectedTableLayout geometry (fixed 160x28
  cells matching the legacy bridge width, merge spans with covered-cell
  flags) and per-block origins so blocks after a table clear it instead
  of using the fixed line step
- ProjectionRenderer draws the table grid through a new draw_table_border
  live field and consumes layout block origins for text placement
- table_hit_test maps points to merge-anchor cells for future cell editing
- doc-engine: split the cramped style-patch line clippy flagged as
  possible_missing_else; the crate is clippy-clean again
- README: tick verified CRDT bridge boxes (dependency, wiring, selected
  replacement/formatting/table/toolbar migration, font size/color
  migration) and document the table milestone
- CI: add doc-engine workflow (engine tests + clippy -D warnings +
  nigig-build consumer check/test) and trigger nigig-build CI on doc
  changes
2026-07-28 21:01:31 +00:00
Arena Agent
667f565ddd fix(cad): match arms bound new variables instead of matching KeyCode
Went looking for a clippy ratchet and found that clippy had never run on
this crate at all: two dependencies fail deny-by-default lints, so
`cargo clippy -p nigig-build` aborted before linting nigig-build. Fixing
those two unblocked the crate and immediately exposed a real defect
class.

THE BUG. A bare identifier in a match pattern that is not a known variant
is parsed by Rust as a NEW BINDING that matches everything. Nine such
names were used as KeyCode patterns:

  KeyEnter, KeyBackspace, BracketLeft, BracketRight   (cad/viewport.rs)
  Digit0..Digit9, Equal, LeftBracket, RightBracket,
  Apostrophe                                          (doc, invoice, pm)

Real names are ReturnKey, Backspace, LBracket, RBracket, Key0..Key9,
Equals, Quote.

Consequences, in severity order:

- cad/viewport.rs: `BracketLeft => { .. }` is UNGUARDED and sits above
  the numeric arms, so it swallowed every remaining key. All
  direct-distance-entry input -- digits, '.', '-', ',' -- was
  unreachable. The entire DDE feature was dead.
- The guarded ones fired for any key satisfying the guard: pressing Q
  while drawing with a non-empty buffer committed the coordinate.
- doc/invoice/project_management: `Digit0 => '0'` swallowed the rest of
  the keymap, so every digit and symbol typed produced '0'.

This compiles cleanly and no test catches it. rustc's only signal is
`unreachable_pattern` plus `unused variable: \`Capitalised\`` -- both
buried in the 200+ warnings nobody could see, because clippy never ran.
85 unreachable-pattern warnings before, 1 after (a benign catch-all).

UNBLOCKING CLIPPY. Two deny-level errors in dependencies:

- nigig-uikit user_project_pill.rs: a `for` loop returning on its first
  iteration (never_loop). Rewritten as `.next()`.
- spreadsheet-engine formula2.rs: CellRef had an inherent to_string
  shadowing Display (inherent_to_string_shadow_display). The naive fix is
  a trap: Display::fmt was `f.write_str(&self.to_string())`, which
  resolved to the inherent method -- delete it and the same call resolves
  to ToString::to_string, which calls Display::fmt, recursing until the
  stack overflows. Verified with a standalone repro before fixing. The
  body moved into Display; Range got the Display impl it never had.

Tests: keycode_variant_tests names the four correct variants, so it stops
compiling if any is renamed -- the point being that the old code compiled
precisely because the names were wrong. Two formula2 tests pin that
`x.to_string()` and `format!("{x}")` agree, which is what the shadowing
lint exists to protect.

CI gate added and negative-tested: greps for a capitalised
`unused variable`, which is the signature of this bug. Restoring
BracketLeft makes it fire.

625 lib + 154 integration + 225 spreadsheet-engine + 44 doc-engine, 0
failed.
2026-07-28 20:24:03 +00:00
Arena Agent
80c3f578dd ci(cad): gate nigig-build with cargo-deny (Phase 0.4)
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
Payment domain and storage / isolated-payment-tests (push) Has been cancelled
Blocked since Phase 0 because cargo-deny resolves from the lockfile, and
unblocked once Cargo.lock was committed in 0.2. I predicted this would
surface a large backlog. It did: 5 advisories, 6 licence rejections, 126
git-source errors and 20 unlicensed crates.

The triage is the work here, not the config.

ONE ACTUAL VULNERABILITY. RUSTSEC-2026-0187: unbounded recursion parsing
nested PDF objects in lopdf 0.31, reached via printpdf 0.7. A ~21 KB
crafted file aborts the process with SIGABRT, and because it is a stack
overflow rather than a panic, catch_unwind cannot contain it.

Not exploitable here, and I checked rather than assumed. The advisory is
specifically about Document::load*, the parsing entry points. This crate
never parses a PDF -- arch_pdf.rs only writes them through printpdf's
drawing API, there is no lopdf import anywhere in the workspace, and
there is no PDF read path of any kind. There is also no fix in range:
printpdf 0.7 pins lopdf 0.31 and `cargo update -p lopdf` moves nothing.
Clearing it means printpdf 0.8+, a breaking change across arch_pdf.rs.

So it is ignored with the reasoning written down AND with the condition
that invalidates it: the moment anything in this crate reads a PDF -- an
import feature, a thumbnailer, a preview pane -- this becomes a live DoS
and the ignore must go. An ignore without its expiry condition is how
real vulnerabilities get inherited.

The other four advisories are unmaintained/unsound transitive crates
(proc-macro-error, ttf-parser, atomic-polyfill, glib VariantStrIter),
none with a safe upgrade, all arriving through the GUI/platform stack.
Listed individually rather than disabling the unmaintained class, so a
new one still fails.

Six licence rejections were permissive licences simply absent from the
allow list. MPL-2.0 (option-ext) is weak copyleft, so it is granted to
that one crate rather than added globally -- MPL reciprocity is per-file
and only bites if the crate is vendored and edited.

The 126 git-source errors and 20 unlicensed crates are artifacts, not
findings. Every git dependency is pinned to a full rev, which is stronger
than a version range and already enforced by its own CI step; the
unlicensed crates are first-party and vendored forks with no license
field. Both handled WITHOUT allow-ing the class: sources are governed by
the rev-pinning step, and the 20 crates are clarified by name, so a
genuinely new unlicensed dependency still fails. Blanket-disabling would
have hidden exactly the case worth catching.

Separate file from deny.toml on purpose. Merging them would mean
loosening the payment rules to fit a Makepad + Robius graph, and the
payment policy is the one worth keeping tight. Verified both pay crates
still pass unchanged.

Negative-tested in both directions: removing the lopdf ignore fails the
gate, and removing MIT from the allow list produces 79 rejections. A gate
that cannot fail is worse than no gate.

622 lib + 154 integration, 0 failed.
2026-07-28 19:51:37 +00:00
Arena Agent
f20ba795c0 fix(cad): time-box script evaluation so a runaway cannot wedge the worker
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
Phase 2.4, which I deferred during Phase 2 on a wrong assumption.

An unterminated CAD script hung the evaluator permanently. vm.eval is a
single blocking call, so `while true { s = s.translate(..) }` never
returns: the rebuild worker blocks forever, every later edit queues behind
it, and the UI sits on "Computing 3D model..." until the app is killed.
The source is user- and AI-authored, and an LLM will emit a loop like that
without much provocation.

Reproduced before fixing. A probe test was still running at 60 seconds,
and again at 90 with the budget removed.

I deferred this originally saying it needed "a cooperative check inside
the Makepad script VM or a watchdog that can kill and respawn the worker
thread", and called it a VM design decision. That was wrong, and I should
have read the VM before concluding it. makepad_script already provides
ScriptRunBudget::from_durations(soft, hard, sample_interval), checked in
run_core against a sampled Instant. The CAD module simply never set it.
The fix is six lines in eval_cad_script_in_vm. PHASE2_STATUS.md now
records the bad call rather than quietly marking the item done.

Only the hard deadline is used. A soft hit sets TimeBudgetYield, which
expects a host that drives the VM back to completion; there is no such
driver here, so a soft deadline would present as a script that silently
produced nothing at all. Soft is set equal to hard so the budget can only
ever produce a real, reported error. The previous budget is saved and
restored, so one evaluation cannot starve the next.

5 seconds, sampled every 4096 instructions. Generous deliberately: a real
document is a few hundred CSG ops and finishes in milliseconds, so
anything still running is a runaway rather than a slow model.

Result: the same script now returns "script time budget exceeded" in 5.2s.

Three tests. The runaway case is #[ignore]d because it burns the whole
budget by design; the other two guard the ways a timeout typically breaks
things -- that a normal script is unaffected across repeated evaluations
(catching a budget that is not reset), and that a syntax error still
reports itself rather than being misattributed to the timeout.

CI gate added and negative-tested: losing the budget line reintroduces a
hang that no test failure announces, only a frozen app.

617 lib + 154 integration, 0 failed.
2026-07-28 19:26:26 +00:00
Arena Agent
0405067bd5 fix(cad): properties panel and Extend tool never wrote anything (Phase 4.3)
Collapsing the nine duplicated properties-panel blocks exposed that all
nine were silent no-ops, and so was the Extend tool.

CadNode::pos/rot/size return Vec3f BY VALUE. `p.pos().x = val` therefore
assigns to a temporary and discards it. It compiles, warns about nothing,
and produces a control that does nothing: type a position into the panel
and the part does not move. Confirmed with a standalone repro before
touching anything. Eleven call sites, all wrong the same way:

- the nine position/size/rotation inputs in workspace.rs
- viewport.rs Extend, which carefully computes new_w/new_h and throws
  both away, so the tool has never extended a part

Fixed by reading into a local, mutating it and calling set_pos/set_rot/
set_size, which have existed in cad_scene.rs the whole time.

The duplication is what hid this. Nine copies of a 25-line block, each
differing in one token, is not a thing anyone reads closely. Phase 4.3
was scheduled as tidying; it turned out to be the only reason the bug
was found. The blocks now call one helper,
apply_field_to_selected_part(cx, value, set), which also fixes the other
hazard of the copy-paste: each block had to remember four separate
invalidation steps (part_geoms.remove, invalidate_node, mark_scene_dirty,
script_dirty), and missing the first one fails silently -- the part keeps
rendering its old shape until something else evicts it. part_geoms.remove
call sites in workspace.rs: 13 -> 5.

Three defences, each negative-tested by reintroducing the defect:

- cad_scene::size_tests::setters_write_through_but_getters_are_copies
  asserts both halves: that writing through the getter does NOT reach the
  node, and that the set_* methods do. It documents the trap rather than
  just guarding against it.
- workspace::properties_panel_setter_tests applies all nine closures
  exactly as the call sites do and asserts each reaches its own axis and
  leaves the other two groups alone -- so a wrong-axis copy-paste fails
  too. Verified: restoring one broken closure fails with
  "size.y: setter did not reach the node (got 1)".
- A CI grep for `.pos()/.rot()/.size().[xyz] =` outside comments.
  Verified it fires on the restored Extend defect.

610 lib + 154 integration, 0 failed.
2026-07-28 18:29:26 +00:00
Arena Agent
0a8d5b5abb test(cad): move the integration suite out of src/ (Phase 4.7)
src/.../cad/tests.rs -> tests/cad_integration.rs. It was 2,840 lines of
integration tests living inside the library, compiled into every `--lib`
build and able to reach anything in the crate.

The move is worth more than the line count suggests, because a test
target compiles as a separate crate and so sees only the public API. That
turned an invisible question into a compile error: six items would have
needed `pub` for the file to build in its new home —
cad_mesh_data_from_solid, part_mesh_buffers, CommandBorrows,
CadRenderMode, DrawingState, SnapSettings, plus the private fields of the
last two.

Visibility was not widened. Promoting editor internals to a public
contract so a file can sit in a different directory is the wrong trade,
and `pub` is far harder to take back than to grant. Instead the 16 tests
that reach those items moved to where the items live:

- viewport.rs gains `mod viewport_helper_tests` (10 tests: the mesh
  producers and the plain-data helpers).
- mod.rs gains `mod editor_state_tests` (6 tests: SnapSettings polar
  defaults, DrawingState beam sections, CadEditorActivePane).

Two of the relocated tests are worthless and are now visible as such:
cad_render_mode_variants and command_borrows_fields_accessible assert
that a type exists and derives Debug, which the compiler already
guarantees. Left in place rather than deleted in the same commit as a
move; they are for the Phase 6 sweep.

The rule and its rationale are now written down in ARCHITECTURE.md under
"Where a test goes", alongside corrected LOC figures for the six files
that changed size since the table was written.

CI gained a step. The existing job ran only `--lib`, which does not build
a test target, so all 154 relocated tests would have run in no pipeline.
The step names cad_integration explicitly instead of testing the whole
crate, because tests/cost_estimator.rs and tests/cost_estimator_ui.rs do
not compile — pre-existing upstream breakage, verified against a clean
checkout, documented in a comment with instructions to fold them in once
fixed.

Test names diffed before and after: 0 lost, 12 gained (the Phase 4.4
characterization tests). 608 lib + 154 integration = 762.
2026-07-28 18:29:26 +00:00
Arena Agent
b5471e32e3 fix(cad): make the crate buildable, testable and safe to ship
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (push) Has been cancelled
Phases 0-2 of CAD_ASSESSMENT_AND_PLAN.md. The crate did not compile and no
test had ever run; it now builds clean with a green suite.

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

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

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

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

Add system_prompt.md and embed it with include_str!. The file was missing
from the repository, so the agent silently used a one-line fallback.
2026-07-28 16:49:30 +00:00