This commit is contained in:
@@ -0,0 +1,776 @@
|
||||
# AGENTS.md — VernierCAD
|
||||
|
||||
Parametric 3D CAD. Rust, OCCT kernel via `cxx`, wgpu render, egui shell, Linux-native.
|
||||
|
||||
Product name is **VernierCAD**; crate prefix, binary, and repo are plain `vernier`. Use
|
||||
"VernierCAD" in user-facing strings, docs, and the window title. Use `vernier` everywhere
|
||||
in code.
|
||||
|
||||
Read `PROJECT_BRIEF.md` before your first substantial change. Read `MISTAKES.md` before
|
||||
debugging anything that smells familiar.
|
||||
|
||||
**Sequencing is the plan of 2026-09-06, not the phase table below.**
|
||||
`docs/PLAN_2026-09-06_shapr3d-competitor.md` supersedes the phases for *what to build next*;
|
||||
the phase record that follows stands as the account of what was built and why. Three things
|
||||
it changes. **Multi-body is IN scope, as milestone M5** — the brief's "Phase 8+, if ever"
|
||||
line is amended for multi-body only; assemblies, joints, 2D drawings and rendering/materials
|
||||
stay out, as do CAM, simulation, sheet metal and cloud. **The acceptance instrument for every
|
||||
milestone is the driven-GUI harness** (`vernier-drive`, milestone M1): agents drive the real
|
||||
shell headlessly through scripted egui input and assert the *exported geometry*, because
|
||||
nothing on this machine can capture a real window — external screen capture returns
|
||||
transparent frames on this Wayland session, so "we looked at it" is not available as
|
||||
evidence and a script is the only honest substitute. **Four new crates are approved** and
|
||||
need no further ask: `rfd` (native file dialogs), `directories` (XDG paths), `image` (PNG
|
||||
only, harness goldens) and `rayon` (parallel tessellation from M5). Anything else is still a
|
||||
separate discussion. Finally, `docs/GAP_MAP_2026-09-06.md` and its matrix/findings companions
|
||||
are the authority on what EXISTS / is WIRED / is COVERED as of that date — they were verified
|
||||
against the code line by line, which the prose below has not been since it was written; where
|
||||
the two disagree about the present tense, the gap map wins and the prose is stale.
|
||||
|
||||
**M0 (foundation) landed 2026-09-06; M1 (the driven-GUI harness) landed the same day except
|
||||
its L1 script.** The five monoliths are module trees now — `vernier-app/src/{app,camera,edit,
|
||||
graphics,headless,picking,preview,scene,server,sessions,reachability}.rs` + `tests/`, `compile/`,
|
||||
`command/`, `selftest/`, `shell/` — every move mechanically verified as a pure move, and
|
||||
`vernier-app` is a **library plus a 12-line bin** so a harness can link the app core.
|
||||
`reachability.rs` lists every app source file in `SOURCES` with a `Role` and fails by name when
|
||||
a module is added without a row; a command reached only from a test is still unreachable.
|
||||
**The GUI persists the naming store**: `Save` writes the document and a `<document>.names`
|
||||
sidecar atomically (temp + rename beside the destination, the previous sidecar kept as `.bak`
|
||||
until both names land), `Open` loads both and refuses a mismatched or missing store loudly, and
|
||||
"open with fresh identities" is a separate explicit command, never a fallback — the
|
||||
`store-sidecar-roundtrip` selftest check (the 25th) pins it. **Cut-extrude is a chip** on the
|
||||
extrude card (`Edit::ExtrudeSketch { cut }`), and a walk that yields more than one solid raises
|
||||
`CompileWarning::MultipleSolids` until M5 gives bodies names. A cut whose tool is clear of the
|
||||
body raises `CompileWarning::CutRemovesNothing` — OCCT calls that boolean a success and
|
||||
returns the body unchanged, so the timeline otherwise gains a row labelled "cut" for a pocket
|
||||
that does not exist; both volumes are already in `FeatureEvaluation::summary`, so the check
|
||||
costs no kernel call. **One `Deflection` type**
|
||||
(`vernier_kernel::evaluate::Deflection`, default 0.1 mm) replaced the app's 0.1 and the CLI's
|
||||
0.2 bare floats, section curves included; eight selftest artefact digests moved once,
|
||||
deliberately, and the table is in that commit. **`vernier-cli --bench` is the first wall-clock
|
||||
number this project has had**: release, this machine — a full build costs 31 ms / 527 ms / 9.7 s
|
||||
at 10 / 50 / 200 features, which is roughly O(N²) in feature count on a fixture whose topology
|
||||
does not grow, while a steady tail edit costs 4.8 / 20 / 143 ms (the M6 memo, now measured);
|
||||
`solve_sketch`'s `analyze()` costs ~20× the solve it wraps and runs on every compile. CI asserts
|
||||
one wall-clock ceiling and one memo ratio, nothing finer. **Every UI test that drives egui goes
|
||||
through `vernier_ui::testing::driven`** (bounded, panic and hang distinguished), the app crate's
|
||||
push/pull drive included. **`vernier-drive`** is a new crate: `Headless` in `vernier-app` runs
|
||||
the real shell and document server on scripted egui input with one shared offscreen wgpu device
|
||||
(`vernier_render::Offscreen`), frames digested per index at a fixed 1/60 s clock, picks through
|
||||
the ID buffer, expectations on selection, timeline, document JSON paths and *exported geometry*,
|
||||
PNG goldens as a coarse backstop with an absolute hot-pixel budget, and a **cross-process**
|
||||
double run as the determinism check (two `Headless` in one process share one hash seed and
|
||||
cannot see invariant #1's canonical failure). `scripts/drive/m1-smoke.json` is the first gate
|
||||
and `scripts/check.fish` runs it after `--selftest` — a **recorded deviation from invariant #7**
|
||||
(a second headless entry point), taken because the drive needs a Vulkan adapter and a GPU
|
||||
check inside the CLI gate would fail every machine without an ICD. Of the two app bugs the
|
||||
harness found on its first day, ~~a click on an inert egui region (a caption, a panel heading)
|
||||
is not consumed by the release and reaches `pick`, selecting whatever face is behind the
|
||||
panel~~ is **FIXED** (the press's verdict is recorded per button and `camera::maus_route` is
|
||||
the one rule the app and the harness share); the status readout staying empty after a
|
||||
committed push/pull is untouched and still an open card. The M0/M1 adversarial review has
|
||||
been RUN; its findings are landing lane by lane, and the three that live in
|
||||
`vernier-app/src/server.rs` (a save during a failed compile losing the "saved to" readout,
|
||||
undo on a freshly opened document marking it unsaved, the missing-sidecar refusal promising a
|
||||
fresh-identity open "will build") are still open.
|
||||
|
||||
**M1 closed later the same day.** `scripts/drive/l1-bike-light-mount.json` drives the bike-light
|
||||
mount through the real shell (99 steps: clamp opened from a fixture because a circle's radius
|
||||
cannot yet be typed — see the gaps below — arm drawn as a swung parallelogram, drafted, a
|
||||
platform sketched on the drafted face, the clamp edited underneath it) and asserts two analytic
|
||||
deltas plus the drafted face's `Generated` key surviving the edit; its negative control fails at
|
||||
its own platform step with exit 1 and nothing else. The driver gained positional document
|
||||
pointers (`/feature:<n>/`, because `state.features` is a JSON object whose keys are allocator
|
||||
ids), `volume_delta_from`, `expect_no_warnings`, a `"document": "empty"` start, and a timeline
|
||||
target filtered by the panel's own band (the tab strip's `sketch` label sits in the same
|
||||
column). **The L1 gate moved once, deliberately**: M3-A's auto-constraints square a freehand
|
||||
parallelogram drawn 0.63° off perpendicular, so the arm total went from 11439.407 to 11439.866
|
||||
and the script now asserts the three inferred constraints by value rather than pretending the
|
||||
clicks alone fix the shape. **Gaps the L1 pass surfaced, all open:** the value card offers
|
||||
exactly ONE parameter command per selection (`card_for`), so a circle's radius, a point's lock
|
||||
and a two-point distance have no typeable field; the value card and the gizmo handle float over
|
||||
the face being sketched on and silently eat sketch clicks; no gesture names a feature at
|
||||
creation and the rename button can only rename a row to its current name; the driver cannot
|
||||
send Alt, so a script cannot suppress inference. Still unmeasurable here: the RADV-to-lavapipe
|
||||
hot-pixel budget (no lavapipe ICD).
|
||||
|
||||
**M3 (sketching parity) landed 2026-09-07, in a serial prelude plus three parallel lanes.**
|
||||
The sketch canvas went from eight tools to fourteen — `Select`, `Line`, `Circle`, `Arc`,
|
||||
`Rectangle`, `Slot`, `Spline`, `Point`, and now `CentreRectangle`, `Polygon`, `ThreePointArc`,
|
||||
`TangentArc`, `Trim`, `Split` — and every drawing gesture now COMMITS INFERRED CONSTRAINTS
|
||||
(`vernier-solver`'s `infer_*`, at all four commit sites), with Alt suppressing, and with three
|
||||
filters that are the difference between a guess and noise: never what the sketch states
|
||||
verbatim, never a primitive's curve against another curve of the same primitive, and never one
|
||||
whose trial solve makes the sketch `Redundant` — the solver's verdict decides, since a slot
|
||||
stating `Parallel` itself makes a second `Horizontal` a duplicate no value comparison can see.
|
||||
Sketch points and curves DRAG against the solver (a continuation drag with an applied echo, so
|
||||
a locked rectangle does not move and a free point tracks). The **Project tool** pulls a face
|
||||
centroid, an edge's ends or an edge's centre into the sketch as references, `ProjectEdgeEnds`
|
||||
composing three commands behind one gesture; **construction geometry** is a per-curve flag with
|
||||
a `┄` tile and a dashed drawing, refused as a profile by name; **trim and split** cut a curve at
|
||||
a click, with the branch table's own refusals (a click on a curve's own end is refused rather
|
||||
than silently duplicating the shared corner); **dimensions** gained arc radius, a `⌀ diameter`
|
||||
reading, `RemoveConstraint`, and CLICK-THE-NUMBER — every `Distance`/`Radius`/`Angle` constraint
|
||||
paints its value in the viewport and clicking it opens a card that edits it in place; **sketch
|
||||
pattern** repeats picked curves linearly or circularly; and **construction planes** landed as
|
||||
`SketchPlane::Offset` and `SketchPlane::Angled` (a hinge of two sketch points plus an angle),
|
||||
with `RePlaneSketch` moving an existing sketch onto either. `SketchData.plane` has carried a
|
||||
`SketchPlane` since 2026-09-02; what M3 adds is that a GESTURE chooses it — the base plane
|
||||
buttons, the offset and angle steppers, and two re-plane rows — so the variants stopped being
|
||||
document-layer-only. Document format **24**. The three repoints Lane C
|
||||
wired (`SetLoftSections`, `SetSweepOrientation`, `SetSweepGuide`) were the last entries of
|
||||
`reachability.rs`'s `NOT_YET`, so **the built-but-unreachable list is EMPTY for the first time
|
||||
since it was written** and its expiry machinery (`Blocker`, the UI-enum reader, the prose pin)
|
||||
is deleted; what remains is the guard — `SOURCES`, both directory walks, and `Role::Commands`
|
||||
over `vernier-doc/src/command/`, which counts a command composed inside another command's
|
||||
`apply()` as reached — **transitively, since the post-merge review**: an `impl Command for X`
|
||||
block's body is credited only once `X` is itself reached, iterated to a fixpoint, because the
|
||||
first version chained that text in unconditionally and so credited one hop as a chain (a leaf
|
||||
composed only inside an unreached command, or two commands constructing each other, scored
|
||||
reachable with zero app dispatch). `--selftest` holds **41 checks**, in lane order A · B · C,
|
||||
with `enclosure-shell-volume` appended after them (the report digest moved once, deliberately;
|
||||
nothing stores it).
|
||||
**The L2 gate is now DRIVEN, not only headless** (`scripts/drive/l2-enclosure.json` + its
|
||||
negative control): the enclosure is built through the real shell from an EMPTY document —
|
||||
centre rectangle, locked centre, typed dimensions, an inferred `Horizontal`, a trim, the
|
||||
construction toggle, a sketch created on a FACE and re-planed onto an offset plane by the
|
||||
ribbon's stepper, a CUT extrude through the `cut` chip, a linear pattern, and a dimension
|
||||
edited by clicking its number, which propagates through the cut and all four instances. Its
|
||||
volumes are analytic except the shelled body, which has no closed form and takes the oracle's
|
||||
own plausibility band plus exact face and solid counts, with every later operation asserted as
|
||||
an analytic delta from that export. **Those deltas are differences and cancel any drift in the
|
||||
shell exactly**, so the band alone would have let a kernel change move that body by up to the
|
||||
final STL's ±222 mm³ in silence — which is what the new `enclosure-shell-volume` selftest check
|
||||
is for: it builds box → draft → shell headlessly and pins 11591.38578318365 at 1e-9, in one
|
||||
place, with the two analytic inputs asserted first so a failure says which step moved.
|
||||
**Recorded M3 deviations.** A click within `DRAW_SNAP_MM` (2 mm) reuses the existing point, so
|
||||
`Coincident` and `Concentric` are not what a drawing gesture demonstrates — the draw snap
|
||||
subsumes the 0.5 mm inference tolerance, and two of lane A's own tests were passing on a
|
||||
constraint that made the sketch `Redundant`. The drag echo is cleared on every refusal path in
|
||||
`handle`, not in the arm, because a step the COMMAND accepts and the COMPILE refuses is rolled
|
||||
back there. A cut click that lands on a point MARK (±1 mm of model space, arms reaching past
|
||||
the corner) used to resolve to the point and be dropped in silence, so the refusal written for a
|
||||
click on a corner was unreachable for exactly those clicks; `cut::curve_at` leaves the marks out
|
||||
of a CUT pick only. The hover preview is answered as `Ask`/`Clear`/`Keep` with the armed tool
|
||||
inside the question, because arming Select raises no pointer event and the trim cross went on
|
||||
being painted under a tool that picks. Dimension labels are painted for exactly ONE sketch — the
|
||||
one being drawn into, or the one selected in the timeline — because a label is an interactive
|
||||
target that takes the click before the viewport sees it. The re-plane gestures ride ribbon
|
||||
STEPPERS rather than value-card params, because `card_for` would float an offset field over an
|
||||
empty viewport. There is no `show_datums` toggle yet, and a datum resolves a click only where
|
||||
the ID buffer has no face.
|
||||
**Five gaps the driven gate found, none of them fixed here.** (1) **A circular bore cannot be
|
||||
cut BY THE NATURAL GESTURE**, in two independent places — and this claim was **overstated when
|
||||
first written and corrected at the M3 post-merge review**, where the first version said "cannot
|
||||
be cut through the GUI at all". That is false: `toolbar::actions_for` offers `Action::Extrude`
|
||||
for `Selection::Circle`, `edit::edit_for` has an explicit picked-circle fallback carrying
|
||||
`cut: shell.extrude_cut`, and the registry's `needs` string for extrude reads "close a profile,
|
||||
or pick a circle" (all three pinned by `the_cut_chip_reaches_the_extrude_dispatch` in
|
||||
`vernier-app/src/tests/m0_store_cut.rs`). What is blocked is the gesture: the `cut` chip is
|
||||
painted only on the EXTRUDE card and a picked circle floats the RADIUS card instead
|
||||
(`card_for` prefers a `Surface::Value` row), so the chip cannot be toggled while a circle is
|
||||
picked; and a lone circle never becomes a `closed_profile` at all, because
|
||||
`DocumentServer::extend_chain` returns early for a curve with no endpoints — even though
|
||||
`vernier_doc::validate_profile` accepts a lone circle as closed. The L2 gate's bolt holes are
|
||||
square pockets drawn as closed chains for that reason. (2) **`ShellState::extrude_cut` is a
|
||||
sticky session mode** with no state visible outside its own card: written only by the chip,
|
||||
initialised `false` once, cleared by nothing after `Edit::ExtrudeSketch` commits — so every
|
||||
later extrude in that session is silently a cut, and that is also the route by which gap (1)
|
||||
*can* be reached. (3) **A trim leaves its old endpoint behind** as a free point of the sketch and
|
||||
no gesture deletes a point, so reaching zero degrees of freedom costs a lock on geometry that
|
||||
draws nothing. (4) **A dimension label takes a viewport click before the sketch does** (measured:
|
||||
a click on one edge selected the neighbouring edge's label). (5) **The ribbon is one non-wrapping
|
||||
row and overflows**: the four committed M3 drive scripts run at 1600 × 1000 and four of them are
|
||||
red there, but *not for one reason and not at one width* — measured, `m3-b-construction` and
|
||||
`m3-c-planes` go green at 2600, `m3-c-sketch-pattern` needs 3200, and **`m3-b-project` does not
|
||||
recover at any width**: at 3800 it reaches step 23 and fails with "the frame paints 2 copies of
|
||||
'project ends'", because its `$`-command-bar workaround becomes an ambiguous `text:` match once
|
||||
the ribbon button is painted. So three are width-bound and the fourth has a second, independent
|
||||
defect that a width bump does not fix. At 2400 the sketch workspace clips its last four
|
||||
constraint tiles — lock, unlock, construction, remove — which is why the L2 gate first ran at 2600.
|
||||
**Retired 2026-09-07 (ribbon collapse):** the ribbon now measures its row and collapses groups
|
||||
from the RIGHT into chevron buttons whose popup paints that group's rows (fixed 66 px band, one
|
||||
rule per frame, no hysteresis; the popup closes when a command is taken or the session moves,
|
||||
so a builder's next pick still reaches the viewport); the sketch tool row does the same; and a
|
||||
drive script keeps saying `ribbon:<label>`, the driver opening the collapsed group itself.
|
||||
Every drive script runs at 1600 × 1000 now, none was widened, and the measurers of every
|
||||
painter are pinned against what they paint (a `POLYGON_GAP` of 18 that painted 12 was found by
|
||||
that test). **The post-merge lanes closed the L1 gaps the same day:** a ribbon press on a
|
||||
command that has a card of its own AIMS the card (Enter runs it; a card the sketch withholds
|
||||
still dispatches), so a circle's radius, a point's lock, a two-point distance and a draft angle
|
||||
are typeable; the picked face's card and gizmo are withheld while a sketch is open, so sketch
|
||||
clicks under them draw; features can be named through the card's text field and new ones take
|
||||
counted defaults (`sketch 2`, `extrude 2`), with a rename to the current name refused rather
|
||||
than recorded; a sketch point's row shows its SOLVED coordinate (the server keeps its last
|
||||
`Compiled`); the `$` bar's Enter on an empty query runs nothing; the two hover steps are one
|
||||
function. Still open from that pass: a lone circle never becomes a `closed_profile`
|
||||
(`extend_chain` returns early for a curve with no endpoints), `extrude_cut` is a sticky session
|
||||
mode with no indicator, a drive script cannot send a modifier-reading shortcut (egui reads
|
||||
`ModifiersChanged`, which the driver never emits), and a number card's Enter on an untouched
|
||||
field still dispatches the unchanged value.
|
||||
**One spec step the L2 gate does not carry**: §8's `⌀ diameter` chip click and its readout,
|
||||
which follows from the square pockets (the chip is painted for `Action::EditDimension`, and a
|
||||
length dimension on a rectangle edge is not a radius). It is not a gate hole —
|
||||
`scripts/drive/m3-c-dimensions.json` clicks `chip:⌀ diameter` and asserts the geometry 12 typed
|
||||
as a diameter produces — but the L2 gate does not exercise it.
|
||||
|
||||
**M4 and M5 landed partially on 2026-09-07, in a close-out pass, and what did NOT land was cut
|
||||
rather than left floating.** Three interrupted lanes were finished to green and merged: **M4-R**
|
||||
(edge and vertex picking with per-compile ids that are never persisted, hover off the ID buffer
|
||||
already rendered, `Selection::Edges/EdgeChain/Vertices`, and the section cutaway with an offset
|
||||
plane), **M4-K** (the kernel and façade half of direct modelling: per-face offsets, defeaturing,
|
||||
face splitting by imprinted edges, and the general transforms, each written against a K0 probe
|
||||
number rather than a guess), and **M5-1** (the multi-body compile core: `BodyId` from the document
|
||||
allocator, `FeatureTarget` replacing `cut: bool`, the `Boolean` feature, `Compiled.bodies` with
|
||||
`body_of_face`, body-scoped resolution with `BrokenReason::BodyGone`, and document format 27 on a
|
||||
two-step migration ladder with naming store v5). **The gate is 1306 tests, 44 selftest checks and
|
||||
18 drive scripts, both negative controls failing at their own step.** What is NOT here: M4's
|
||||
document glue, gestures and history tools (lanes D, G, H), M5's scene, items panel and gesture
|
||||
wiring (lanes 2 and 3), and the L3 and L4 ladder gates. The `sole_body()` shims are deliberate
|
||||
and refuse ambiguity by name rather than guessing a body. Everything the close-out removed or left
|
||||
owed is listed in the session's follow-up notes, and one item is worth repeating here because it
|
||||
was a defect on main rather than any lane's: **`SilencedMessenger` mutated a process-global OCCT
|
||||
singleton with no lock**, which corrupted the heap in three of six workspace runs and could make a
|
||||
STEP export silently write no file; it is mutex-guarded now.
|
||||
|
||||
**M2 (naming completeness) is complete as of 2026-09-07; M2-1/2 and M2-6/7 in brief, M2-3/4/5
|
||||
below.** `TopoRef` exists as a record in the naming store keyed (holder feature, picked id);
|
||||
payloads keep the bare id; fingerprints are persisted (store v3) and `OwnerTable::Live` reads the
|
||||
owner slot's live set from this compile, so the m1-profile-extrude and m2-revolve gates now break
|
||||
as `Dormant` and go red under the prescribed mutations (every computed owner table → Absent:
|
||||
m1, m2 AND m4-loft rebind onto lookalikes; the M1 bait scores 0.752, not NAMING.md's
|
||||
hand-arithmetic 0.885 — adjacency is 1/3 on that geometry). Score weights were CALIBRATED and
|
||||
left unchanged, measured: every grid set met the criteria, so the criteria did not choose and the
|
||||
recorded baits favoured production (`m2-score-landscape`, a pinned JSON landscape). `FaceRecord`
|
||||
carries a `SurfaceAxis` (axis, radius or semi-angle, and the material side as an `Option` —
|
||||
`None` for a surface of revolution, whose side nobody can measure); a vertex survey ranks
|
||||
`VertexKey{faces, sibling}` by position and never fails an evaluation; one edge membership rule
|
||||
and one comparator serve both `shape_edges` and `fillet_edge`, seams refused by name.
|
||||
`EdgeRef { faces in pick order, sibling }` and `VertexRef` are the document's spellings
|
||||
(format 25: `Fillet.edges: Vec<EdgeRef>`, projections carry `edge: EdgeRef`, no migration from
|
||||
the pair form), resolved through face records plus an edge snapshot (store v4, `edge_refs`) that
|
||||
refuses by reason when the pair's shared count changed, ranks tie, or the geometry matches a
|
||||
sibling rank better — a rank is not a proof; a fillet repoint expands its pairs against the body
|
||||
the fillet is APPLIED TO through `compile_document_observing`. **The naming store migrates from
|
||||
v3 up** (the M4/M5 decision record reversed refuse-never-translate for the store; older is
|
||||
stepped forward in its own shape, newer or below v3 refused by name). The hole point cap is
|
||||
**62**, measured from the u8 slot stride, refused at command, load and compile. The store
|
||||
prunes ON SAVE (`to_json_for`: feature-present tables, holder-still-names-it face and edge
|
||||
records, slots never pruned by count) and keeps everything in memory for §6 undo/redo; a pruned
|
||||
owner breaks `OwnerGone` after reload (`m2-store-prune`, mutation-verified twice). **Bench
|
||||
re-take, 2026-09-07, release:** a full 50-feature build fell 527 → 96 ms and a steady tail edit
|
||||
20 → 2 ms, because Option C unification merges the bench fixture's stacked coplanar caps into one
|
||||
lateral face (its triangle count is now 124 at every size instead of growing) — the O(N²)
|
||||
reading of 2026-09-06 was partly the un-unified face count; `analyze()` is unchanged at 14.5 ms.
|
||||
Owed: `VertexRef` carries no snapshot yet (a vertex crossing another of its set re-ranks
|
||||
silently, stated); no delete command exists, so OwnerGone-after-prune is reached in tests by
|
||||
forging the document text until M4's history tools land.
|
||||
|
||||
**M2-3/4/5 (booleans and seams) landed 2026-09-06.** Every production boolean runs
|
||||
`simplify: true` under Option C — a target face that absorbs tool faces of the same
|
||||
operation keeps its id and the absorbed tool faces are `Merged` into it; two target faces in
|
||||
one output stay `Merged{parents}` (NAMING.md §4 tier 3) — at a fuzzy of 1e-7 × the target's
|
||||
bounding-box diagonal, both bounds measured (δ* = 5e-7 mm is the smallest gap OCCT leaves
|
||||
open; `Bnd_Box` tolerance inflation ~1.6× fuzzy against the split-rebind 1e-6 pin on D; the
|
||||
floor holds for D ≥ 10 mm only, an open §10 question), and `push_pull_face` unifies by
|
||||
construction: 10 → 6 faces on a pushed box with every id kept, `m3-mirror` re-based 14 → 12
|
||||
(its solved mirror plane is tilted ~1.5e-10 rad, so the copy's walls miss the unifier's
|
||||
angular criterion — measured, and not something a fuzzy value reaches), `m1-smoke.json` at
|
||||
faces 6, three selftest checks added (`m2-unify-push-pull`, `m2-flush-wall`,
|
||||
`m2-seam-encoding`; 40 in all once M3 lanes A, B and C's ten landed beside them). Seams have one spelling `(t, t)`; a one-face `(t, 0)` row is
|
||||
an open shell, out of contract and refused; `rank_sibling_keys` is the one ranking rule
|
||||
(lane C1 rebases on it) and the M4-3 probe found no operation that generates a face from a
|
||||
seam edge.
|
||||
|
||||
**Current phase: 5 gate (L2) PASSED 2026-08-09 — headlessly.** The l2-jig selftest check
|
||||
builds an enclosure (box → 2° wall draft → 2 mm shell through the top → linear-patterned
|
||||
bolt holes), then stresses naming: a bolt-radius edit propagates through draft, shell, and
|
||||
every pattern instance with zero identity churn; a pattern shrink keeps survivor ids; the
|
||||
save/load round-trip reproduces the part on a fresh kernel; a lost naming store breaks the
|
||||
face references loudly. All volume asserts are analytic. The Phase 5 *row* items are built to their headless substance, each with exact tests:
|
||||
**measure** (face min-distance via BRepExtrema, planar angles, mass properties — the
|
||||
measure-section selftest check), **sections** (plane × body → exact total length + sampled
|
||||
polylines for the future viewport overlay), **direct push/pull as a timeline feature**
|
||||
(`PushPull` payload + `SetPushPullDistance`; the pushed face keeps its identity on the
|
||||
moved surface — G4 fixture + orchestrator test), **snapping** (`vernier_ui::snap`:
|
||||
coincidence > axis alignment > grid, deterministic tie-breaks), **gizmo math**
|
||||
(`vernier_ui::gizmo`: ray→axis drag parameter, ray→plane intersection, degenerate cases
|
||||
refuse), and the **contextual toolbar model** (`vernier_ui::toolbar`: selection →
|
||||
available actions, only shipped operations offered). **The egui shell is
|
||||
built and runs** (egui is the brief's locked UI decision; egui-wgpu 0.36 resolves to
|
||||
exactly wgpu 30 — no version split): vernier-app hosts a *document server* worker owning
|
||||
the Document, NamingStore, and kernel (invariant #6 — the render thread never touches
|
||||
them), with the timeline panel, contextual toolbar, and measure/section readouts from
|
||||
`vernier_ui::shell` composited over the viewport. Toolbar actions apply real timeline
|
||||
features (push/pull, fillet, chamfer, shell, draft, pattern) with placeholder default
|
||||
parameters; failing edits roll back and report. Smoke-verified on RADV. **Gizmo handle
|
||||
rendering shipped** (commit `da9b371`, 2026-08-09): picking a planar face paints an arrow
|
||||
along its normal with a draggable handle, wired to `vernier_ui::gizmo`'s drag math. What
|
||||
remains for Phase 5 is genuinely user-gated feel work, the dogfooding backlog: snap-tolerance
|
||||
tuning, sRGB framebuffer color tweak. The Phase 5 code
|
||||
(shell/draft/pattern/push-pull/measure/shell UI) also still owes its adversarial review —
|
||||
next session's first item. Recorded Phase 5 deviations: a shelled-away
|
||||
face *continues* as its opening's rim ring (recorded OCCT history, probe-verified — a
|
||||
fillet on the old top edge stays on the outer top edge); patterns repeat extrude features
|
||||
only and cap at 64 instances (per-instance naming slots live in a `u8`).
|
||||
**The shell redesign — landed 2026-08-15**, from the briggen.dev handoff
|
||||
(`Early alpha UI redesign.zip`). Four default-dark egui panels became the designed
|
||||
paper-and-ink shell: title bar 44 · workspace tabs 30 · ribbon 66 · body (262px timeline) ·
|
||||
history strip 58 · status 26, one user-settable accent, vendored Silkscreen / IBM Plex
|
||||
Mono / IBM Plex Sans, 43 pixelarticons, a `$` command bar on ctrl+k, a gizmo-anchored value
|
||||
card, and a settings screen persisting `~/.config/vernier/ui.json`.
|
||||
**The load-bearing part is `registry.rs`.** Every command is declared once — action, label,
|
||||
icon, workspace, group, params, and *why it is off* — and the ribbon, command bar, value
|
||||
card and contextual dimming are four views of that one table. `toolbar.rs` is UNCHANGED and
|
||||
remains the authority on what a selection allows; `readiness_for` delegates to `actions_for`
|
||||
and only decides what to render when the answer is no. Adding a feature is pushing one row
|
||||
in. **Alpha honesty is structural, not a label:** `Dispatch` is `Run(Action)` or
|
||||
`NotYet(&str)`, so a button with no operation has no action to name — "ships a button it
|
||||
cannot press" is unrepresentable. Exactly five are marked: **revolve, sweep, loft** (kernel
|
||||
and document build all three; nothing *creates* one, because their inputs are picked on the
|
||||
sketch canvas) and **join, cut** (there is no boolean feature at all — `BooleanKind` is
|
||||
internal to shell/pattern/cut-extrude merges). The handoff drew `revolve` live beside
|
||||
`extrude`; it is not, and `a_not_yet_command_is_never_ready` is the regression test.
|
||||
Two further handoff-vs-codebase corrections are recorded in the table: **`hole` is offered
|
||||
by picked POINTS, not by a face** (so its group caption is "from points", not "on this
|
||||
face"), and the title bar's commands are outside the selection system entirely, asserted
|
||||
rather than assumed. **Icons are baked by `build.rs`**, not rasterised — every pixelarticon
|
||||
is axis-aligned rectangles on a 24×24 grid, so nonzero winding at pixel centres is exact and
|
||||
no dependency was added; an icon carrying a curve fails the build by name rather than being
|
||||
baked as the polygon through its endpoints. **The four ad-hoc mode colours are gone** (sketch
|
||||
cyan, closed-profile green, unsaved amber, session violet): one accent, and the mode is said
|
||||
in words in an ink pill — which is also what lets the accent become a setting.
|
||||
**The brand marks are ported, not approximated** (`brand.rs`). The first pass shipped a
|
||||
placeholder tile and no mascot, having read the `.dc.html` screens — which import `Logo` and
|
||||
`Nib` as design-tool components — without opening `_ds_bundle.js` beside them, where both
|
||||
sources sit in full. Neither is a picture, which is why no vector existed to look for: the
|
||||
**logo is live Silkscreen type** (`[` `]` at 1.5em in the accent, `nb` between them), so it
|
||||
recolours with the accent and an SVG would have frozen today's amber into it; and **Nib is a
|
||||
16×16 character grid** animated pixel by pixel, one letter per palette entry, which paints
|
||||
exactly like a baked icon. All eight animations are ported and two are wired — idle on an
|
||||
empty document, waving on hover — on the same principle as the icon set, where all 43 are
|
||||
vendored and about 36 are used. One measured correction: the design's `size * 0.34` type
|
||||
ratio is a *browser* Silkscreen measurement and asks for a 29.6px lockup inside a 26px tile
|
||||
under egui's shaping, so `tile_em` honours the ratio wherever it fits and scales it by
|
||||
exactly what does not — clipping a logo would have hidden that, and hard-coding a smaller
|
||||
ratio would have been wrong at another size. Recorded deviations: **rollback is drawn but
|
||||
not wired** (the strip needs an end marker; the caption says so); and the design's own
|
||||
default accent measures **2.97:1 on paper**, just under the 3.0:1 floor it specifies and
|
||||
over it on ink — which is why the Settings readout reports both surfaces rather than a
|
||||
verdict, pinned by `the_shipped_accent_sits_just_under_the_paper_floor_and_over_the_ink_one`.
|
||||
Two egui behaviours are pinned in tests because both fail *silently*: an `Area` paints
|
||||
nothing on its first pass, and `RawInput::default()` has no screen rect — either would make
|
||||
"the card is absent" pass for the wrong reason. Still owed: the six unbuilt settings panes,
|
||||
and an adversarial review of the redesign itself.
|
||||
**M6 — memoized recompute — landed 2026-08-11.** `compile_document` no longer rebuilds the whole
|
||||
timeline for an edit below the top of it. Each entry gets a chained FNV digest of its payload's
|
||||
canonical JSON, so one comparison decides a whole prefix; the compiled state after that prefix —
|
||||
body, sketches, warnings and the verdict chain — is held in the `NamingStore` and resumed from.
|
||||
The verdict chain is restored exactly, not rebuilt, because a face reference below the prefix
|
||||
resolving against a truncated history is invariant #5's failure.
|
||||
**What the memo aims at, and what it costs.** It retains the state just before the first entry
|
||||
whose digest moved, because interactive editing hammers one feature — a dimension drag is the
|
||||
same entry over and over. So the *first* edit of a newly-touched feature still pays full price
|
||||
and leaves the memo pointing below it; every edit after that is cheap. The `recompute-cost` gate
|
||||
asserts the steady state rather than the first edit, and says so, because asserting the first
|
||||
edit is cheap would claim something the design does not do. Measured on a six-extrude stack: a
|
||||
full build and a root edit each mint 11 shapes; a repeated tail edit mints under half that.
|
||||
Mutation-verified — disabling the resume makes the repeated tail edit mint 11, exactly a full
|
||||
build.
|
||||
**The ownership rule, which is the dangerous part.** The walk releases each body as the next
|
||||
feature consumes it, so intermediate shapes normally live one step. The memo keeps one alive
|
||||
across compiles, making it the only place a façade handle outlives its walk. Two failures
|
||||
follow and *neither shows up in a volume assertion*: dropping the handle leaks a registry entry,
|
||||
releasing it while the memo names it leaves a dangling one. Both are contained by single
|
||||
ownership — `NamingStore::retained` names the one shape the walk may not free
|
||||
(`release_superseded` is the only release path for a consumed body), and
|
||||
`NamingStore::discard_prefix` is the only thing that ever frees it. `recompute-cost` gauges live
|
||||
registry entries across four compiles so a leak is a red test. A store loaded from disk carries
|
||||
no memo: the handle would be meaningless in another process.
|
||||
|
||||
**M5 — STEP import/export — landed 2026-08-11.** `export_step` / `import_step` in the façade,
|
||||
`OcctKernel::export_step` / `import_step` above them, and the `m5-step` gate. Round-trip
|
||||
fidelity is exact — the sample part's volume and area come back at relative error 0, face count
|
||||
preserved, units MM by construction (invariant #8).
|
||||
**M5 is the first milestone to touch `build.rs`**, and it needs three OCCT toolkits the earlier
|
||||
milestones did not: `TKDESTEP`, `TKDE`, `TKXSBase`. OCCT 7.9 renamed the data-exchange
|
||||
libraries, so the older `TKSTEP` name in the roadmap does not exist here — the set above is what
|
||||
a linked probe found actually resolves. These are native libraries already installed, not new
|
||||
Rust crates.
|
||||
**Determinism (invariant #1) needed real work and the fix is load-bearing.** OCCT's STEP writer
|
||||
is not deterministic: `FILE_NAME` carries a wall-clock timestamp, pinned through
|
||||
`APIHeaderSection_MakeHeader`, and the `PRODUCT` entity carries a *process-global counter* that
|
||||
no `Interface_Static` parameter reaches (`write.step.product.name` sets the base, not the
|
||||
counter). The façade normalizes that one field out of the written text — counting what it
|
||||
rewrites and returning `kUnexpected` if the pattern is not what it expects, so an OCCT upgrade
|
||||
must break loudly rather than quietly restoring non-determinism no test would notice. Verified
|
||||
by mutation: disabling the normalization fails `m5-step` with "two exports of one shape differ".
|
||||
Recorded M5 deviations: **an imported body is opaque** — a STEP file carries no VernierCAD
|
||||
entity ids and nothing in it is a stable topological identity across two imports, so an import
|
||||
has no parametric history and no feature may reference its faces (invariants #4/#5); it is
|
||||
geometry to look at, measure, tessellate and export again. There is **no timeline feature and no
|
||||
shell wiring** for either direction yet — both are kernel-level capabilities with a headless
|
||||
gate, and import in particular has no honest place in the feature DAG until the opacity question
|
||||
above is answered. And the STEP calls run under a scoped **messenger guard**, because OCCT
|
||||
narrates to stdout and `vernier-cli`'s JSON report lives there (see MISTAKES.md).
|
||||
**M4 — loft — kernel half landed 2026-08-10; the document half is below.**
|
||||
`make_loft` → `evaluate_loft` lofts N ≥ 2 closed sections into a ruled solid, keyed by a new
|
||||
provenance kind: `ProvenanceKey::Bridged { sources: BTreeSet<EntityId>, sibling }`, because a
|
||||
ruled span is bounded by the corresponding curve of *two* adjacent sections and no single
|
||||
curve owns it (NAMING.md's loft amendment). It is **added, never a widening** of `Generated`,
|
||||
and its dormancy is conclusive — its anchors are sketch curves, which resolution never
|
||||
rebinds. Caps take the reserved `LoftStart` (13) / `LoftEnd` (14) roles and, unlike the
|
||||
revolve's, are mandatory. Two measured findings drive the façade and are commented as
|
||||
load-bearing at their call sites: OCCT's `Generated(edge)` is **column**-keyed, not
|
||||
span-keyed (`GeneratedFace` is the span-resolving accessor), and `CheckCompatibility(true)`
|
||||
is what makes the lower/upper pairing knowable at all — with it off a section drawn from a
|
||||
different corner builds a *twisted* valid solid of 2666.67 where the answer is 8000. New
|
||||
façade status **7 `kSelfIntersecting`** (`BRepAlgoAPI_Check`), which catches the class
|
||||
`BRepCheck_Analyzer` calls valid; applied to the loft only for now. Recorded M4 deviations:
|
||||
`evaluate::LoftSection::offset_mm` places each section's Z = 0 sketch geometry at a height —
|
||||
a stopgap until sketch planes land, because every sketch lives at Z = 0 and a loft between
|
||||
two coincident sections is one OCCT refuses outright; `sibling` ships at 0 and is unexercised
|
||||
(the façade fails closed on a collision, and `assemble_source` has no centroid ranking, so a
|
||||
future collision must *lift* the ranking loop out of `assemble_from_history`, not copy it);
|
||||
lofts are ruled only, since a smoothed loft's column is one face spanning every section and
|
||||
adding a section would re-mint all of them.
|
||||
**M4 — sweep — kernel half landed 2026-08-10; the document half is below.**
|
||||
`make_sweep` → `evaluate_sweep` sweeps a closed profile along an open or closed path
|
||||
(`BRepOffsetAPI_MakePipeShell`), and `Bridged` carries it unchanged: a side face is the
|
||||
product of one profile curve and one path segment, so `sources` is that pair — the second
|
||||
consumer is the evidence the key shape was right rather than loft-specific. Caps take the
|
||||
newly declared `SweepStart` (15) / `SweepEnd` (16) roles, optional on the revolve's rule
|
||||
(a **closed** path closes on itself and has none; the two ids go dormant and revive when it
|
||||
opens). Three measured findings are commented as load-bearing at their call sites:
|
||||
`SetTransitionMode(RightCorner)` — the default `Transformed` builds **768** at a 90° corner
|
||||
where the mitre is 1920, and `RoundCorner` adds corner faces belonging to no (profile, path)
|
||||
pair; the attribution is the **unique element of `Generated(profileEdge) ∩
|
||||
Generated(pathEdge)`**, since `MakePipeShell` has no two-argument accessor; and cap tokens are
|
||||
read as `FindIndex`, never `IsNull` (on a closed path both are non-null and neither is a
|
||||
sub-shape of the result — the M2 full-turn situation verbatim). `BRepAlgoAPI_Check` (status 7)
|
||||
now guards the sweep too, and this is where it earns its keep: a circle of radius 5 swept
|
||||
round a bend of radius 2 is a valid solid with the exact Pappus volume of a tube that passes
|
||||
through itself. Recorded sweep deviations: the path is read in the world **XZ** plane and must
|
||||
start at its origin — a stopgap retiring with sketch planes, structural for the revolve axis's
|
||||
reason (a path in the profile's own plane sweeps a valid solid of zero volume, and OCCT
|
||||
otherwise translates the spine to the profile silently); a **full circle is not a legal path**
|
||||
(it has no start point, so OCCT's seam parameterization would decide the placement — drawn as
|
||||
arcs the seam is the sketch's, and the torus is unchanged); the spec's arc-radius pre-check for
|
||||
the self-intersecting pipe is *not* implemented, because it would need a model of OCCT's frame
|
||||
transport and `BRepAlgoAPI_Check` already refuses that class by name; `evaluate_sweep` takes
|
||||
`path_closed` as a parameter rather than deriving it, because closedness is a statement about
|
||||
the chain's point identity that coordinates only corroborate.
|
||||
**M4 — the document and shell half — landed 2026-08-10.** `FeaturePayload::Loft { sections,
|
||||
cut }` (each `LoftSection` naming its own sketch, chain and height) and `Sweep {
|
||||
profile_sketch, profile, path_sketch, path, cut }`, with `AddLoft` / `SetLoftSections` and
|
||||
`AddSweep` / `SetSweepProfile` / `SetSweepPath`, the two orchestrator arms, and the save
|
||||
format at **8** — one bump per payload, since a build that knows lofts and not sweeps is a
|
||||
real generation. `validate_loft` and `validate_sweep` (in `vernier-doc`) state every rule the
|
||||
façade states, in document terms, and both commands *and* the orchestrator run them: a forged
|
||||
save never met a command. The M2/M3 lesson is carried — commands validate **solved** sketch
|
||||
geometry, and the sweep is where that is load-bearing rather than cautionary (where a path
|
||||
starts and whether its first move leaves the profile plane are coordinates the solver moves),
|
||||
so `the_sweep_is_validated_against_the_solved_sketch` skews authored against solved in both
|
||||
directions. The naming store now carries `NAMING_STORE_VERSION` and refuses a version-less
|
||||
store by name: M4 is the first change to the persisted *key vocabulary* since the store was
|
||||
written, so "unknown variant `Bridged`" would otherwise have been the whole diagnosis.
|
||||
Timeline order is enforced in one place, `resolve_input`, which `source_solids` (pattern,
|
||||
mirror) and the two new resolvers share — the M3 mirror-plane bug has one home now instead of
|
||||
three. Recorded deviations of this half: a loft section may name a sketch *later* in the
|
||||
timeline only to be refused (`SetLoftSections` can be handed one, `AddLoft` cannot), while a
|
||||
**sweep** has no such command-level check because its two sketches are fixed when the feature
|
||||
is appended — the orchestrator enforces the order for both, and a doc-side branch nothing can
|
||||
reach was left out rather than written untestable. **Two claims here are RETIRED, corrected
|
||||
2026-09-02 (card c66b4c51).** This entry said no toolbar button creates either feature;
|
||||
`registry::COMMANDS` now dispatches `Action::Revolve`, `Action::Sweep` and `Action::Loft`, plus
|
||||
`Action::AddLoftSection`, so all three are creatable from the ribbon and not only editable. It
|
||||
also said the `m4-loft` / `m4-sweep` gate checks were still owed; both run in the CLI selftest
|
||||
and both pass. What stands is the reasoning behind the original choice: a loft is edited by
|
||||
repointing its sections and a sweep by repointing its chains, and a picked chain is the sketch
|
||||
canvas.
|
||||
**M2 — revolve — landed 2026-08-10.** A closed line/arc chain now sweeps about an in-plane
|
||||
axis through one general-profile revolve (`make_profile_revolve` → `evaluate_revolve`), and
|
||||
it reuses the prism's identity model unchanged: side faces keyed `Generated{sketch curve}`,
|
||||
caps on the reserved `RevolveStart`/`RevolveEnd` roles (NAMING.md §9). What is new is that a
|
||||
revolve's face set is a function of *the angle* and of *where the axis lies*, so two of its
|
||||
keys are absent by construction rather than by death — the substrate had not met that before.
|
||||
**The M2 gate (`m2-revolve`) is what makes the claim falsifiable:** a turned spool (a 30 × 8
|
||||
blank with a half-disc groove of radius 5 at x = 15, revolved about the X axis, which is also
|
||||
the profile's own bottom edge) hits its analytic volume, surface and centroid at both 2π and
|
||||
π/2 — Pappus, valid precisely because the axis lies *in* the profile plane — with the
|
||||
per-curve surface column asserted face by face as (surface class, centroid, area, id), since
|
||||
the two cylinders are area-identical and a total alone would not catch a swap. Both absences
|
||||
are asserted positively: `Generated{L_base}` is never minted (and the orchestrator says so as
|
||||
a `CurveOnAxis` warning), and a full turn mints no cap keys at all, while the
|
||||
2π → π/2 → 2π → π/2 cycle on one store proves the cap ids are minted once, go dormant at the
|
||||
full turn and revive identically. A 30 → 36 lengthening keeps the whole live id set, compared
|
||||
as a set rather than as "the compile succeeded". Two profile swaps break loudly, the bait
|
||||
being a coplanar same-normal annulus at 93.75% of the old area and centroid distance exactly
|
||||
zero (≈0.975 under NAMING.md §5's weights). Verified by mutation: attributing side faces by
|
||||
chain position instead of by curve fails **only** `m2-revolve`; letting the resolver rebind
|
||||
onto the nearest lookalike fails `m2-revolve` **and** `m1-profile-extrude` together, which is
|
||||
correct — both gates pin the same resolver contract, so a gate that stayed green under that
|
||||
mutation would be the broken one. The save format is at version 5. Recorded M2 deviations:
|
||||
**revolves are un-patternable** (`Pattern` repeats extrudes only; pinned by
|
||||
`pattern_refuses_a_revolve_source` and by the toolbar, so it is a refusal rather than a
|
||||
surprise); **a full turn that pinches the axis is refused** — history is complete and correct
|
||||
but the solid is not valid (`BRepCheck_NotConnected`), and an invalid solid propagates into
|
||||
every downstream boolean and into tessellation. The predicate was *measured*, not reasoned:
|
||||
21 profiles probed against OCCT say a full turn is invalid iff the axis-touch set leaves two
|
||||
or more gaps in the ring of side faces and at least one is an isolated point. Both obvious
|
||||
guesses are wrong — a *single* isolated touch builds fine (a cone, a horn torus), and an
|
||||
on-axis curve does **not** rescue a pinch elsewhere — and every partial sweep is valid
|
||||
whatever the profile, right up to 2π − 1e-6, because the caps make a pinch's neighbourhood a
|
||||
disc rather than an annulus. `RevolveError::PinchesAxis` mirrors it at the command boundary
|
||||
and names the point to move; the façade's `BRepCheck_Analyzer` is now the backstop rather
|
||||
than the primary gate. That check is still a **deliberate asymmetry with
|
||||
`make_profile_prism`**, which does not validate its result (adding it could turn an accepted
|
||||
golden red) — but the doc-layer mirror weakens the argument for leaving it, since one could
|
||||
be written for the prism too. Raise it at the next adversarial review rather than quietly
|
||||
closing it; `AXIS_CLEARANCE_MM = 1e-6` and the
|
||||
cap-separation guard are **scale-absolute**, defensible because the `Precision::Confusion()`
|
||||
they track is itself absolute, but they join `fuzzy_mm: 0.0` on the tolerance-calibration
|
||||
item rather than counting as settled; and **`Shell` cannot offset this spool through its
|
||||
toroidal opening** — measured during the gate, `kOperationFailed` at 2.0, 1.0 and 0.5 mm,
|
||||
which is why the gate's plain control is a fillet. That last one is a real capability hole
|
||||
for turned parts with grooves and is worth a look before shell reaches dogfooding.
|
||||
**M1 — general 2D profiles — landed 2026-08-09.** Sketches hold lines, arcs and circles
|
||||
under eleven constraints through the solver bridge, and a closed line/arc chain extrudes
|
||||
through one general-profile prism (`make_profile_prism` → `evaluate_prism`): each side face
|
||||
is keyed `Generated{sketch curve}` and the two caps take the reserved `PrismStart`/`PrismEnd`
|
||||
roles, so a face's identity is a function of *which curve drew it* rather than of
|
||||
enumeration order. **The M1 gate (`m1-profile-extrude`) is what makes that claim
|
||||
falsifiable:** a locked obround (2 lines + 2 arcs) extrudes to its closed-form volume, area
|
||||
and centroid; each of the four side faces is confronted with the table's `Generated{curve}`
|
||||
entry and each cap with its role; a push/pull on one wall survives the slot being lengthened
|
||||
20→25 mm with zero identity churn; and two control documents carry the other half — swapping
|
||||
the extrude onto a chain that drops a curve breaks that curve's face reference *loudly*,
|
||||
including when the new chain offers a coplanar, same-normal wall covering 80% of the old
|
||||
one's area. Verified by mutation: attributing side faces by chain position instead of by
|
||||
curve, and letting the resolver rebind onto the nearest lookalike, each fail **only** this
|
||||
check — l0/l1/l2 pass through both. Both stopgaps are now retired — the rectangular `Box` payload (enclosures
|
||||
are line rectangles; the l2-jig builds one) and the circle→cylinder fast path (one
|
||||
construction, one identity: the same solid authored two ways must not carry two identity
|
||||
schemes, invariant #5). Retiring them cost a one-time id churn for circle extrudes and put
|
||||
the save format at version 4. A circle authored as two half-arcs is accepted: a two-curve
|
||||
loop's two traversal orders build the same wire and the same per-curve keys, so the choice
|
||||
is arbitrary rather than undecidable — `ProfileError::AmbiguousJoin` still fires for three
|
||||
or more curves meeting at one node pair. Recorded M1 deviations: **tangency is deferred, so
|
||||
the gate's profile is locked rather than dimensioned** — an obround with both arc centres on
|
||||
one horizontal axis is mirror-symmetric, and every non-tangency constraint set that
|
||||
expresses it leaves the two `Horizontal` rows linearly dependent (dof 1 + `Redundant`), so
|
||||
the gate drives its edit with `SetSketchLock`; locking an arc's centre *and* both endpoints
|
||||
states its radius twice, which the gate accepts as one honest `Redundant` warning while
|
||||
refusing any under-constrained one; **inner loops are deferred** — one profile is one outer chain, so a washer is still an outer extrude plus a
|
||||
bore cut, and widening later means `profile: Vec<Vec<EntityId>>` plus an outer/inner rule;
|
||||
~~**the sketch plane is still XY at Z = 0** with +Z extrusion (sketch planes on faces are a
|
||||
separate milestone)~~ — **RETIRED 2026-09-02**: `SketchData.plane` carries a `SketchPlane`, one
|
||||
of `World(PrincipalPlane)`, `Offset` or `Face`. How many of those the app can reach is
|
||||
FUSION_LOG's problem to rank, not this record's; and every extrude/pattern merge still passes `fuzzy_mm: 0.0`, which is
|
||||
**deferred with known risk** — a rectangular profile flush with an existing wall is exactly
|
||||
the near-coincident case that tolerance exists for, and line profiles make that geometry
|
||||
ordinary where circles never produced it. Changing the value moves every existing golden, so
|
||||
it is an M2 item with a scale-relative default; there is a pointer beside the zero-volume
|
||||
trap in `MISTAKES.md`. **Two decisions the M1 gate surfaced are owed before the work that
|
||||
makes them live, and both are recorded where they will be found:** the gate's loud break is
|
||||
loud only because `resolve_face_ref` passes `fingerprint: None` — scored by NAMING.md §5's
|
||||
weights the coplanar lookalike lands at ~0.885, over the 0.85 orphan threshold, so the day
|
||||
`TopoRef` fingerprint persistence lands this gate goes red and a reference silently rebinds
|
||||
(NAMING.md §10 has the analysis and a proposal: a key that went dormant in its *owner's own*
|
||||
live table is affirmative death, not an orphan). And `merge_into_body` passes
|
||||
`simplify: false`, so a push/pull leaves caps coplanar-but-not-unified with the body's — the
|
||||
gate's third control shows 6 faces becoming 10. That is correct and deterministic, but it
|
||||
means direct push/pull accumulates coplanar slivers on a real part, each a separately
|
||||
pickable identity. **That deadline has passed, noted 2026-09-02:** the gizmo drag IS wired
|
||||
(`vernier_ui::gizmo::plane_drag_point` in `vernier-app`, handle along the picked face normal),
|
||||
so push/pull is already the common edit and this decision is overdue rather than pending.
|
||||
Phase 4 gate (L1) passed 2026-08-09: the l1-fillet-edit selftest check builds the spacer,
|
||||
fillets its top outer edge (referenced as the adjacent-face identity pair — edges carry no
|
||||
ids), edits the outer radius 10→12, and recompiles: the fillet re-finds its edge, keeps
|
||||
its identity, and regenerates; ghost references break loudly. A sketch edit propagates
|
||||
through a fillet without exploding. ~~Deferred from the Phase 4 row (FUSION_LOG-driven
|
||||
backlog): shell, draft, patterns, mirror, dedicated hole feature; chamfer is implemented
|
||||
via the fillet machinery but owes a fixture. An interactive sketch canvas remains the
|
||||
largest gap between the pipeline and real dogfooding.~~ **ALL RETIRED 2026-09-02 (card
|
||||
c66b4c51)** — the largest stale block in this file. `FeaturePayload` carries `Shell`, `Draft`,
|
||||
`Pattern`, `Mirror` and `Hole`; chamfer has two selftest fixtures of its own (`chamfer-edit`,
|
||||
`chamfer-two-distance-edit`); and the sketch canvas exists, with eight `SketchTool` arms. The
|
||||
largest gap now is not a missing canvas but a missing set of gestures: see FUSION_LOG's
|
||||
reachability section, nine built commands that no gesture reaches.
|
||||
Phase 3/4 adversarial review done 2026-08-09 (17 confirmed findings, all fixed): kernel
|
||||
entity ids now come from the document allocator (the id-space unification the Phase 3
|
||||
deviation owed — a lost naming store breaks references loudly instead of rebinding them),
|
||||
stores are validated against their document at compile entry and reject duplicate tables,
|
||||
degenerate radius dimensions are rejected at the command and the residual, FromEdge sibling
|
||||
ranks are centroid-ordered, compile releases superseded façade shapes, and under-constrained
|
||||
sketches surface a compile warning. Recorded deviations still open: NameTables live in the
|
||||
NamingStore beside the document and are NOT undo-snapshotted (NAMING.md §6 asks for that;
|
||||
append-only tables + dormant-key revival make undo-then-recompile reproduce ids meanwhile);
|
||||
dead-feature tables are never pruned (unbounded but collision-free growth); a store spliced
|
||||
against a *different* document whose allocator happens to sit above the store's ids is
|
||||
caught only by reference resolution, not by an identity handshake (a document identity
|
||||
would need entropy — raise before adding one; determinism invariant #1).
|
||||
Phase 0 (foundation) and Phase 1 (OCCT façade, primitives, transforms, booleans with full
|
||||
history, naming substrate per NAMING.md, tessellation, STL export, viewport with ID-buffer
|
||||
picking) completed 2026-08-08. Phase 1 gate held: box − cylinder renders; a click picks the
|
||||
face (proven headlessly via the ID-buffer readback test). Deliberately deferred from Phase 1:
|
||||
the vendored-OCCT submodule build (the risk table's own advice — do not fight the vendored
|
||||
build during development; system OCCT serves both feature configurations until it lands).
|
||||
If asked to build something from a later milestone of the plan (or from a phase the plan has
|
||||
not reached), say so and confirm before starting.
|
||||
|
||||
---
|
||||
|
||||
## Hard invariants
|
||||
|
||||
Violating any of these is a bug even if tests pass.
|
||||
|
||||
1. **Determinism is mandatory.** Same input → byte-identical output. No `HashMap` iteration
|
||||
where order reaches output; use `IndexMap` or sort. No time, no RNG, no address-dependent
|
||||
ordering, no float accumulation whose order varies with thread scheduling. If you cannot
|
||||
make an operation deterministic, stop and raise it.
|
||||
|
||||
2. **`vernier-occt-sys` is the only crate permitted `unsafe`.** Every other crate carries
|
||||
`#![forbid(unsafe_code)]`. No exceptions without discussion.
|
||||
|
||||
3. **Every kernel operation returns a history delta.** An operation that returns a bare
|
||||
shape is incomplete. See `FeatureEvaluation` and `VerdictMap` in `vernier-kernel`
|
||||
(`OpResult` was the Phase 1 sketch of this and was removed in M0, 2026-09-06).
|
||||
|
||||
4. **Entity identity never derives from index or pointer.** `EntityId` is allocated by
|
||||
`vernier-doc` and mapped through the façade. If you find yourself writing
|
||||
`shape.face(3)` in Rust, you have made a mistake.
|
||||
|
||||
5. **Never silently resolve an ambiguous topological reference.** Below the fingerprint
|
||||
confidence threshold, mark the reference broken and surface it. A wrong-but-plausible
|
||||
face assignment is worse than an error, because the user ships the part.
|
||||
|
||||
6. **The render thread never blocks on the kernel.** Kernel work runs on a worker. The
|
||||
viewport orbits at refresh rate during an eight-second recompute or the design is wrong.
|
||||
|
||||
7. **No feature is finished without a headless test.** If it cannot be exercised through
|
||||
`vernier-cli` with no display, it is not done.
|
||||
|
||||
8. **Units are millimetres, floats are `f64`, angles are radians internally.** Degrees only
|
||||
at the UI boundary.
|
||||
|
||||
9. **Target GPU is AMD RX 7800 XT (RDNA3, Mesa/RADV).** Never write NVIDIA-specific code or
|
||||
suggest CUDA. Validate against Vulkan via RADV.
|
||||
|
||||
10. **Scope is Section 1 of the brief.** No CAM, no simulation, no cloud, no sheet metal.
|
||||
Decline and say why.
|
||||
|
||||
## Crate map
|
||||
|
||||
```
|
||||
vernier-occt-sys/ cxx bridge + facade.cpp [unsafe allowed]
|
||||
vernier-kernel/ OcctKernel, FeatureEvaluation, EntityId, naming substrate
|
||||
vernier-solver/ 2D constraint solver [no kernel dep]
|
||||
vernier-doc/ feature DAG, undo, serde [no kernel dep]
|
||||
vernier-tess/ mesh → GPU arrays + STL [no cache; see the crate doc]
|
||||
vernier-render/ wgpu, ID-buffer picking
|
||||
vernier-ui/ egui shell: theme, icons, chrome, registry, timeline
|
||||
vernier-cli/ headless driver
|
||||
vernier-app/ thin main()
|
||||
```
|
||||
|
||||
Dependencies point downward only. `vernier-solver` and `vernier-doc` importing `vernier-kernel`
|
||||
is a design failure, not a convenience. If a task seems to require it, the abstraction is
|
||||
wrong — raise it rather than adding the dependency.
|
||||
|
||||
## Commands
|
||||
|
||||
Shell is **fish**. No bash heredocs. No `export VAR=x` — use `set -x`. Write files with
|
||||
`printf` or multi-line `echo`, not `cat <<EOF`.
|
||||
|
||||
```fish
|
||||
# fast iteration against system OCCT
|
||||
cargo build --features system-occt
|
||||
cargo run -p vernier-app --features system-occt
|
||||
|
||||
# headless — the primary verification path
|
||||
cargo run -p vernier-cli -- --selftest
|
||||
cargo run -p vernier-cli -- --selftest --json-report /tmp/report.json
|
||||
cargo run -p vernier-cli -- --l0-spacer /tmp/spacer.stl # part + document + .names sidecar
|
||||
cargo run -p vernier-cli -- --export-sample /tmp/out.stl
|
||||
cargo run -p vernier-cli -- --sketch-debug rectangle /tmp/sketch.svg
|
||||
|
||||
# full check, must pass before any commit
|
||||
cargo fmt --check; and cargo clippy --all-targets -- -D warnings; and cargo test --workspace
|
||||
|
||||
# reproducible build (vendored OCCT — lands at the end of Phase 1; until
|
||||
# then this also links the system OCCT)
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
If OCCT headers are somewhere unusual: `set -x DEP_OCCT_ROOT /usr/include/opencascade`
|
||||
|
||||
## Testing
|
||||
|
||||
**Golden-file geometry tests are the backbone.** Every kernel operation gets a fixture
|
||||
under `tests/golden/`. Assert on:
|
||||
|
||||
- topology hash (counts + adjacency signature, stable across runs)
|
||||
- volume, surface area, centroid — `f64`, tolerance `1e-9` relative
|
||||
- history deltas (`VerdictMap`): which entities were generated, modified, deleted
|
||||
|
||||
Not on OCCT internals, not on tessellation vertex counts, not on floating-point equality.
|
||||
|
||||
**Naming tests are adversarial by design.** For every feature that references upstream
|
||||
topology, there is a test that edits the upstream sketch in a way that changes face count,
|
||||
and asserts the reference either survives correctly or is marked broken. Never that it
|
||||
"resolves to something."
|
||||
|
||||
**Render tests** use screenshot diffs via lavapipe in CI. Threshold-based, not exact.
|
||||
|
||||
**Solver tests** use property tests: a fully-constrained sketch has zero DOF and does not
|
||||
move under drag; an over-constrained sketch reports conflict rather than converging to
|
||||
garbage.
|
||||
|
||||
**A test driving UI code through a shared non-reentrant lock must be bounded, never bare.**
|
||||
Spawn the drive on its own thread, gate the result on a `recv_timeout`, and separate a panic
|
||||
(`RecvTimeoutError::Disconnected`) from a genuine hang (`RecvTimeoutError::Timeout`) in the
|
||||
failure message — see `dragging_the_gizmo_does_not_deadlock_the_ui_thread` in
|
||||
`vernier-ui/src/shell/tests.rs`, and MISTAKES.md's 2026-09-02 entry for the incident and the
|
||||
pre-existing tests still owed a migration. The reason is BOUNDING, not diagnosis: a bare
|
||||
synchronous call to such code (egui's `Context` today; any future non-reentrant lock reached
|
||||
from application code) still fails visibly in a debug build, but hangs the calling thread
|
||||
forever in release with nothing to bound it — and because `cargo test`'s default harness shares
|
||||
one thread pool across tests, one hung test wedges the entire invocation with no red and no exit
|
||||
code. The Disconnected/Timeout split does not by itself prove which of panic/hang/deadlock fired
|
||||
(card b7531df4) — only that whichever one did is now finite and visible. Print the elapsed time
|
||||
in the failure message so the reader is not left guessing.
|
||||
|
||||
## Conventions
|
||||
|
||||
- `thiserror` for library errors, `anyhow` only in `vernier-cli` and `vernier-app`
|
||||
- No `unwrap()` or `expect()` outside tests and `main()`
|
||||
- `tracing` for logging, never `println!`
|
||||
- `IndexMap` over `HashMap` anywhere ordering could reach output
|
||||
- Public API on each crate stays narrow — internals `pub(crate)`
|
||||
- Doc comments on every public item, with a `# Panics` section if it can
|
||||
- One logical change per commit, conventional commit messages
|
||||
|
||||
## OCCT bridge rules
|
||||
|
||||
- The C++ façade owns a shape registry; Rust holds opaque `u64` handles
|
||||
- `Handle<T>` never crosses the FFI boundary. Ever.
|
||||
- Every façade function that creates or modifies a shape also returns its history
|
||||
- All OCCT exceptions are caught in C++ and converted to result codes. An OCCT exception
|
||||
crossing into Rust is undefined behaviour and will eventually corrupt something quietly.
|
||||
- `facade.hpp` is the complete API surface. If a new OCCT capability is needed, it gets a
|
||||
new façade function — not a leaked type.
|
||||
|
||||
## Working agreements
|
||||
|
||||
- When an invariant above seems wrong for the task at hand, **say so and stop.** Do not
|
||||
route around it, do not add a local exception, do not `#[allow]` it. The invariants are
|
||||
load-bearing; if one is wrong it needs changing here, not bypassing there.
|
||||
- When a bug costs more than an hour, add an entry to `MISTAKES.md`: symptom, root cause,
|
||||
the wrong hypothesis that wasted the time.
|
||||
- Explain *why* alongside any non-obvious change. Directional effects, not just the diff.
|
||||
- Prefer showing a small example over describing the approach.
|
||||
- Push back on bad instructions. Being agreeable about a design error is not helpful.
|
||||
- Do not add dependencies without asking. The dependency tree is a maintenance liability
|
||||
and this project outlives most crates.
|
||||
- Do not write speculative abstraction for phases that do not exist yet.
|
||||
@@ -0,0 +1,24 @@
|
||||
# Paused for SSD replacement — 2026-09-08
|
||||
|
||||
User explicitly requested that work pause. Do not resume agents or implementation until the user asks.
|
||||
|
||||
Worktree: target/worktrees/reliability
|
||||
Branch: codex/reliability-and-function-verification
|
||||
Last committed checkpoint: f56bb172dc258f85188edf302a81c8a7397c4602
|
||||
Main remains b3f661fac0282dd5d02c5fc632b4343ca9ab18be. No main integration or pending dependency changes were authorized/applied.
|
||||
|
||||
Current changes are UNCOMMITTED. Selected-profile repair passes focused libraries/review and actual Undo/Open workflows, but the combined workspace gate is RED: cut-then-boss step 27 cannot find chip:cut. See retained vernier-profile-final-workspace.log. Do not claim this candidate is accepted or commit it before correcting the regression and rerunning the combined gate. Formatting and workspace/all-targets Clippy pass. Workspace gate exited 101 before release Headless/build/selftest stages.
|
||||
|
||||
Agents were interrupted at the user's pause request. finish_persistence had just been assigned the Cut regression; review_persistence was asked to inspect it independently. drive_solid_builders completed its control review. No new dimension repair has been implemented.
|
||||
|
||||
Next after resume: inspect current source versus this snapshot, correct Cut-card regression without weakening cut-then-boss script; incorporate two point-distance review corrections (notes say Ctrl-pick but second click is plain; add exact authored point3=[10,-0] native assertion). Re-run final combined gate and 9 release cross-process scripts. The planned /tmp/vernier-profile-final-drives.py has not run. Refresh frozen source hash inventory after edits. Commit only a reviewed, verified feature-branch checkpoint with a new exact manifest; old /tmp/vernier-checkpoint.py still guards the prior cba7382 parent and must NOT be reused unchanged.
|
||||
|
||||
Then natural Length/Distance/Angle creation-on-existing-dimension repair from docs/NATURAL_DIMENSION_REPAIR_PLAN_2026-09-08.md. Three actual red probes exist and are included in the pause archive. Signed Angle policy suggested by independent reviewer: reversed picks negate canonical value while preserving original row references; new creation range [-180,180]; existing finite out-of-range targets retain full finite display/edit domain, exact untouched bits, and no implicit normalization. Review/final implementation still needed.
|
||||
|
||||
Full R1-R14 goal remains open; native worker process containment is not implemented. The broad project ledger is docs/IMPLEMENTATION_2026-09-08.md. App serde dependency edges and main integration remain pending prior approval; no need to reask until needed. Both AGENTS.md files are user-owned and must not be edited/staged/deleted.
|
||||
|
||||
The pause archive at project root includes all current modified/new task files, a binary Git diff, branch/HEAD metadata, and selected /tmp evidence. It is a recovery supplement, not a complete repository backup. Copy the entire VernierCAD directory INCLUDING .git and target/worktrees/reliability to the replacement disk before discarding the old disk. The archive itself remains on this disk until copied elsewhere.
|
||||
|
||||
## Explicit resume
|
||||
|
||||
User explicitly resumed on2026-09-08 after SSD replacement. Original HEAD and uncommitted file set verified;24 archived /tmp evidence files recovered. New agents repair_profile_cut and review_profile_cut resumed the Cut regression, with prepare_dimension_controls preparing the next bounded verification plan. The pause instructions above are the historical stop record.
|
||||
@@ -0,0 +1,115 @@
|
||||
# VernierCAD assessment — 8 September 2026
|
||||
|
||||
VernierCAD has a substantial parametric modeling foundation, but several basic user workflows still fail. The immediate priorities are file-command access, protection against accidental exit, and keeping the viewport consistent with the current document. Completing multi-body interaction and direct modeling will then close more of the practical Shapr3D gap than adding more isolated kernel operations.
|
||||
|
||||
Assessed revision: `b3f661f`. This was an exploration and audit; no application source was changed. Findings distinguish executed reproductions, source-confirmed behavior, unfinished integration, and capabilities outside the project's current scope.
|
||||
|
||||
**Verification performed**
|
||||
|
||||
| Check | Result |
|
||||
|---|---|
|
||||
| `cargo test --workspace` | 1,306 passed, 0 failed, 19 ignored across 66 result summaries, including integration tests and doctests |
|
||||
| `vernier-cli --selftest` | All 44 checks passed |
|
||||
| `cargo fmt --all --check` | Passed |
|
||||
| `cargo clippy --workspace --all-targets -- -D warnings` | Passed; generated C++ bridge warnings were still printed |
|
||||
| Additional probes through the real egui shell | Confirmed disabled Export, stale geometry after opening an empty document, fixed-distance sketch snapping, and persistent Cut mode |
|
||||
| Synthetic document probes through the app server | Confirmed multi-body export failure and inert suppression/rollback fields |
|
||||
|
||||
The available adapter was **llvmpipe (LLVM 22.1.8, 256 bits)**. An explicit RADV requirement failed. Additional probes used that named software adapter; screenshots were inspected, but were not compared against RADV goldens. This is not a claim that the full RADV-specific local gate passed, nor a live window/compositor usability test. Existing test success also does not demonstrate the performance or ergonomics of a large interactive model.
|
||||
|
||||
**How the project fits together**
|
||||
|
||||
The document layer owns feature order, sketch geometry, body identifiers, commands, serialization, and undo. The solver handles sketch constraints. A C++ façade over OCCT supplies geometry operations and history; the Rust kernel adds evaluation and topological naming. The compiler in `vernier-ui` turns the document into evaluated bodies and maintains naming records and cached work. A document server in `vernier-app` performs this work off the render thread and returns scene snapshots to the egui/wgpu application. `vernier-drive` runs the same shell with synthetic input and checks document state, rendered frames, and exported geometry.
|
||||
|
||||
This separation is useful and the geometric regression coverage is extensive. Its main weakness is that a capability can be implemented and thoroughly tested below the UI while the user-facing path remains absent or broken. The file-command finding below is a concrete example.
|
||||
|
||||
The current file format is 27 and naming-store format is 5. Multi-body data and compilation are implemented. The README's M0 status, five-tool description, and claim that arcs cannot be dimensioned are stale. Current code has fourteen sketch tools, arc radius constraints, diameter display, construction geometry, trim/split, sketch patterns, edge/vertex picking, and section cutaways.
|
||||
|
||||
**Prioritized issues**
|
||||
|
||||
1. **P1 — Open and Export cannot be activated from the command palette.** Executed for Export STL; the same source path blocks Open, fresh-identity Open, and Export STEP. File commands are registered on the command surface, but palette dispatch requires `readiness_for(...) == Ready`. That function exclusively consults `actions_for(selection)`, whose selection arms never return the file actions. Consequently a valid typed path still produces the misleading message “name a path first,” and Enter does nothing. The title-bar Save button follows a different path and works.
|
||||
|
||||
The probe typed a path, saved through the actual Save button, opened the palette, typed `export stl`, and pressed Enter. The palette stayed open, the status still said “saved to,” and the file was still native JSON. [Palette dispatch](/home/nilsb/Documents/Projects/VernierCAD/crates/vernier-ui/src/shell/ribbon.rs:1987), [readiness](/home/nilsb/Documents/Projects/VernierCAD/crates/vernier-ui/src/registry.rs:1529), [selection actions](/home/nilsb/Documents/Projects/VernierCAD/crates/vernier-ui/src/toolbar.rs:685), [file registrations](/home/nilsb/Documents/Projects/VernierCAD/crates/vernier-ui/src/registry.rs:1079).
|
||||
|
||||
Acceptance: save, open, and export a part through actual controls, including an unsaved-change prompt on Open. Give document commands availability rules based on path, document state, and body selection. Also separate native-document and export destinations: [the current dispatch uses the same path for all file operations](/home/nilsb/Documents/Projects/VernierCAD/crates/vernier-app/src/edit.rs:1763), which will become an overwrite hazard when the palette is fixed.
|
||||
|
||||
2. **P1 — Escape and window close can discard unsaved work.** Source-confirmed. An unconsumed Escape outside sketch mode maps to quit, and the key handler exits the event loop. `CloseRequested` also exits immediately. Neither consults the unsaved state or asks whether to save. Leaving a sketch and pressing Escape again can therefore terminate the app. [Escape mapping](/home/nilsb/Documents/Projects/VernierCAD/crates/vernier-app/src/camera.rs:208), [key exit](/home/nilsb/Documents/Projects/VernierCAD/crates/vernier-app/src/app.rs:1298), [window close](/home/nilsb/Documents/Projects/VernierCAD/crates/vernier-app/src/app.rs:1395).
|
||||
|
||||
Acceptance: Escape cancels the current operation or clears selection; explicit quit and window close offer Save/Discard/Cancel when dirty and account for pending edits. Add recovery/autosave afterward.
|
||||
|
||||
3. **P1 — Opening an empty document leaves the previous solid visible.** Executed. Starting from the default block, the probe opened a valid empty document through the server and waited for completion. The timeline correctly showed zero features, while the viewport still displayed the old block. `apply_scene` replaces `self.part` and picking maps only when the incoming mesh is `Some`; it never clears them when the scene has no mesh. The same issue is exposed by a multi-body scene that cannot produce the app's single-body mesh. [Scene application](/home/nilsb/Documents/Projects/VernierCAD/crates/vernier-app/src/app.rs:818).
|
||||
|
||||
Acceptance: a successful empty scene clears body buffers, identity maps, and obsolete selection state. Define any last-good preview during a failed rebuild explicitly, so geometry from a previous document cannot be mistaken for the current model.
|
||||
|
||||
4. **P1 — Small sketch features are swallowed by a fixed 2 mm snap radius.** Executed. On an empty document at camera distance 20 mm, clicking a line from `(0,0)` to `(1,0)` left no curve and raised no error, including with Alt on the endpoint click. The draw path always uses `DRAW_SNAP_MM = 2.0`; snapping back to the pending point silently discards the endpoint. Zooming does not reduce the radius, and Alt suppresses inference rather than this point snap. [Tolerance](/home/nilsb/Documents/Projects/VernierCAD/crates/vernier-app/src/server/mod.rs:927), [snap and discard](/home/nilsb/Documents/Projects/VernierCAD/crates/vernier-app/src/server/apply_draw.rs:424).
|
||||
|
||||
Acceptance: a visible screen-distance snap threshold, an explicit snap override, and a reproducible 1 mm feature at close zoom. Dimensioning larger geometry afterward is a workaround, but does not make small-feature drawing work.
|
||||
|
||||
5. **P1 for multi-body readiness — Valid multi-body documents cannot be displayed/exported correctly by the app.** Executed with a synthetic document containing two `NewBody` extrudes. It loaded without a compile error and showed three timeline features, but STEP export was refused with “the document has no solid body.” The compiler's useful “2 bodies and none was chosen” error is erased by `.sole_body().ok()`. Scene construction, export, and several measurements still expect exactly one body. Combined with issue 3, the viewport can misleadingly retain the previously displayed block. [Body extraction/export](/home/nilsb/Documents/Projects/VernierCAD/crates/vernier-app/src/server/mod.rs:397), [explicit compiler error](/home/nilsb/Documents/Projects/VernierCAD/crates/vernier-ui/src/compile/mod.rs:880).
|
||||
|
||||
Acceptance: render all visible bodies, pick by body, support explicit operation targets, and export selected/all bodies. Until then, opening a multi-body file should clearly report the unsupported app state.
|
||||
|
||||
6. **P2 — Cut mode persists into subsequent extrusions without an indication outside the extrude card.** Executed. The probe created a radius-5 cut of depth 8 in the starter block, then armed another extrusion and entered depth 12 without choosing Cut again. Both saved feature targets were `Cut`; the second export measured `17057.52220392306 mm³`, matching the deeper bore. Cut is a shell-wide boolean initialized once, with no successful-commit reset. [Cut chip](/home/nilsb/Documents/Projects/VernierCAD/crates/vernier-ui/src/shell/value_card.rs:691), [extrude dispatch](/home/nilsb/Documents/Projects/VernierCAD/crates/vernier-app/src/edit.rs:1207).
|
||||
|
||||
Acceptance: show the operation choice clearly whenever creating an extrusion, with a deliberate rule for defaults and persistence across operations/documents. This is an interaction hazard; the resulting geometry correctly follows the stored Cut choice.
|
||||
|
||||
7. **P2 — A newly drawn circle does not become the active closed profile.** Source-confirmed and visible in the circle probe. `extend_chain` returns for curves without endpoints, including circles. Users must switch to Select, pick the circle, and then arm Extrude. Crucially, **circular cuts are possible today**: arming Extrude now exposes its height and Cut controls. The older claim that the radius card makes a circular cut inaccessible no longer applies. [Circle early return](/home/nilsb/Documents/Projects/VernierCAD/crates/vernier-app/src/server/mod.rs:880), [picked-circle fallback](/home/nilsb/Documents/Projects/VernierCAD/crates/vernier-app/src/edit.rs:1210).
|
||||
|
||||
Acceptance: recognize circles and other closed regions consistently; a drawn closed circle should have the same obvious extrusion path as a closed line chain.
|
||||
|
||||
8. **P2 reliability gap — Native saves are not crash-durable transactions.** Source-confirmed, not fault-injected in this audit. Document and naming store are separate files, installed with multiple renames. Normal write/rename failures have rollback handling, but a crash between renames can leave a mismatched pair, and the files are not fsynced. A backup sidecar can remain for manual recovery; Open does not implement transaction recovery. [Save protocol](/home/nilsb/Documents/Projects/VernierCAD/crates/vernier-app/src/server/apply_files.rs:60).
|
||||
|
||||
Acceptance: a recoverable transaction/container format, durable writes, and recovery tests at each interruption point. Preserve topological identities during recovery.
|
||||
|
||||
**Shapr3D capability comparison**
|
||||
|
||||
This compares available product capabilities, including paid Shapr3D capabilities where applicable. It does not assume that every Shapr3D feature belongs in VernierCAD's scope.
|
||||
|
||||
| Area | VernierCAD today | Gap / next step |
|
||||
|---|---|---|
|
||||
| Parametric sketching | Fourteen tools; solver constraints, inferred relations, dimensions, solver-backed dragging, construction curves, trim/split, offset/mirror/pattern | Fix small-scale snapping and circle closure; add practical geometry deletion, ellipse/text, and broader spline authoring. Shapr3D's [sketch tool catalog](https://support.shapr3d.com/hc/en-us/articles/9760033847964-Shapr3D-Manual) includes these authoring tools. |
|
||||
| Profile selection | Extrusion consumes one circle or one closed chain; profile IDs are a flat vector | Region selection and nested loops/holes in one extrusion are missing; a washer still needs an outer extrusion plus a separate cut. [Profile representation](/home/nilsb/Documents/Projects/VernierCAD/crates/vernier-doc/src/document.rs:496). |
|
||||
| Solid construction | Extrude, revolve, loft, sweep, shell, draft, hole, feature patterns and mirrors exist | Improve selection and parameter workflows; feature presence alone does not establish the L3 organic bracket workflow. |
|
||||
| Edge finishing | Fillet/chamfer via faces and face pairs; real edge/vertex picking exists | Picking an edge or edge chain currently offers **no operations**. Wire edge fillet/chamfer and an edge gizmo. [Empty action branch](/home/nilsb/Documents/Projects/VernierCAD/crates/vernier-ui/src/toolbar.rs:990). |
|
||||
| Direct face editing | Planar push/pull is wired; offset, delete/heal, and face-split kernel operations exist | Curved-face push/pull, face editing commands and UI remain incomplete. Shapr3D [Offset Face](https://support.shapr3d.com/hc/en-us/articles/7874400678428-Offset-Face) supports planar and non-planar faces; [Replace Face](https://support.shapr3d.com/hc/en-us/articles/7874457242268-Replace-Face) is another missing workflow. [Kernel operations](/home/nilsb/Documents/Projects/VernierCAD/crates/vernier-kernel/src/evaluate.rs:3355). |
|
||||
| History editing | Parameter edits, repointing, rename, and in-memory undo/redo | Delete, reorder, suppression, and rollback behavior are unfinished. Serialized suppression/rollback are intentionally inert; the probe still exported the suppressed block at `18000 mm³`. Compare Shapr3D's [History controls](https://support.shapr3d.com/hc/en-us/articles/11567903089180-History). |
|
||||
| Multi-body and organization | Body IDs, boolean/split/import/move commands, body patterns and mirrors exist below the UI | Complete body scene/picking, items panel, visibility/isolate, targeting and export. The [explicit unreachable-command list](/home/nilsb/Documents/Projects/VernierCAD/crates/vernier-app/src/reachability.rs:788) names ten production commands. Compare [Items Manager](https://support.shapr3d.com/hc/en-us/articles/7873936188956-Items-Manager), [Align](https://support.shapr3d.com/hc/en-us/articles/7874398317596-Align), and [Split Body](https://support.shapr3d.com/hc/en-us/articles/7874462683036-Split-Body). |
|
||||
| Variables and units | Parameters are numeric `f64` values; value cards parse plain numbers; units/precision settings are disabled | Named variables, expressions, unit-aware entry, and display-unit preferences are absent. Shapr3D supports [variables and expressions](https://support.shapr3d.com/hc/en-us/articles/18320182069916-Variables-and-expressions). [Numeric parser](/home/nilsb/Documents/Projects/VernierCAD/crates/vernier-ui/src/shell/value_card.rs:784). |
|
||||
| Interchange | Native JSON plus naming sidecar; working STEP/STL export below the UI; STEP body import exists in the document/compiler | First fix actual file-command access and wire STEP import. Then prioritize DXF sketch exchange and 3MF according to use. Shapr3D's [supported formats](https://support.shapr3d.com/hc/en-us/articles/7874523890076-Supported-file-types) are substantially broader. |
|
||||
| File/product workflow | Typed paths and title-bar Save; unsaved marker; Open-discard logic exists | Native dialogs, Ctrl+S, recovery/autosave, saved-state tracking, and usable document management remain missing. Undoing back to saved geometry still leaves the dirty marker. |
|
||||
| Inspection | Geometric measurement, section curves, and section cutaway are implemented | Extend inspection to explicit multiple-body selections and verify workflow usability alongside the items panel. |
|
||||
| Technical drawings | No drawing-sheet subsystem | A major product difference from Shapr3D's [2D Drawings](https://support.shapr3d.com/hc/en-us/articles/7874466328348-2D-Drawings), but excluded from the currently recorded scope. |
|
||||
| Presentation and collaboration | Basic modeling viewport; Linux desktop focus | Materials/visualization, AR, device sync, and browser review are outside the current scope. Shapr3D offers broader presentation and [published-version sharing](https://support.shapr3d.com/hc/en-us/articles/7873807462940-Share-Published-Versions). |
|
||||
|
||||
The scope record explicitly brings multi-body into the core milestone while leaving assemblies/joints, drawings, materials/rendering, and cloud out. This audit identifies those product differences without silently adding them to the implementation backlog.
|
||||
|
||||
**What the green tests do and do not establish**
|
||||
|
||||
The strongest existing checks assert geometry and naming under edits, not merely successful command returns. Keep those. However, driver steps called `save`, `export_stl`, and `export_step` invoke headless file APIs directly. They bypass the palette's readiness logic, which explains why export tests pass while a person cannot activate Export. The [driver dispatch](/home/nilsb/Documents/Projects/VernierCAD/crates/vernier-drive/src/run.rs:492) should be supplemented with actual-control acceptance cases.
|
||||
|
||||
Likewise, source reachability guards establish that command constructors occur in production paths; they do not prove a user can satisfy the selection/card/palette preconditions. Suppression roundtrip tests explicitly preserve an unfinished behavior rather than implementing suppression. [That test states the limitation](/home/nilsb/Documents/Projects/VernierCAD/crates/vernier-ui/src/compile/tests/m4_p0.rs:31).
|
||||
|
||||
The next acceptance cases should cover: actual file controls; dirty close and Escape; opening an empty/sketch-only document after a solid; small sketch geometry at multiple zoom levels; selecting an edge and applying a fillet; and two separately editable bodies saved, reopened, and exported. L3 and L4 remain the appropriate modeling gates after the missing integrations land.
|
||||
|
||||
**Recommended sequence**
|
||||
|
||||
1. Restore trustworthy basic use: palette file actions, destination handling, close/Escape protection, empty-scene clearing, and small-scale snapping.
|
||||
2. Complete multi-body integration from compiler through scene, items, targets, transforms, booleans, and import/export. Make the two-part enclosure an actual user workflow.
|
||||
3. Finish edge-based filleting and face editing together with useful history delete/suppress/rollback and broken-reference recovery.
|
||||
4. Add variables/expressions, unit entry, and the sketch authoring gaps that real parts expose. Improve file dialogs/recovery alongside this work, not after feature parity.
|
||||
5. Reassess drawings and visualization only if the product scope changes.
|
||||
|
||||
**Reproduction artifacts**
|
||||
|
||||
All additional probes and outputs are retained under [the audit artifact directory](/home/nilsb/Documents/Projects/VernierCAD/target/audit-2026-09-08). That directory is ignored by Git and can be removed by a future clean build.
|
||||
|
||||
| Probe | Evidence |
|
||||
|---|---|
|
||||
| Actual Save/Export controls | [Script](/home/nilsb/Documents/Projects/VernierCAD/target/audit-2026-09-08/file-commands-disabled.json), [report](/home/nilsb/Documents/Projects/VernierCAD/target/audit-2026-09-08/file-commands-disabled/report.json), [disabled Export screenshot](/home/nilsb/Documents/Projects/VernierCAD/target/audit-2026-09-08/file-commands-disabled/run1/disabled-export.png) |
|
||||
| Empty document retains old solid | [Script](/home/nilsb/Documents/Projects/VernierCAD/target/audit-2026-09-08/empty-scene.json), [screenshot showing zero features and the old block](/home/nilsb/Documents/Projects/VernierCAD/target/audit-2026-09-08/empty-scene/run1/empty-scene.png) |
|
||||
| 1 mm line with Alt produces no curve | [Script](/home/nilsb/Documents/Projects/VernierCAD/target/audit-2026-09-08/small-line.json), [saved sketch](/home/nilsb/Documents/Projects/VernierCAD/target/audit-2026-09-08/small-line/run1/small.vernier) |
|
||||
| Persistent Cut and usable circle cut card | [Script](/home/nilsb/Documents/Projects/VernierCAD/target/audit-2026-09-08/circle-sticky.json), [saved targets](/home/nilsb/Documents/Projects/VernierCAD/target/audit-2026-09-08/circle-sticky/run1/second-extrude.vernier), [report with exported-volume assertions](/home/nilsb/Documents/Projects/VernierCAD/target/audit-2026-09-08/circle-sticky/report.json) |
|
||||
| Two bodies fail export | [Script](/home/nilsb/Documents/Projects/VernierCAD/target/audit-2026-09-08/two-bodies.json), [failing report](/home/nilsb/Documents/Projects/VernierCAD/target/audit-2026-09-08/two-bodies/report.json). The export step fails immediately because no file is written, before its later expected-error assertion. |
|
||||
| History flags have no geometric effect | [Script](/home/nilsb/Documents/Projects/VernierCAD/target/audit-2026-09-08/history-flags.json), [report](/home/nilsb/Documents/Projects/VernierCAD/target/audit-2026-09-08/history-flags/report.json) |
|
||||
|
||||
Example from the repository root: `target/debug/vernier-drive target/audit-2026-09-08/small-line.json --out target/audit-2026-09-08/small-line-rerun --once --require-adapter llvmpipe`. Probe success means that the documented behavior was reproduced; it does not mean the behavior is desirable.
|
||||
@@ -0,0 +1,63 @@
|
||||
# Native save/recovery implementation and verification
|
||||
|
||||
2026-09-08, reliability worktree `target/worktrees/reliability`, branch `codex/reliability-and-function-verification`. This records the implemented R8 persistence lane and its fresh verification; it is not a claim that all other audit lanes or the workspace gate are complete.
|
||||
|
||||
## Implemented contract
|
||||
|
||||
`crates/vernier-app/src/server/native_files.rs` implements the full-pair redo design and the corrections in `PERSISTENCE_DESIGN_REVIEW_2026-09-08.md`. It uses only existing dependencies and std. Settled saves remain document JSON version 27 and `.names` version 5. The journal is version 1 framing around the exact serialized document and pruned naming bytes; replay never compiles, remints or reserializes the payloads.
|
||||
|
||||
The parent-directory handle holds a cooperative exclusive `try_lock` across recovery and both live reads/writes. The writer syncs the journal before publishing it, syncs its publication before changing either live name, syncs both replacement files before their renames, syncs the installed pair before retiring the journal, and syncs journal retirement. Recovery validates the full journal before mutation, syncs its file and parent publication again, then replays both payloads. This recovery barrier handles a writer that exited immediately after publishing a still-volatile journal name.
|
||||
|
||||
Live reads refuse leaf symlinks and nonregular entries (including FIFOs) before opening them. Save preserves the existing replacement semantics for live leaf symlinks, but refuses symlink staging files. Reserved suffixes `.names`, `.names.bak`, `.transaction`, `.transaction.tmp`, `.transaction.document.tmp` and `.transaction.names.tmp` cannot become native document destinations. Parent path aliases converge on the same directory lock. Native file destinations and staging directories are refused during preflight.
|
||||
|
||||
A malformed published journal is retained and fails closed even next to a valid live pair and even for explicit fresh-identity Open. A legacy `.names.bak` without a new transaction is preserved and refuses ordinary Open/Save; explicit fresh identities does not silently restore it, and saving that fresh document requires another unblocked name while the backup remains. Stale unpublished regular staging bytes are never a recovery source and are replaced by a subsequent save.
|
||||
|
||||
`apply_files.rs` still owns serialization/pruning, running-session replacement, dirty state and fresh-identity explanation. It validates incoming state before discarding the outgoing naming memo. A recovered file produces `Applied::RecoveredFile`, which the server reports in the readout. Post-publication write failures keep the document dirty and distinguish pending recovery, durable pair with pending cleanup, and uncertain cleanup durability. New code creates no `.names.bak` and does not claim rollback after journal publication.
|
||||
|
||||
Both new Rust source files have explicit production/test reachability rows. Existing old rollback tests now assert the actual preflight behavior and the absence of the new reserved transaction/staging names, rather than describing an obsolete last-rename restore.
|
||||
|
||||
## Evidence
|
||||
|
||||
Commands ran from the reliability worktree with `CARGO_TARGET_DIR=/home/nilsb/Documents/Projects/VernierCAD/target`.
|
||||
|
||||
| Verification | Fresh result |
|
||||
|---|---|
|
||||
| `cargo test -p vernier-app -- --test-threads=1` | Exit 0: 273 library tests, 19 headless integration tests, 6 edge/hover integration tests; no failures/ignored tests. Library 6.02 s, headless 8.66 s, edge/hover 3.05 s. |
|
||||
| Final additional test `cargo test -p vernier-app --lib malformed_published_journal_refuses -- --nocapture` | Exit 0: 1 passed (274 library tests now exist). |
|
||||
| `cargo clippy -p vernier-app --all-targets -- -D warnings` after that addition | Exit 0. Existing CXX generated-code build warnings remain emitted by the dependency build script; no Rust lint errors. |
|
||||
| Owned-file `rustfmt --check --edition 2024 --config skip_children=true` | Exit 0 for `native_files.rs`, `native_files_tests.rs`, `apply_files.rs`, `tests/m0_store_cut.rs`. Appended headless test separately formatted, without reformatting others' edits. |
|
||||
| `cargo test -p vernier-app --lib omitted_ -- --nocapture` | Exit 0: both negative-control tests passed by finding the required failing power-loss states. |
|
||||
|
||||
The full app run includes source reachability, existing missing/spliced-sidecar and fresh-identity tests, exact persisted face-ID roundtrip, save-after-undo pruning plus identity-preserving redo, file command isolation, native dirty-state/save/close tests, and the new recovery tests. A first full library run exposed one stale negative assertion requiring a lone circle not to close; it was replaced by a positive closed-profile/extrude-card assertion matching the concurrently implemented circle fix. The later app run above includes that correction.
|
||||
|
||||
### Production protocol faults
|
||||
|
||||
A private `FileSystem` seam runs the same production transaction sequence against std and a volatile/durable filesystem model. The model separates file contents from directory bindings and enumerates subsets of unsynced namespace/content persistence at every writer boundary, then interrupts recovery again before a modeled power loss. It covers first save and replacement save. Ordinary injected create/write/sync/rename/remove/barrier errors assert phase-specific outcomes and successful subsequent replay/retry.
|
||||
|
||||
Negative controls actually omit, through that seam, the writer publication directory barrier, recovery publication directory barrier, each of the three payload file syncs (journal, document, store), and the pair-installation directory barrier. Every omission yields a decisive invalid or irrecoverable modeled state. These tests would not be replaced by ordinary child exits: a process exit normally leaves unsynced bytes visible.
|
||||
|
||||
Real child processes exit immediately after the production writer/recovery filesystem operation returns. There are 16 writer boundaries and 13 recovery boundaries, each run for first save and replacement: 58 checked boundary cases. Recovery cases each need an earlier writer exit to prepare their journal, so the boundary test actually launches 84 intentionally exiting children. Each child has a 5-second parent-enforced deadline, distinguishing timeout, intended exit 77 and unexpected panic/signal. Separate bounded children prove document, sidecar and journal FIFOs refuse without a producer. Separate processes produce identical journal bytes and settled document/names bytes from identical serialized inputs.
|
||||
|
||||
Other protocol tests cover malformed/truncated/overflow/trailing-byte/invalid-UTF8/invalid-payload journals, all nine absent/old/new live-pair combinations, conservative legacy backup behavior, unpublished staging, parent-alias lock contention, live leaf symlink refusal, save replacement without modifying symlink referents, and reserved-name collision refusal while preserving the sole recovery journal.
|
||||
|
||||
### CAD identity and exported geometry
|
||||
|
||||
`recovery_replays_real_parameter_and_topology_generations_even_when_live_pair_validates` starts with a shelled block containing a persisted face reference. Changing the upstream extrude height from 15 to 20 mm leaves the allocator ceiling unchanged; the stale document plus new store explicitly passes the structural validator. A later 0.25 mm bottom-face fillet changes the identity set. Both generations recover from old, mixed and missing live layouts, matching exact document bytes, naming bytes and sorted face IDs. STEP export triggers a second compile whose IDs remain exact, and a fresh kernel's STEP import matches the source volume within 1e-7 mm³ and one solid.
|
||||
|
||||
`malformed_published_journal_refuses_even_fresh_open_without_changing_the_session` verifies both Open identity policies preserve the current serialized document, naming state, active sketch and dirty marker; valid on-disk bytes and malformed journal also remain unchanged.
|
||||
|
||||
`palette_open_recovers_a_saved_shell_and_exports_its_exact_geometry` is the driven acceptance check. Real egui input picks the block's top, invokes Shell through the command palette, types a native destination into the title field and presses Save. A test fixture then places the exact saved pair in a synced published journal and removes both live files. A fresh harness invokes real palette Open, checks the recovered readout and exact live bytes/face IDs, invokes real palette Export STEP, and independently imports the exported shape to check one solid, exact face count and analytic box-minus-interior shell volume within 1e-7 mm³. Export's second compile retains every face identity and leaves native bytes intact. It runs behind the existing 180-second headless bound and takes about 1.3 seconds on this host.
|
||||
|
||||
The UI test's interrupted layout is explicitly synthetic fixture setup, not a claimed process crash. Actual production-writer interruption is independently verified by the bounded child tests described above. No user-visible fault command or direct worker dispatch was introduced for this acceptance path.
|
||||
|
||||
## Limits and remaining integration work
|
||||
|
||||
The implemented guarantee assumes cooperative Linux callers, regular local files, same-directory atomic rename, and storage honoring file/directory fsync. These tests are not physical power-cut tests, filesystem-vendor certification, or real ENOSPC/read-only-media experiments. Ordinary filesystem faults are injected at the protocol seam; the actual app state checks use destination refusal, missing/corrupt state and malformed published journals. Tests do not prove every physical I/O call has a bounded completion time; stalled storage remains outside the try-lock/hang guarantee.
|
||||
|
||||
The format detects structural invalidity, not arbitrary externally substituted but valid pairs, silent bit rot or deliberate tampering. Replay has the full-pair source and never uses structural validation as a freshness heuristic. Old builds and uncooperative writers do not participate in the lock/recovery protocol.
|
||||
|
||||
This is recovery of interrupted explicit saves, not autosave of edits never submitted to Save. No autosave behavior was added.
|
||||
|
||||
The root agent is continuing other reliability changes, including the sketch snap API. The full workspace test/selftest/driven-script gate and the final combined formatting check remain the root integration responsibility. The app results above precede the root's new `SketchDraw::snap_tolerance_mm` field; rerun affected tests after that mechanical interface update. No commits were made by this lane.
|
||||
|
||||
Temporary verification logs: `/tmp/vernier-persistence-app-final.log`, `/tmp/vernier-persistence-session.log`, `/tmp/vernier-persistence-clippy-final.log`, `/tmp/vernier-persistence-barriers.log`, `/tmp/vernier-persistence-generations.log`, `/tmp/vernier-persistence-gui.log`.
|
||||
@@ -0,0 +1,194 @@
|
||||
# VernierCAD function verification inventory — 2026-09-08
|
||||
|
||||
Baseline: `b3f661f`. This is a source-and-runtime coverage inventory, not a claim that every registered function works. It deliberately distinguishes a click through the real egui shell from a direct server call, a registry reachability test, and a kernel/document unit test. Those layers answer different questions.
|
||||
|
||||
The source population is `vernier_ui::registry::COMMANDS`: 75 rows, of which 73 are `Dispatch::Run` and two (`join`, `cut`) are honest `Dispatch::NotYet` rows (`crates/vernier-ui/src/registry.rs:366-1515`). The committed driven suite contains 18 scripts, including the two negative controls. Only 24 of the 73 runnable action routes are exercised by a committed script through a painted shell control. Ten of the fourteen sketch-tool modes are driven. None of the four command-palette-only file actions is driven through its control; 36 `save`/`export_*` steps in the passing scripts call `Headless` directly and bypass palette readiness (`crates/vernier-drive/src/run.rs:492-523`).
|
||||
|
||||
Runtime checks made for this inventory:
|
||||
|
||||
- `cargo test -p vernier-ui registry::tests:: -- --nocapture`: 16 passed. This proves uniqueness, table shape, non-empty workspaces, `NotYet` honesty, and that some synthetic `Selection` offers each non-file runnable action. It does not press the control or run the operation.
|
||||
- `cargo test -p vernier-drive --test l1 the_l1_script_passes -- --nocapture`: 1 passed in 11.35 s. This gate drives sketch, extrude, draft, and an upstream parameter edit through the real shell and checks exact exported STEP geometry in two processes.
|
||||
- The source-backed audit probe in `docs/AUDIT_2026-09-08_shapr3d.md` reproduced the command-palette file-action failure, empty-scene stale body, fixed 2 mm sketch snap, sticky cut mode, and multi-body export refusal on the same baseline.
|
||||
|
||||
## Evidence levels
|
||||
|
||||
| Mark | Meaning |
|
||||
|---|---|
|
||||
| `E-X` | Committed `vernier-drive` script presses the painted control and checks exported geometry with an exact or stated tolerance oracle. |
|
||||
| `E-D` | Committed drive presses the control and checks document JSON, timeline, selection, or naming, but the operation is not isolated by exported geometry. |
|
||||
| `E-R` | Committed drive presses the control and checks an exact UI/readout or image result appropriate to a non-modeling operation. |
|
||||
| `C30` | A component test drives egui under `vernier_ui::testing::driven`'s 30 s watchdog, but does not traverse the full app/server/kernel path. |
|
||||
| `L` | Lower-layer document/kernel/app-dispatch tests exist; no committed real-control test. |
|
||||
| `B` | The committed drive is protected by both the per-`wait_idle` budget (10 s default) and `run_bounded`'s whole-run watchdog (600 s in the binary). |
|
||||
| `—` | No operation-specific real-control evidence or watchdog. |
|
||||
|
||||
Every driven row below carries `B`. The watchdog reports the last step on panic/hang (`crates/vernier-drive/src/run.rs:2029-2088`), while `Headless::wait_idle` bounds every server drain (`crates/vernier-app/src/headless.rs:549-575`). Separately, `no_frame_path_in_this_crate_blocks_on_the_worker` bans blocking worker waits from production frame code (`crates/vernier-app/src/reachability.rs:895-960`). The worker's `Drop` still joins one in-flight OCCT job without a deadline because OCCT has no cooperative cancellation (`crates/vernier-ui/src/worker.rs:180-207`); an uninterruptible kernel call can therefore still delay shutdown.
|
||||
|
||||
## Registry action matrix
|
||||
|
||||
“None” in the control column means no committed script presses the action's actual control. A lower-layer test is recorded where it helps show that the underlying implementation exists, but it does not upgrade the user-facing verdict.
|
||||
|
||||
| Family | Registered action / label / surface | Real-control evidence | Oracle | Hang | Verification gap |
|
||||
|---|---|---|---|---|---|
|
||||
| Solid create | `NewSketch` / sketch / Solid ribbon | `l1`, `l2`, M3 scripts | `E-D`; downstream exact solids in several scripts | `B` | Good control coverage on XY and faces. |
|
||||
| Solid create | `Extrude` / extrude / Solid ribbon or aimed card | `l1`, `l2`, `m3-a-primitives`, `m3-c-dimensions`, `m3-p2-card-arming` | `E-X`, exact STEP/STL volume | `B` | Join/cut choice remains sticky for cut; circle is not auto-promoted to active profile. |
|
||||
| Solid create | `Revolve` / revolve / Solid ribbon | None; session and server tests only (`L`) | Lower-layer revolve geometry only | — | No painted-control → picks → commit → export gate. |
|
||||
| Solid create | `Sweep` / sweep / Solid ribbon | None; session and compiler tests only (`L`) | Lower-layer sweep geometry only | — | No real-control gate for either required slot. |
|
||||
| Solid create | `Loft` / loft / Solid ribbon | None; builder and compiler tests only (`L`) | Lower-layer loft geometry only | — | No real-control gate for add-section/commit workflow. |
|
||||
| Face edit | `PushPull` / push/pull / Solid ribbon/card | `m1-smoke` | `E-X`, 24,000 mm³ STEP and STL | `B` | Strongest freeze regression route; also backed by the 30 s gizmo deadlock component test. |
|
||||
| Face edit | `Draft` / draft / Solid ribbon | `l1`, `l2` | `E-X`, exact volume | `B` | Multi-face path is driven by L2. |
|
||||
| Hole | `MakeHole` / hole / Sketch ribbon | None; `features.rs` dispatch tests (`L`) | Lower-layer geometry | — | No real point-tool → point selection → hole card → export path. |
|
||||
| Body combine | `NotYet` / join / Solid ribbon | Unavailable by construction | None | — | No registry `Action`; multi-body boolean UI is absent. |
|
||||
| Body combine | `NotYet` / cut / Solid ribbon | Unavailable by construction | None | — | No registry `Action`; this is distinct from the extrude-card cut chip. |
|
||||
| Edge finish | `FilletEdge` / fillet / Modify ribbon | None; face-pair/dispatch and kernel tests (`L`) | Lower-layer geometry | — | No real-control gate; direct edge and edge-chain selections intentionally offer no actions. |
|
||||
| Edge finish | `ChamferEdge` / chamfer / Modify ribbon | None; dispatch and kernel tests (`L`) | Lower-layer geometry | — | Same missing real-control and direct-edge path as fillet. |
|
||||
| Face edit | `Shell` / shell / Modify ribbon | `l2` | `E-X`, face count and volume band; exact selftest pin exists below UI | `B` | Driven and geometry-bearing. |
|
||||
| Repeat | `Pattern` / pattern / Modify ribbon | `l2` | `E-X`, analytic volume delta | `B` | Creation is covered; later parameter edit is separate and uncovered. |
|
||||
| Repeat | `PatternCircular` / pattern (circular) / Modify ribbon | None; app dispatch tests (`L`) | Lower-layer geometry | — | No real-control gate. |
|
||||
| Repeat | `Mirror` / mirror / Modify ribbon | None; app/compiler tests (`L`) | Lower-layer geometry | — | No real-control gate or body/feature choice exercise. |
|
||||
| Feature history | `Rename` / rename / Modify ribbon/value card | `C30` captures a typed egui card action | No server/document assertion in that control test | 30 s component only | No actual timeline row → type → save/reopen gate. |
|
||||
| Feature history | `EditFeatureInput` / repoint / Modify ribbon | None; session helper and repoint probes (`L`) | Lower-layer naming/repoint assertions | — | No real-control gate for any feature input family. |
|
||||
| Inspect | `Measure` / measure / Inspect ribbon | None; `edit_for`/server readout tests (`L`) | Lower-layer exact measurement | — | No shell-control readout test. |
|
||||
| Inspect | `Section` / section / Inspect ribbon | `m4-cutaway` | `E-R`, exact curve count/length plus PNG | `B` | Appropriate non-export oracle; both cutaway directions driven. |
|
||||
| Sketch dimension | `ConstrainLength` / length / Sketch ribbon/card | `l2` | `E-X`, typed profile dimensions feed exact solid | `B` | Covered. |
|
||||
| Sketch dimension | `ConstrainAngle` / angle / Sketch ribbon | None; app/solver tests (`L`) | Lower-layer solve | — | No real click on two lines, typed angle, or downstream geometry. |
|
||||
| Sketch dimension | `EditDimension` / radius / Sketch value card | `m3-c-dimensions`, `m3-c-sketch-pattern`, `m3-p2-card-arming` | `E-X`, exact volume at radius/diameter edits | `B` | Covered for circles; arc-radius control is not driven. |
|
||||
| Sketch dimension | `ConstrainPointDistance` / distance / Sketch ribbon | `m3-c-dimensions` | `E-D`, saved constraint value | `B` | Control works in the gate, but its own geometry is not extruded or otherwise isolated. |
|
||||
| Sketch constrain | `ConstrainHorizontal` / horizontal / tile | No explicit tile press; L2 only infers Horizontal | Saved inferred constraint, not this action | — | The explicit user control is unverified. |
|
||||
| Sketch constrain | `ConstrainVertical` / vertical / tile | None (`L`) | Lower-layer solver only | — | No real-control gate. |
|
||||
| Sketch constrain | `ConstrainParallel` / parallel / tile | No explicit tile press; L1 infers Parallel | Saved inferred constraint, not this action | — | The explicit user control is unverified. |
|
||||
| Sketch constrain | `ConstrainPerpendicular` / perpendicular / tile | No explicit tile press; L1 infers it | Saved inferred constraint, not this action | — | The explicit user control is unverified. |
|
||||
| Sketch constrain | `ConstrainEqual` / equal / tile | None (`L`) | Lower-layer solver only | — | No real-control gate. |
|
||||
| Sketch constrain | `ConstrainCoincident` / coincident / tile | None (`L`) | Lower-layer solver only | — | Draw snapping also makes the natural near-point gesture ambiguous. |
|
||||
| Sketch constrain | `ConstrainPointOnLine` / point on line / tile | None (`L`) | Lower-layer solver only | — | No real-control gate. |
|
||||
| Sketch constrain | `ConstrainMidpoint` / midpoint / tile | No explicit tile press; centre rectangle creates one automatically | Saved generated constraint, not this action | — | The explicit control is unverified. |
|
||||
| Sketch constrain | `ConstrainCollinear` / collinear / tile | None (`L`) | Lower-layer solver only | — | No real-control gate. |
|
||||
| Sketch constrain | `ConstrainConcentric` / concentric / tile | None (`L`) | Lower-layer solver only | — | No real-control gate. |
|
||||
| Sketch constrain | `ConstrainTangent` / tangent curves / tile | None (`L`) | Lower-layer solver only | — | No real-control gate. |
|
||||
| Sketch constrain | `ConstrainTangentLineCircle` / tangent / tile | None (`L`) | Lower-layer solver only | — | Same label as curve/curve tangent makes a unique text target context-dependent. |
|
||||
| Sketch constrain | `ConstrainSymmetric` / symmetric / tile | None (`L`) | Lower-layer solver only | — | No real-control gate. |
|
||||
| Sketch constrain | `ConstrainLock` / lock / tile | `l2` | `E-X`; saved locks and downstream exact solid | `B` | Covered for point locking. |
|
||||
| Sketch constrain | `Unlock` / unlock / tile | None (`L`) | Lower-layer document only | — | No real-control gate. |
|
||||
| Sketch edit | `SketchOffset` / offset / Sketch ribbon | None; builder tests (`L`) | Lower-layer document/compile | — | No painted-control → curve picks → commit gate. |
|
||||
| Sketch edit | `SketchMirror` / mirror / Sketch ribbon | None; builder tests (`L`) | Lower-layer document/compile | — | No real-control gate. |
|
||||
| Sketch edit | `SketchFillet` / fillet / Sketch ribbon | None; command/dispatch tests (`L`) | Lower-layer sketch geometry | — | No real-control gate. |
|
||||
| History title | `Undo` / undo / title | None; server tests (`L`) | Lower-layer document state | — | No title-button gate and no undo-to-saved dirty-state acceptance case. |
|
||||
| History title | `Redo` / redo / title | None; server tests (`L`) | Lower-layer document state | — | No title-button gate. |
|
||||
| File title | `SaveDocument` / save / title | None. Passing scripts use direct `Step::Save` bypass | File bytes/sidecar only through bypass | — | Add a `title:save` control test with dirty-state and path assertions. |
|
||||
| File palette | `OpenDocument` / open / command palette | None; palette action is currently dead | Audit reproduces failure; component prompt test is `C30` | — | `readiness_for` consults selection-only `actions_for`, which never offers file actions. |
|
||||
| File palette | `OpenDocumentFresh` / open with fresh identities / palette | None; same dead palette route | Lower-level open tests | — | Same readiness defect; no full unsaved-confirm → open gate. |
|
||||
| File palette | `ExportStl` / export stl / palette | None. Passing scripts directly call `Headless::export_stl` | Exported STL oracles exist only behind bypass | — | User cannot activate the registered control. |
|
||||
| File palette | `ExportStep` / export step / palette | None. Passing scripts directly call `Headless::export_step` | 20 passing-script STEP exports are bypass evidence | — | User cannot activate the registered control. |
|
||||
| Session | `CommitEdit` / commit / session banner | `m3-c-sketch-pattern` | `E-X`, patterned sketch feeds exact solid | `B` | One builder shape covered; revolve/sweep/loft/repoint commits are not. |
|
||||
| Session | `CancelEdit` / cancel / session banner | None; helper state tests (`L`) | None at real-control layer | — | No proof that a rendered cancel preserves document and geometry. |
|
||||
| Loft session | `AddLoftSection` / add section / session | None; builder tests (`L`) | Lower-layer builder state | — | No real-control gate. |
|
||||
| Mirror session | `ArmSketchMirrorAxis` / arm axis / session | `C30` clicks the painted component button | Shell state only | 30 s component only | No app/server or geometry gate. |
|
||||
| Parameter edit | `SetExtrudeHeight` / extrude height / value card | `l1` selects timeline extrude and types height | `E-X`, analytic volume delta | `B` | Covered. |
|
||||
| Parameter edit | `SetRevolveAngle` / revolve angle / value card | None; dispatch/server tests (`L`) | Lower-layer geometry | — | No real-control edit gate. |
|
||||
| Parameter edit | `SetSweepScale` / sweep scale / value card | None; dispatch/server tests (`L`) | Lower-layer geometry | — | No real-control edit gate. |
|
||||
| Parameter edit | `SetFilletSpec` / fillet size / value card | None; dispatch tests (`L`) | Lower-layer geometry | — | No real-control edit gate. |
|
||||
| Parameter edit | `SetShellThickness` / shell thickness / value card | `l2` | `E-X`, face count/volume band with exact lower-layer pin | `B` | Covered. |
|
||||
| Parameter edit | `SetDraftAngle` / draft angle / value card | `l1`, `l2` | `E-X`, exact volume | `B` | Covered. |
|
||||
| Parameter edit | `SetPushPullDistance` / push/pull distance / value card | None; app session tests (`L`) | Lower-layer geometry | — | The creation path is driven; editing an existing push/pull is not. |
|
||||
| Parameter edit | `SetPatternCount` / pattern count / value card | None; app/value-card tests (`L`) | Lower-layer geometry | — | L2 creates a default pattern but never edits its count. |
|
||||
| Parameter edit | `SetMirrorPlane` / mirror plane / value card | None; app/repoint tests (`L`) | Lower-layer geometry | — | No real-control gate. |
|
||||
| Parameter edit | `EditHole` / hole size / value card | None; app tests (`L`) | Lower-layer geometry | — | Hole creation and editing both lack a real-control gate. |
|
||||
| Projection | `ProjectFaceCentre` / project centre / Sketch ribbon | `m3-b-project` | `E-D`, exact saved reference rows | `B` | Control and naming are covered; projected point does not drive the exported solid. |
|
||||
| Projection | `ProjectEdgeEnds` / project ends / Sketch ribbon | `m3-b-project` | `E-D`, exact saved reference rows | `B` | Same limitation. |
|
||||
| Projection | `ProjectEdgeCentre` / project centre of edge / Sketch ribbon | `m3-b-project` | `E-D`, exact saved reference rows | `B` | Same limitation. |
|
||||
| Sketch constrain | `ToggleConstruction` / construction / tile | `m3-b-construction`, `l2` | `E-D`, saved construction flag | `B` | Covered as document state; its profile-exclusion effect has lower-layer checks. |
|
||||
| Construction plane | `NewSketchOffset` / sketch (offset) / Solid ribbon | `m3-c-planes` | `E-X`, exact boss volume and saved plane | `B` | Covered. |
|
||||
| Construction plane | `NewSketchAngled` / sketch (angled) / Sketch ribbon | `m3-c-planes` | `E-D`, saved angle | `B` | Plane creation is covered; no geometry is built on the new plane. |
|
||||
| Construction plane | `RePlaneSketchOffset` / re-plane (offset) / Modify ribbon | `m3-c-planes`, `l2` | `E-X`, exact moved boss/cut geometry | `B` | Covered. |
|
||||
| Construction plane | `RePlaneSketchAngled` / re-plane (angled) / Sketch ribbon | None; app tests (`L`) | Lower-layer document/compile | — | No real-control gate. |
|
||||
| Sketch repeat | `SketchPattern` / pattern / Sketch ribbon | `m3-c-sketch-pattern` | `E-X`, exact volume after patterned circle extrusion | `B` | Covered for linear placement. |
|
||||
| Sketch repeat | `SketchPatternCircular` / pattern (circular) / Sketch ribbon | None; lower-layer tests (`L`) | Lower-layer geometry | — | No real-control gate. |
|
||||
| Dimension edit | `EditSketchDimension` / dimension / value card | `m3-c-dimensions`, `l2` | `E-X` in L2 propagation; saved value in M3 | `B` | Covered for distance; radius uses the separate `EditDimension` action. |
|
||||
| Dimension edit | `RemoveConstraint` / remove / tile | None; app command tests (`L`) | Lower-layer document state | — | No real label-pick → remove → solve/geometry gate. |
|
||||
|
||||
The 24 end-to-end action routes are: `NewSketch`, `Extrude`, `PushPull`, `Draft`, `Shell`, `Pattern`, `ConstrainLength`, `EditDimension`, `ConstrainPointDistance`, `ConstrainLock`, `CommitEdit`, `SetExtrudeHeight`, `SetShellThickness`, `SetDraftAngle`, `ProjectFaceCentre`, `ProjectEdgeEnds`, `ProjectEdgeCentre`, `ToggleConstruction`, `NewSketchOffset`, `NewSketchAngled`, `RePlaneSketchOffset`, `SketchPattern`, `EditSketchDimension`, and `Section`.
|
||||
|
||||
## Sketch tool modes outside the registry
|
||||
|
||||
`SketchTool` is deliberately outside `COMMANDS` because it arms the meaning of the next viewport click (`crates/vernier-ui/src/shell/sketch_mode.rs:18-226`). It is still user-facing and must be included in an “all available functions” claim.
|
||||
|
||||
| Tool | Committed real-shell evidence | Oracle / gap |
|
||||
|---|---|---|
|
||||
| Select | Many M3/L2 scripts | Selection state; no model mutation expected. |
|
||||
| Line | L1, L2, M3 scripts | Saved curves/constraints and exact downstream solids (`E-X`). |
|
||||
| Circle | `m3-c-dimensions`, `m3-c-sketch-pattern`, `m3-p2-card-arming` | Exact downstream solid volumes (`E-X`). |
|
||||
| Arc (centre/start/end) | None | Lower-layer drawing tests only. |
|
||||
| Point | None | Lower-layer drawing tests only; this also blocks a natural real-control hole gate. |
|
||||
| Rectangle | L1, L2, M3 project/trim scripts | Exact downstream solids (`E-X`). |
|
||||
| Slot | None | Lower-layer drawing tests only. |
|
||||
| Spline | `m3-a-drag` | Saved control/fit-point document assertions; no solid oracle. |
|
||||
| CentreRectangle | `l2`, `m3-a-primitives` | Exact downstream solid (`E-X`). |
|
||||
| Polygon | None | Lower-layer drawing tests only. |
|
||||
| ThreePointArc | `m3-a-primitives` | Saved geometry; no solid oracle. |
|
||||
| TangentArc | `m3-a-primitives` | Saved arc/constraint; no solid oracle. |
|
||||
| Trim | `m3-b-trim-split`, `l2` | Saved document; L2's later solid does not isolate trim. |
|
||||
| Split | `m3-b-trim-split` | Saved document only. |
|
||||
|
||||
This is 10/14 tool modes driven. The absent real-control modes are centre-point `Arc`, `Point`, `Slot`, and `Polygon`.
|
||||
|
||||
## Dead, bypassed, and unavailable surfaces
|
||||
|
||||
All four workspace tabs (`Sketch`, `Solid`, `Modify`, `Inspect`) contain registered controls, and the structural test `every_workspace_has_commands` passes. That is a non-empty-tab check, not a functional workspace gate. The important unavailable surfaces are narrower:
|
||||
|
||||
1. The command palette cannot be opened by a drive script because the harness emits modifier bits on key events but no `Event::ModifiersChanged`; egui's `ctrl+k` path reads `InputState::modifiers` (`crates/vernier-drive/tests/scripts.rs:2100-2108`). This prevents any real-shell test of palette-only commands.
|
||||
2. Even when opened manually, `OpenDocument`, `OpenDocumentFresh`, `ExportStl`, and `ExportStep` are all marked non-ready because palette dispatch calls selection-only `readiness_for`; `actions_for` never offers document actions. This is the audit's executed P1 failure (`crates/vernier-ui/src/shell/ribbon.rs:1987`, `crates/vernier-ui/src/registry.rs:1523-1541`).
|
||||
3. Direct driver file steps bypass that control path and call `Headless::{save_document,export_stl,export_step}`. The strong export geometry assertions therefore prove the server/export pipeline while saying nothing about user reachability.
|
||||
4. Edge, edge-chain, and vertex picking is driven by `m4-edge-pick`, but `actions_for` returns an empty vector for every one of those selections (`crates/vernier-ui/src/toolbar.rs:980-995`). Fillet/chamfer are only offered through face and face-pair picks.
|
||||
5. Six settings panes are displayed as unavailable: units & precision, viewport, files & export, keyboard, solver & naming, and about. Appearance is the only built pane (`crates/vernier-ui/src/settings.rs:230-300`).
|
||||
6. Ten production multi-body document commands are explicitly unreachable from the app: `SetFeatureTarget`, `AddBoolean`, `SetBooleanTools`, `AddSplitBody`, `ImportStepBody`, `AddMoveBody`, `SetBodyMotion`, `SetBodyAttributes`, `AddBodyPattern`, and `AddBodyMirror` (`crates/vernier-app/src/reachability.rs:780-800`). `AddForgedProducer` is the eleventh allow-list entry but is test-only. This is why a valid two-body document compiles below the UI yet cannot be displayed/exported as a normal app workflow.
|
||||
|
||||
## Required acceptance gates before claiming every available function works
|
||||
|
||||
The present suite cannot support that claim. The highest-value additions, in order, are:
|
||||
|
||||
1. Fix modifier delivery, then drive command palette Save/Open/Open Fresh/Export STL/Export STEP through typed paths. Assert the unsaved-open choice, destination separation, file bytes, naming sidecar, and exact exported geometry. Delete or clearly label direct file steps as pipeline helpers rather than control acceptance.
|
||||
2. Add bounded real-shell geometry gates for the kernel-heavy but user-visible operations with zero such coverage: revolve, sweep, loft, fillet, chamfer, hole, circular solid pattern, mirror, sketch offset/mirror/fillet, and circular sketch pattern. Each should have a false-oracle negative control.
|
||||
3. Drive every explicit sketch constraint tile. Inference is a different path and must not count for Horizontal, Parallel, Perpendicular, or Midpoint. Include remove and unlock, and assert solver verdict plus downstream geometry where the constraint can affect a profile.
|
||||
4. Drive the full parameter-edit set: revolve angle, sweep scale, fillet size, push/pull distance, pattern count, mirror plane, and hole size. Existing `edit_for` tests prove dispatch construction; they do not prove selection, card visibility, text entry, commit, or responsiveness.
|
||||
5. Drive repoint and cancel for each distinct session shape, plus rename, undo, and redo through painted controls. Assert document identity/naming after save and reopen, not only UI state.
|
||||
6. Preserve the current two-level hang defense for every new script. Add an explicit shutdown budget or out-of-process kill strategy for an OCCT call that never returns; the in-process watchdog can report a hung drive but cannot stop a stuck worker that `Drop` joins.
|
||||
7. Treat edge/vertex operation wiring and multi-body UI as unavailable until their current empty/unreachable paths are replaced by real controls and bounded exported-geometry gates. Keep assemblies, drawings, rendering/materials, CAM, simulation, sheet metal, and cloud outside this acceptance scope.
|
||||
|
||||
## Implementation addendum — 2026-09-08, first reliability batch
|
||||
|
||||
This dated addendum records subsequent implementation and runtime evidence in `target/worktrees/reliability`, branch `codex/reliability-and-function-verification`, reviewed before the batch commit. The baseline matrix above remains the historical `b3f661f` inventory. Its 24/73 count and committed-script descriptions have not been silently replaced or presented as a current recount. The scripts below existed in the reviewed worktree; their commit status is a separate integration fact. This addendum does **not** establish that every available operation works.
|
||||
|
||||
### Newly verified solid-builder routes
|
||||
|
||||
The three `scripts/drive/functions-{revolve,sweep,loft}.json` scripts start from empty documents, draw inputs with actual sketch controls, open the Solid ribbon builder, pick its inputs and click the painted session Commit control. They use direct `export_step` only as the resulting geometry oracle. Those helper calls are **not** counted as Export STEP control coverage.
|
||||
|
||||
| Route newly covered beyond the baseline | Concrete driven result | Evidence and practical limit |
|
||||
|---|---|---|
|
||||
| `Revolve` → fixed-slot `CommitEdit` | Line/Rectangle inputs, two axis-point picks and four profile-curve picks produce an annular cylinder of `1200*pi` mm³, one solid. | `functions-revolve.json`; `/tmp/vernier-functions-revolve-review/report.json`: pass, 88 steps, cross-process deterministic. Geometry tolerance `1e-9` relative. |
|
||||
| `SetRevolveAngle` through its value card | Typing 180 halves the volume to `600*pi`; typing 360 restores it. | Original and restored STEP digest `0xba2ad81f55edaa83`. This is a real parameter-edit route, distinct from builder creation. |
|
||||
| `Sweep` → fixed-slot `CommitEdit` | Circle radius typed as 3, line path length 30, profile/path picks produce a cylinder of `270*pi` mm³. | `functions-sweep.json`; `/tmp/vernier-functions-sweep-review/report.json`: pass, 96 steps, cross-process deterministic. Straight-cylinder tolerance `1e-9` relative. |
|
||||
| `SetSweepScale` through its value card | Scale 2 produces a frustum of `630*pi`; scale 1 restores the cylinder. | Original/restored STEP digest `0x7b0cc95e60106851`. Frustum tolerance `1e-6` is stated for the kernel's approximated sweep-law surface, not widened to conceal a failed input pick. |
|
||||
| `Loft`, `AddLoftSection` and Loft `CommitEdit` | Rectangle profiles, actual Add section control, second offset typed as 20, then Commit produce the square frustum `2080/3` mm³, one solid, six faces. | `functions-loft.json`; `/tmp/vernier-functions-loft-review/report.json`: pass, 82 steps, cross-process deterministic, relative tolerance `1e-9`. Existing-loft repointing is not established by this creation test. |
|
||||
| `CancelEdit` for fixed-slot Create and Loft builders | Each script opens a second partial builder, clicks `ribbon:cancel`, asserts selection `none`, unchanged two-feature timeline and no error. | Proves the builder exits and the partial picks do not commit. It does not cover every Edit/repoint session family. |
|
||||
| Undo/Redo keyboard routes after Revolve, Sweep and Loft creation | Actual Ctrl+Z returns to the sole sketch; Ctrl+Y restores the solid and its two-row timeline. | Each redone STEP digest matches its original: Revolve `0xba2ad81f55edaa83`, Sweep `0x7b0cc95e60106851`, Loft `0xb338986ad120fefc`. The title Undo/Redo buttons remain a separate unverified surface in this addendum. |
|
||||
|
||||
All three final reports require llvmpipe, run at 1600 x 1000 and identify two distinct process IDs. Script `wait_idle` budgets remain 10 seconds; the existing whole-run watchdog remains in force. These are representative single-body fixtures, not all valid/invalid input combinations or a new guarantee about a stuck kernel call.
|
||||
|
||||
The primary Commit/Cancel omission was repaired for fixed-slot Create/Edit sessions. Painter and width measurement now share the Commit readiness rule and account for the new controls before ribbon collapse. Review also found and corrected the nonempty-but-incomplete axis case: shared `FeatureInput::accepts` supplies cardinality/type checks to both UI readiness and app edit construction, while deeper geometric/stale-reference validation remains downstream. Fresh independent verification passed 9 ribbon overflow tests and 18 app session tests. Detailed review and handoff: [solid-builder review](SOLID_BUILDERS_REVIEW_2026-09-08.md), [solid-builder verification](SOLID_BUILDERS_VERIFICATION_2026-09-08.md).
|
||||
|
||||
### File and sketch routes verified during the same batch
|
||||
|
||||
The baseline's unavailable-file-control and sticky-circle/cut observations also have subsequent evidence. These entries preserve the distinction between actual controls and storage/geometry helpers.
|
||||
|
||||
| Route or behavior | Actual-control evidence | Oracle / qualification |
|
||||
|---|---|---|
|
||||
| Title `SaveDocument`; palette `OpenDocument`, `ExportStl`, `ExportStep` | `scripts/drive/file-commands-actual.json` types separate native/export paths, clicks title Save, opens `$`, selects the export commands, then modifies the part and clicks the actual `discard and open` confirmation. | `/tmp/vernier-file-commands-actual/report.json`: pass, 53 steps, **single-process**, not a cross-process claim. Saved JSON version is 27; STL volume is 18000 mm³ within 2%; STEP volume 18000, area 4500, centroid `[20,15,7.5]`, one solid and six faces at `1e-9` relative tolerance. Open restores the saved two-row timeline. This script uses no direct Save/Export helper steps. |
|
||||
| Save → interrupted-pair recovery through palette Open → palette Export STEP | App headless integration `palette_open_recovers_a_saved_shell_and_exports_its_exact_geometry` clicks the real controls on a shelled block. | Exact document/naming bytes and face IDs survive, recovery is disclosed, independently imported STEP matches the analytic shell volume. The interrupted disk layout is explicitly synthetic; separate bounded process/fault-model tests establish the writer's interruption protocol. |
|
||||
| Completed Circle → closed-profile Extrude; one-operation Cut choice | `cut-then-boss.json` requires immediate profile selection after both circle drawings, types exact radii, chooses `chip:cut` for the first extrusion only, then creates a boss. | `/tmp/vernier-reliability-release-final/cut-then-boss/report.json`: cross-process pass. Exported analytic deltas are `-251.32741228718345` and `+141.3716694115407` mm³. Helper exports measure geometry; they add no file-control coverage. |
|
||||
| Screen-derived draw capture and Alt override | `small-profile-40.json`, `small-profile-80.json`, `snap-alt-override.json` use actual canvas clicks and typed extrusion height. | Cross-process passes in `/tmp/vernier-reliability-release-final/small-profile-40/report.json`, `/tmp/vernier-reliability-release-final/small-profile-80/report.json`, `/tmp/vernier-reliability-release-final/snap-alt-override/report.json`. The 1 x 1 x 2 mm profiles survive both zoom levels. The submillimetre Alt case uses a documented 30% volume tolerance for pixel quantization; removing Alt fails its own profile expectation at step 9 (`/tmp/vernier-snap-without-alt.log`). |
|
||||
|
||||
The newly clickable file commands now use document/path context rather than viewport selection alone. Native save uses its own destination, and the palette's Open/Export destination is separate. **Open with fresh identities is not promoted to actual-control coverage here**: its lower-layer refusal/preservation tests and readiness implementation are not a driven successful fresh-open workflow. The actual palette scripts open `$` by clicking its control; they do not prove every modifier-based palette shortcut.
|
||||
|
||||
Review closed the durability dirty-state, inherited-directory-lock and asynchronous Cut-choice defects found during the batch. A pending extrusion now blocks further card input until acknowledgement; the real held-worker chip test has an idle positive control. Headless chip targeting now obtains the currently painted card bounds, never an absent card's remembered Area rectangle. Evidence and limits are in [persistence review](PERSISTENCE_IMPLEMENTATION_REVIEW_2026-09-08.md) and [sketch review](SKETCH_RELIABILITY_REVIEW_2026-09-08.md). These are safeguards for the newly measured routes, not additional registry actions to inflate a coverage count.
|
||||
|
||||
The other baseline gaps remain unpromoted unless separate evidence is supplied: fillet/chamfer/hole, other patterns and mirrors, undriven constraint/tool families, broad repoint coverage, multi-body operation UI, and unavailable settings are not declared verified by these fixtures. Final combined workspace, release and commit results belong to the batch integration record; they do not replace operation-specific control/oracle evidence.
|
||||
@@ -0,0 +1,96 @@
|
||||
# Audit implementation and function verification
|
||||
|
||||
Goal: implement the audit findings and in-scope capability gaps, and verify every available function through its user-facing path, including responsiveness. A green unit suite alone is insufficient.
|
||||
|
||||
Base: b3f661f. Worktree: `/home/nilsb/Documents/Projects/VernierCAD/target/worktrees/reliability`, branch `codex/reliability-and-function-verification`. Existing user-owned AGENTS.md and audit are preserved. Initial audit baseline: 1306 tests and 44 selftests passed. Full hardware-specific validation remains pending; audit used llvmpipe.
|
||||
|
||||
Architecture: keep OCCT and file operations on the document worker, egui and rendering on the UI thread. Reuse document commands and deterministic naming, extend scene state explicitly, and make the acceptance harness drive actual controls. No new cloud, materials, drawings, assemblies, CAM, simulation or sheet-metal scope.
|
||||
|
||||
Global constraints: Rust 1.96+, f64 millimetres and radians internally; no kernel work on the render thread; no unsafe outside vernier-occt-sys; preserve topological identity and deterministic output. No new dependency except the already-approved rfd, directories, image(PNG), rayon without discussion. Every UI test is bounded and distinguishes panic, timeout, refusal, and success. Source-only reachability is not acceptance.
|
||||
|
||||
## Requirements ledger
|
||||
|
||||
- [x] R1 Actual-control Open/Save/Export work; valid paths accepted; export cannot overwrite native documents; Ctrl+S. Verified in the first reliability batch; integration is recorded separately under R14.
|
||||
- [ ] R2 Escape cancels; explicit/window quit protects unsaved and pending changes; Save/Discard/Cancel outcomes verified.
|
||||
- [ ] R3 Empty scenes clear all old body/picking state; failed rebuild display is explicit; document switches cannot retain old geometry.
|
||||
- [ ] R4 Screen-scale snapping, explicit snap override, 1 mm geometry at multiple zoom levels.
|
||||
- [ ] R5 Multi-body scene, picking, items/visibility/isolate, targets, booleans, transforms, import/export; L4 acceptance.
|
||||
- [x] R6 Operation-scoped/defaulted extrusion mode with visible choice; no hidden Cut carry-over across operations/documents. Exact cut-then-boss geometry and pending-input regression passed.
|
||||
- [ ] R7 Circles participate in the closed-profile workflow; usable region/nested-loop selection.
|
||||
- [x] R8 Crash-durable native save and recovery preserving legacy document/naming identities; interruption tests. Fault-model, bounded process and actual recovery-control evidence independently reviewed.
|
||||
- [ ] R9 Edge-based fillet/chamfer and gizmo; curved-face/direct editing; L3 organic bracket acceptance.
|
||||
- [ ] R10 History delete/reorder/suppress/rollback and broken-reference recovery; remove inert behavior.
|
||||
- [ ] R11 Variables/expressions/unit entry and practical sketch deletion, ellipse/text/spline authoring gaps.
|
||||
- [ ] R12 File dialogs, recovery/autosave and saved-state tracking; interchange gaps prioritized for real parts.
|
||||
- [ ] R13 Enumerate and exercise EVERY registered available function: trigger, resulting document/geometry, refusal, undo/repeat, and responsiveness/hang bound. Update inventory as new functions land.
|
||||
- [ ] R14 Final integrated fmt/clippy/workspace/selftest/driven gates, independent review, current documentation, integrate into user checkout without overwriting unrelated work.
|
||||
|
||||
## Execution order and ownership
|
||||
|
||||
1. File command availability and destination safety: implementer owns registry.rs, shell/ribbon.rs, edit.rs file dispatch and focused tests. Reproduce disabled Export with actual palette; observe red, fix availability based on document/path state, prove native JSON survives STL/STEP export. Open-discard controls remain active. Standard model, independent review.
|
||||
2. Scene/quit safety: root owns app.rs, camera.rs, headless.rs and app integration tests while task 1 is isolated from these files. Write bounded pixel/identity test for opening empty document and key/close state tests before fixes. Quit dialog integration follows file-task handoff to avoid shell-file overlap.
|
||||
3. Sketch/extrusion: implementer after file review owns draw/session paths. Move screen tolerance into explicit draw input with default for non-GUI callers; Alt disables snap as well as inference. Prove 1 mm line and closed circle geometrically. Reset extrusion mode on successful operation/document transition, not during an unfinished edit.
|
||||
4. Persistence: strong-model design first; implement after protocol and fault matrix reviewed. Test interruption at each durable boundary; never recover a document against another generation's names.
|
||||
5. Multi-body: strong-model integration after scene safety. Reuse existing BodyId and commands; expose all-body meshes and explicit selection/targets, then body operations/import/export and L4.
|
||||
6. Direct/history/sketch/variables: implement dependencies from existing 2026-09-06 plan while retaining R9–R12 until end-to-end accepted. Separate bounded task briefs with exact interfaces are written before each dispatch.
|
||||
7. Function inventory agent operates read-only alongside implementation; its complete registry inventory determines R13 acceptance cases. Independent reviewers assess each logical change and the complete final result. No task is marked complete by presence of a constructor alone.
|
||||
|
||||
## Evidence log
|
||||
|
||||
- Initial implementation turn: current tree revalidated, isolated worktree created, audit artifact copied, inventory and persistence-design agents dispatched. No requirements complete yet.
|
||||
- R3: bounded empty-document regression reproduced stale face ID 13 after Open (`/tmp/vernier-impl-empty-red.log`), then passed with mesh/maps/selection clearing, including a changed color-buffer pixel (`/tmp/vernier-impl-empty-green.log`). Successful Open now echoes the native path; integrated recheck pending.
|
||||
- R2/R1: Escape regression failed before the change (`/tmp/vernier-impl-escape-red.log`) and passed after cancellation replaced quit (`/tmp/vernier-impl-escape-green.log`). Actual close-dialog clicks now cover pending-work protection, cancel, explicit discard, save-and-close roundtrip, and save failure remaining dirty/open. Ctrl+S uses the acknowledged native destination. Four focused close tests passed (`/tmp/vernier-impl-close.log`); independent review and release verification pending.
|
||||
- R13 inventory: `docs/FUNCTION_VERIFICATION_2026-09-08.md` enumerates all 75 registry rows (73 runnable, 2 unavailable). Baseline committed actual-control coverage is only 24/73 actions and 10/14 sketch tools. 36 existing Save/Export driver steps bypass controls. These are coverage gaps, not accepted passes. Registry 16/16 and L1 two-process gate passed during inventory.
|
||||
- R8: std-only redo-journal design written to `docs/PERSISTENCE_DESIGN_2026-09-08.md`; independent protocol review in progress before implementation. No durability claim yet.
|
||||
- Scene/close integration: full `vernier-app --test headless` now passes 17/17 in debug and 17/17 in release (`/tmp/vernier-impl-headless{,-release}.log`). The broader run caught and corrected a root-UI wrapper that consumed viewport picks; existing pointer-routing regression is green. Tests use unique temporary paths so a previous failed run cannot contaminate a rerun.
|
||||
- Persistence review requires four corrections before acceptance: recovery must establish journal publication durability before replay, native leaf symlink reads must be refused or canonically locked, special-file reads must be refused before blocking open, and transaction/staging filenames must be reserved against native Save-as. See `docs/PERSISTENCE_DESIGN_REVIEW_2026-09-08.md`.
|
||||
- Independent close review found and root corrected additional cases: retain and retire applied/in-flight preview tokens before close/cancel; block stale native paths and entity IDs while Open/Save awaits its ordered response; clear all document-bound shell state after successful Open; route global Ctrl+S/Ctrl+Q even with a focused text field. In-flight sketch-drag close cancellation failed red and passed green (`/tmp/vernier-impl-close-drag-{red,green}.log`). Focused native-field Ctrl+S test passed after explicitly proving egui owns keyboard focus (`/tmp/vernier-impl-shortcut.log`). Remaining preview/switch/race checks underway.
|
||||
- R1 file implementer handoff is `target/worktrees/reliability/docs/FILE_COMMANDS_IMPLEMENTATION_2026-09-08.md`: contextual readiness, separate destinations, reserved-path alias/hardlink guards, actual-control script. Registry 20/20 and focused byte-preservation tests passed. New barrier exposed a pre-existing palette Enter also committing the underlying value card; whole-frame input ownership regression failed red then passed green. Release actual-control script passed again after correction.
|
||||
- Worker failure was previously indistinguishable from busy: a panic sent no wake and polling erased disconnection. Wake regression failed red then passed with disconnect-before-wake; app now reports a stopped worker, clears impossible pending jobs, and allows explicit close. App failure/close state regression passed (`/tmp/vernier-impl-worker{,-app}-green.log` for worker; app actual log `/tmp/vernier-impl-worker-app.log`). An in-flight OCCT call that never returns remains a separate shutdown limitation, not claimed fixed by panic handling.
|
||||
- R8 implementation delegated to strong-model durable_native_files agent after file lane handoff. Owns paired-file protocol/fault model and tests. Interface will return explicit Applied::RecoveredFile, not a sticky recovery status flag. No durability claim until fault tests and independent review.
|
||||
- User requested sitrep/ETA: reported audit complete, reliability in progress, estimated 1–2 hours for current reliability fixes/verification and several days for full feature backlog. Goal remains active; no requirement was dropped.
|
||||
|
||||
## Verification commands
|
||||
|
||||
Run from the implementation worktree with `CARGO_TARGET_DIR=/home/nilsb/Documents/Projects/VernierCAD/target` to reuse dependencies. Use focused tests during changes; integrated gates after affected tasks are reviewed. Collect complete output and exit status. Test release UI drives as well as debug because egui re-entrancy can panic in debug and hang in release.
|
||||
|
||||
`cargo fmt --all --check`
|
||||
|
||||
`cargo clippy --workspace --all-targets -- -D warnings`
|
||||
|
||||
`cargo test --workspace`
|
||||
|
||||
`cargo run -p vernier-cli -- --selftest`
|
||||
|
||||
`cargo run --release -p vernier-drive -- <actual-control-script> --out <evidence-directory> --require-adapter <verified-adapter>`
|
||||
|
||||
No unsupported adapter may silently stand in for RADV golden verification. Report hardware limitations separately from functional failures and continue all unaffected verification.
|
||||
|
||||
## Continued evidence, 2026-09-08
|
||||
|
||||
- R7 circle closure: natural circle drawing now returns its own closed profile and the real-control circle extrusion exports a cylinder. Closed profiles preserve the no-prior-pick session openers: the old sketch-pattern drive first exposed a stale `none` expectation, then a real blocked Pattern command; both are corrected and its analytic STEP gate passes across processes. Nested regions remain incomplete.
|
||||
- R4 drawing snap: `Edit::SketchDraw` carries a camera-derived tolerance (six physical pixels, conservative local plane mapping); Alt sends no snapping and no inference. A 1 x 1 mm rectangle failed red at its profile assertion with the old 2 mm tolerance, then exports exactly 2 mm3 at camera distances 40 and 80. Both scripts pass across processes. A submillimetre Alt drawing also passes; its Alt-off control fails at its own profile assertion, exit 1. Existing direct server fixtures explicitly use 2 mm. Drag and trim tolerances are not changed by this drawing fix.
|
||||
- R6 extrusion mode: successful extrusion resets Cut and refusal preserves it. The `cut-then-boss` real-control script types both circle radii, chooses Cut only once, and asserts exact negative and positive STEP deltas (71 steps before later bookkeeping additions, cross-process pass). Its early freehand version could not support exact volume assertions; typed radii remove pointer rounding from the oracle.
|
||||
- Instrument: `chip:cut` previously matched both the chip and disabled ribbon command. Chip targets now use the card rectangle. Independent review caught historical egui area bounds surviving suppression; the shell now records only the current painter result. The actual headless show-card/open-palette regression failed red and passes green. New workflow scripts are included in workspace driver tests and the pinned local check script.
|
||||
- R8 handoff and independent review: see `DURABLE_FILES_IMPLEMENTATION_2026-09-08.md` and `PERSISTENCE_IMPLEMENTATION_REVIEW_2026-09-08.md`. The reviewer found a clean marker surviving recovery of a different generation followed by Save failure. Both failure during recovery and successful recovery followed by new-save preflight failure now mark the running document dirty; the precise regression failed red and passes green and re-review accepted it.
|
||||
- Broad validation before the last review corrections: 277 app library + 19 headless + 6 picking integration tests passed. Existing release driven scripts all passed after the sketch-pattern correction; both ladder negative controls failed at their intended geometry step (L1 step 73 and L2 step 247). Runs used llvmpipe with PNG comparison skipped where the originals carry RADV goldens; this does not certify RADV visuals. All 44 CLI selftests passed again.
|
||||
- The next full workspace run exposed a native directory-lock lifetime race (277/278 app lib passed): a concurrent process spawn can temporarily retain the directory open description after the parent drops its file handle. A deterministic duplicated-descriptor test reproduces the held lock; the private Disk guard now explicitly unlocks on drop. The observed fork timing remains a hypothesis, while the descriptor-lifetime defect is directly reproduced. Combined verification is being rerun.
|
||||
- Review of the extrusion reset found that a late result could erase a newer chip choice. An acknowledgement barrier now covers extrusion as well as Save/Open. Its dispatch regression passed, but independent re-review proved the value card creates a separate egui Area unaffected by parent disable. The actual held-worker chip test reproduced this; the Area now inherits enabled state and ignores raw Enter/Escape when disabled. Combined release verification and final re-review remain pending.
|
||||
- R13 solid builders: the agent is driving Revolve, Sweep and Loft with analytic exports. It found the fixed-slot Create/Edit ribbon renders input slots but no Commit/Cancel; the palette can finish the operation. The agent is implementing the primary ribbon controls and matching width measurement, then replacing the palette fallback in its acceptance scripts. See the eventual `SOLID_BUILDERS_VERIFICATION_2026-09-08.md` handoff.
|
||||
- The full scope remains active. Multi-body scene/interaction, direct/history tools, nested profiles, variables and remaining sketch capabilities, dialogs/autosave, complete per-function acceptance and native-call process containment are still outstanding. No production changes are integrated into the original checkout yet.
|
||||
|
||||
### Latest integrated results
|
||||
|
||||
- `/tmp/vernier-reliability-workspace2.log`: workspace command exited 0, 1,354 tests passed, 19 ignored fixtures/measurement cases, no failures. This includes the native unlock and actual held-worker card regressions and the first ribbon controls fix.
|
||||
- `/tmp/vernier-reliability-headless-release-final.log`: release headless integration 20/20 passed. `/tmp/vernier-reliability-clippy-final.log`: workspace/all-targets Clippy exited 0. `cargo fmt --all --check` exited 0.
|
||||
- `/tmp/vernier-reliability-release-final/results.json`: 23 release scripts; all 21 positive scripts passed across processes and both negative controls returned exit 1 at their intended geometry assertion. llvmpipe pinned; RADV PNG comparison deliberately skipped. `/tmp/vernier-file-controls-release-final.log`: actual native file controls passed again, once, at their recorded temporary destination.
|
||||
- `SOLID_BUILDERS_VERIFICATION_2026-09-08.md`: all three builder scripts passed release cross-process with analytic STEP geometry. They are now included in the regular driver regression test and pinned local check script. `/tmp/vernier-reliability-eight-workflows.log`: the combined eight new workflow cases passed in the bounded driver test.
|
||||
- `SOLID_BUILDERS_REVIEW_2026-09-08.md` found one remaining P2: nonempty fixed slots are not necessarily committable (a Revolve axis needs exactly two points). Agent is unifying arity checks and strengthening the scripts' Cancel and undo evidence. Therefore the first batch is not checkpointed yet; impacted verification must follow that final change. No full-goal completion claim is made.
|
||||
|
||||
### First batch checkpoint verification
|
||||
|
||||
- The solid-builder P2 is closed after independent re-review. `FeatureInput::accepts` now supplies the shared slot type/cardinality rule to UI readiness and app constructors. All three scripts perform real Ctrl+Z/Ctrl+Y and assert the redone STEP digest equals the original; Cancel explicitly returns selection to none. Nine ribbon overflow tests and the app session tests passed. Current reports are named in the final solid-builder handoff and review.
|
||||
- Final combined workspace command exited 0: `/tmp/vernier-reliability-workspace-final.log`, 1,355 tests passed in 66 suites, 19 ignored fixture/measurement cases, no failures. `/tmp/vernier-reliability-clippy-checkpoint.log` exited 0 for workspace/all-targets Clippy with warnings denied. Formatting and diff whitespace checks passed. Only explanatory comments about clearing a failed scene's mesh changed after that workspace run.
|
||||
- Final release headless integration exited 0 after those comments: `/tmp/vernier-reliability-headless-checkpoint.log`, 20/20 passed, including actual close/save and palette recovery/export paths.
|
||||
- R1, R6 and R8 now have requirement-specific acceptance evidence. R2/R3 have substantial first-batch fixes and bounded integration tests, but remain open for the complete interruption/recovery lifecycle. R4 covers drawing and Alt override; drag and trim still use their old tolerances. R7 covers circles but not nested regions. R13 has additional measured routes, not universal coverage. R14 stays open until the full goal's final integration and gates.
|
||||
- Next native containment dependency: a separately versioned document session checkpoint preserving undo/redo, allocator and rollback exactly, without changing the native file format. The design remains a plan until the helper process, stop/recovery controls and hostile nonreturning-call acceptance tests are implemented. No claim of bounded native-call shutdown is made by this checkpoint.
|
||||
@@ -0,0 +1,152 @@
|
||||
# Native save durability and recovery
|
||||
|
||||
Read-only implementation design for audit finding 8, based on reliability worktree baseline `b3f661f`, inspected 2026-09-08. No production files were changed by this investigation.
|
||||
|
||||
## Decision
|
||||
|
||||
Keep the document as native JSON at the chosen path and its naming store at `vernier_ui::store_path(path)` (`<path>.names`). Add a **full-pair redo journal** beside them. A published journal contains the exact bytes of both new files. It must be durable before either live file changes. Open and Save complete any published transaction before using the live pair.
|
||||
|
||||
This preserves document format 27, naming-store format 5 and their existing migrations. It preserves the byte output of `Document::to_json()` and `NamingStore::to_json_for()` and introduces no time, random identifier, or new entity identity. A new document container would require changing direct JSON consumers in tests, the driver and CLI; a journal meets this audit's requirement with a smaller compatibility surface.
|
||||
|
||||
No new crate is needed. The workspace and installed compiler are Rust 1.96. `std::fs::File::try_lock` has been stable since 1.89. The app already has the domain deserializers required to validate journal payloads. Use a small fixed binary envelope rather than adding direct serde dependencies to the app merely to serialize transaction metadata.
|
||||
|
||||
## What the current code proves
|
||||
|
||||
- `crates/vernier-app/src/server/apply_files.rs` serializes both files, writes two unsynced temporaries, moves the old store to `.names.bak`, installs the new store, then installs the document. Ordinary errors try to restore the store, but restore errors are ignored. No file or directory is synced.
|
||||
- Its own comments identify new-document/old-store as a pair that can pass validation and silently remint faces. The opposite order is often refused, but **a transaction cannot rely on either direction always failing**: editing parameters may leave the allocator ceiling unchanged.
|
||||
- `crates/vernier-ui/src/compile/store.rs:813`, `validate_store`, checks allocator ceilings and recorded owner membership. It deliberately cannot establish that an arbitrary older store belongs to an otherwise plausible document. It is a structural validator, not a save-generation handshake.
|
||||
- `NamingStore::to_json_for` prunes only serialized tables/records and leaves session state intact for redo. Persistence must call it once and replay those bytes without calling the compiler or minting identities.
|
||||
- `apply_files` parses the incoming document and validates its store before discarding the outgoing cached prefix and replacing session state. Keep this ordering.
|
||||
- `tests/m0_store_cut.rs` checks round-trip face identities, missing/spliced-store refusal, explicit fresh identities, a failure on the first write, and a contrived last rename failure. It explicitly does not fault-inject crashes. `tests/m2_d_save_prune.rs` pins disk pruning and identity-preserving redo in the live session.
|
||||
- The CLI's `store-sidecar-roundtrip` check validates serialization/geometry; it does not currently execute the app's disk transaction. It cannot substitute for the new interruption tests.
|
||||
|
||||
## Files and framing
|
||||
|
||||
Reserve these names relative to the chosen native document (all in the same parent directory):
|
||||
|
||||
| Name | Meaning |
|
||||
|---|---|
|
||||
| `<path>` | Existing native document JSON |
|
||||
| `<path>.names` | Existing naming-store JSON |
|
||||
| `<path>.transaction` | Published full-pair redo record |
|
||||
| `<path>.transaction.tmp` | Unpublished journal staging file |
|
||||
| `<path>.transaction.document.tmp` | Replacement document staging file |
|
||||
| `<path>.transaction.names.tmp` | Replacement store staging file |
|
||||
|
||||
Use `PathBuf`/`OsString` suffix appending internally; do not use lossy conversion to derive a filesystem destination. Existing edit paths are UTF-8 strings, but canonicalized parent directories need not be.
|
||||
|
||||
Journal format v1 is `b"VERNIER-TXN\0"`, a little-endian `u32` version, little-endian `u64` document length, little-endian `u64` store length, then the exact UTF-8 document bytes followed by exact UTF-8 naming-store bytes. No absolute path, clock, process id, random number, compression or map iteration is recorded. The destination is established by the journal's reserved pathname, not by a path read from its contents.
|
||||
|
||||
The decoder rejects wrong magic/version, integer conversion or length overflow, truncation, trailing bytes, invalid UTF-8, invalid document JSON, invalid naming-store JSON and `validate_store` failures. Check lengths against the already-read buffer before slicing; never allocate using an unchecked journal length. Deserialize both strings for validation, but install the **original strings**, so legacy store migrations do not cause an unsolicited rewrite during recovery. The framing is not a cryptographic checksum and makes no claim to detect every bit flip that happens to remain valid data.
|
||||
|
||||
## Serialization of filesystem access
|
||||
|
||||
Open the canonicalized parent directory with `File::open`, acquire `try_lock()` exclusively and retain that handle through recovery and the complete read or write. Use that same handle for directory `sync_all()` calls. This is deliberately a **directory lock** on the Linux target: it needs no persistent `.lock` file, has no stale-file cleanup race, and permits ordinary legacy files to be read from a directory without write permission when no recovery is needed. Locks are advisory, and all new app native reads/writes must cooperate.
|
||||
|
||||
Local probe run with Rust 1.96: open a temporary directory twice; first exclusive `try_lock` succeeds, second returns `WouldBlock`; `sync_all` succeeds; dropping the first handle permits the second to lock and sync. Probe source is `/tmp/vernier-persistence-probe.rs`. This checks this Linux runtime, not portability to every filesystem. Unsupported locking or sync must fail explicitly, never silently downgrade durability. The Rust API does not promise exclusive locks on read-only handles on every platform. [Rust File API](https://doc.rust-lang.org/std/fs/struct.File.html#method.try_lock)
|
||||
|
||||
The cost is that two file operations for different documents in one directory briefly serialize. Use `try_lock` and a clear busy error rather than blocking the document worker indefinitely. Do not reacquire the lock inside helper functions. Ordinary directories, symlinked parent paths and relative path aliases converge on the same directory inode. A malicious directory rename/replacement or an uncooperative external writer is outside this locking contract.
|
||||
|
||||
Before publishing a transaction, reject a document/store destination that is an actual directory, and reject non-regular staging/journal entries, including staging symlinks. Destination symlinks retain current behavior: rename replaces the symlink itself rather than writing through it. Use `create_new` for staging after removing only reserved stale **regular** staging files under the lock. Never remove or overwrite a malformed published journal to get a save unstuck.
|
||||
|
||||
## Save protocol
|
||||
|
||||
1. Serialize `document.to_json()` and `store.to_json_for(&document)` before changing disk. Validate the resulting pair through the existing deserializers and `validate_store`; do not compile it.
|
||||
2. Acquire the parent-directory lock. Recover any existing published transaction first. If recovery is refused, preserve every file and refuse this new save.
|
||||
3. Preflight destination file types. A missing parent or directory in place of either destination fails here without changing the current pair. A permission/full-disk failure later is still possible; preflight is not a guarantee.
|
||||
4. Create the unpublished journal with `create_new`, `write_all` its framing and both payloads, then `sync_all` the journal file. Close it. Nothing live has changed yet.
|
||||
5. Rename journal temp to `<path>.transaction`; then `sync_all` the parent directory. **Do not touch a live file before this barrier succeeds.** After publication, every failure leaves the journal available for replay; do not attempt the old sidecar rollback.
|
||||
6. Write the exact journal document and store bytes to their reserved replacement temporaries, using `create_new`, `write_all`, and `sync_all` for each. Close both. A partially written temporary is not a recovery source.
|
||||
7. Rename the store temporary over `.names`, then rename the document temporary over the document. The order limits confusion for legacy readers, but correctness comes from the journal, not this order.
|
||||
8. `sync_all` the parent directory. Both replacement files were individually synced before rename, and their final names are now durable.
|
||||
9. Remove the published journal and `sync_all` the parent directory again. Only a fully successful protocol clears the application's unsaved flag and yields the ordinary saved acknowledgement.
|
||||
|
||||
Linux requires a separate directory sync for the durability of directory entries; syncing only file data does not establish it. Rename supplies atomic replacement of one name, not a two-file transaction. [Linux fsync documentation](https://man7.org/linux/man-pages/man2/fsync.2.html), [Linux rename documentation](https://man7.org/linux/man-pages/man2/rename.2.html)
|
||||
|
||||
The journal is immutable once published. Recovery uses steps 6–9 and can repeat them indefinitely. File names and bytes determine the outcome; there is no freshness heuristic and no choice based on allocator size.
|
||||
|
||||
## Interruption states
|
||||
|
||||
`D0/N0` is the previous pair; `D1/N1` is the intended saved pair. On a first save, both old names are absent. The table describes process interruption after the named operation; unsynced namespace changes may disappear after power failure.
|
||||
|
||||
| Interruption | Possible live names | Recovery action/result |
|
||||
|---|---|---|
|
||||
| Before journal creation, or during its write | D0/N0 | No published journal: load old pair; discard only reserved unpublished staging files when writing is possible |
|
||||
| After journal file sync, before publication | D0/N0 | Same; unpublished bytes never initiate recovery |
|
||||
| After journal rename, before directory sync | D0/N0 | If published journal exists, replay D1/N1; if rename was lost, use D0/N0. No live file was changed |
|
||||
| After published journal directory sync | D0/N0 | Replay D1/N1; a durable full recovery source exists |
|
||||
| During either replacement write/sync | D0/N0 | Discard staging bytes and regenerate both from the journal |
|
||||
| After store rename | D0/N1 | Replay **both** D1/N1; never try D0 with whichever store validates |
|
||||
| After document rename, before directory sync | D1/N1 in process; power loss may retain any old/new names | Replay both from durable journal |
|
||||
| After pair directory sync | D1/N1 durable | Replay is harmless; then retire journal |
|
||||
| After journal removal, before its directory sync | D1/N1 durable | If journal reappears after restart, replay exact same D1/N1; otherwise load D1/N1 |
|
||||
| After final directory sync | D1/N1 durable, no journal | Load D1/N1 |
|
||||
| Any interruption during recovery | Any intermediate pair, published journal retained until durable pair | Same table; recovery is idempotent |
|
||||
|
||||
Before publication, an ordinary error leaves the previous live pair unchanged. After publication, an ordinary error means the save may have committed, with repair or durability confirmation still pending. Keep the in-memory document dirty and report that distinction. Do not claim the previous version was restored. If final journal unlink succeeded but its directory sync failed, the journal may be absent now yet return after a crash; report durability uncertainty, not an assertion that a recovery file currently exists.
|
||||
|
||||
Under the stated filesystem/fsync contract, after restart there is either the old complete pair or the complete new pair obtainable from the journal. A first save interrupted before journal publication may leave no saved document; after durable publication it is recoverable. A recovery error caused by a now-unwritable directory must leave the journal intact and refuse Open without replacing the running document.
|
||||
|
||||
## Open and legacy behavior
|
||||
|
||||
Acquire the same lock and inspect the published journal **before reading the document**, including when the live document is missing. Validate the journal's full pair before mutating disk. Recover it, then read the live pair while retaining the lock. Only after all I/O, parsing and required store validation succeed may `apply_files` discard the outgoing memo and replace the session document/store.
|
||||
|
||||
An explicit fresh-identities Open still performs pending transaction recovery first, then deliberately creates `NamingStore::new()` instead of loading the settled sidecar. A malformed transaction never triggers a fresh-store fallback. Ordinary missing-sidecar messages retain the existing document-specific explanation of the cost of fresh identities.
|
||||
|
||||
When no journal exists, legacy native JSON plus `.names` loads exactly as before. Settled new saves remain directly readable by old builds and current CLI/driver JSON consumers. An old build does not know to replay a pending journal and can observe an interrupted pair; backwards compatibility applies to settled saves, not interrupted transactions opened with old software.
|
||||
|
||||
Old `<path>.names.bak` files need a deliberate conservative rule. Without a journal containing both versions, `validate_store` cannot prove which candidate belongs to the document. **Do not automatically restore a legacy backup.** If `.names.bak` is present without a new transaction, refuse ordinary Open/overwrite with a recovery diagnostic naming it and preserve all files for explicit recovery. This also makes stale backups from old successful saves visible; that inconvenience is preferable to inventing an identity handshake that the format lacks. Explicit fresh identities remains an intentional escape path when the user accepts its documented consequences. New code never creates `.names.bak`.
|
||||
|
||||
## Exact proposed module boundary
|
||||
|
||||
Add `crates/vernier-app/src/server/native_files.rs`; declare it from `server/mod.rs`. Keep `apply_files.rs` responsible for edit dispatch, naming-store pruning, missing-store/fresh-identity language, cached-prefix disposal and session flags. `native_files` owns reserved paths, directory locking, envelope decoding/validation, durable I/O and replay. It can use `vernier_doc` and `vernier_ui` directly, both already app dependencies. It does not call the kernel or worker.
|
||||
|
||||
Suggested crate-private interfaces (names are proposals, contracts are the requirement):
|
||||
|
||||
```rust
|
||||
pub(crate) struct NativeFile { /* canonical paths and held directory File */ }
|
||||
pub(crate) enum Recovery { None, Replayed }
|
||||
pub(crate) enum SavePhase { BeforePublication, Published, PairDurable, CleanupUncertain }
|
||||
pub(crate) struct FileFailure { /* destination, operation, phase, cause */ }
|
||||
|
||||
impl NativeFile {
|
||||
// Locks only. It must not guess a policy for a legacy .names.bak.
|
||||
pub(crate) fn acquire(path: &str) -> Result<Self, FileFailure>;
|
||||
// Validate and replay a published new-format journal before live reads.
|
||||
pub(crate) fn recover(&mut self) -> Result<Recovery, FileFailure>;
|
||||
pub(crate) fn read_document(&self) -> Result<String, FileFailure>;
|
||||
pub(crate) fn read_store(&self) -> Result<String, FileFailure>;
|
||||
// Requires recovery completed and legacy-backup policy checked.
|
||||
// Internally validates and writes the exact pair; returns only after final sync.
|
||||
pub(crate) fn save_pair(
|
||||
&mut self, document_json: &str, store_json: &str
|
||||
) -> Result<(), FileFailure>;
|
||||
}
|
||||
```
|
||||
|
||||
Use structured I/O error kinds to distinguish a missing sidecar from permission or corruption errors. Expose a small legacy-backup inspection method if needed; avoid making `apply_files` derive another copy of reserved suffixes. A typed outcome can carry `Recovery::Replayed` into the Open readout; the current `Result<bool, String>` edit arm can stay intact if the server stores a separate one-shot file-operation notice. Recovery must be reported as recovered, not silently described as an ordinary untouched Open.
|
||||
|
||||
`reachability.rs` already has a `Role::Production` row for `server/apply_files.rs`. Add a production row for `server/native_files.rs`, and a `Role::Tests` row if tests are a separate source file. Its directory walk intentionally fails when any app `.rs` file is missing. The scan strips a production file at its first `#[cfg(test)]`; put inline tests last and do not insert an early test-only helper before production functions. This module introduces no document command and needs no `NOT_YET` entry. The worker-blocking guard searches for `bounded(`; directory `try_lock` requires no new exception. Source reachability says nothing about durable behavior, so it is only one check, not acceptance.
|
||||
|
||||
## Fault injection and acceptance
|
||||
|
||||
Use one small filesystem seam, private to this module, covering write/create, sync-file, rename, remove and sync-directory. The production implementation calls std. Test injection must happen **at real protocol operations**, never in a separately reimplemented save state machine. At minimum expose an after-operation callback behind test configuration at the end of the source file; compile the production sequence unchanged for interruption tests. Cleanup must not run in `Drop`, because an interrupted process does not get to roll back.
|
||||
|
||||
1. Run a child test process that writes a distinctly changed pair and exits immediately at each operation boundary. Parent opens through the production reader and checks the old or new pair as specified by the table. Bound child execution and distinguish timeout, intended exit, and unexpected panic. Exiting at a callback is a deterministic crash boundary test; it is not a power-cut experiment.
|
||||
2. Inject ordinary errors at every create/write/file-sync/rename/directory-sync/remove operation, including both publication and cleanup directory syncs. Assert exact phase and diagnostic, dirty status, current-session preservation, journal retention where applicable, and successful subsequent recovery. The existing directory-at-destination tests should now fail at preflight, leaving the store byte-identical and no orphan sidecar.
|
||||
3. Model volatile file contents and directory entries separately from durable snapshots. A simulated power loss discards unsynced content/namespace changes; enumerate allowed persisted subsets between barriers. A single snapshot taken only after successful sync is insufficient to exercise unsynced rename survival. Recover all generated states using the production protocol through the same filesystem seam. This verifies ordering under the model; physical storage honesty remains a stated assumption.
|
||||
4. Repeat all interruption cases for first save, replacement save, and an interruption during recovery itself. Test missing live document with valid journal; old document/new store; new document/old store; neither live name; and both already new. The reader must always use the full journal pair, even when a mixed pair happens to pass `validate_store`.
|
||||
5. Use a shelled/filleted part with persisted face references, then edit upstream geometry and add a feature to make both bytes and identity sets distinguishable. Assert document bytes, naming bytes, sorted face identities and exported volume/STEP or STL geometry after recovery and a second compile. Include a parameter-only edit where the allocator ceiling does not change, and a pruned save after undo with redo identity retention in the running session.
|
||||
6. Test malformed framing, overflow, invalid JSON, wrong store version, wrong store/document pair, trailing bytes, malformed published journal beside a valid live pair, stale unpublished temp, legacy `.names.bak`, destination/staging symlinks, a nonempty directory at either destination, directory lock contention and unsupported sync errors. Invalid published journals must neither overwrite good files nor be discarded.
|
||||
7. Compare settled document/store/journal bytes across two independent processes given the same serialized input. Staging path metadata and filesystem timestamps are not document artifacts and must not be included in determinism hashes.
|
||||
8. Add a real-control driven script that saves, reopens a recovered file and exports geometry. To create the interruption, run the crash helper between driver invocations; do not add arbitrary filesystem-fault controls to the user command palette. Retain existing selftest geometry/naming checks and run the source reachability guard plus workspace checks.
|
||||
|
||||
Useful negative controls are deleting each durability barrier, switching recovery to load a live pair first, replaying only the document, calling `to_json()` instead of `to_json_for()`, and reminting the recovered store. The persistence tests should fail for each prescribed mutation for its intended reason. Without the filesystem model, child-exit tests alone will usually remain green when fsync calls are removed; they cannot justify a power-loss durability claim.
|
||||
|
||||
## Remaining risks and scope
|
||||
|
||||
The journal duplicates the document/store payloads and the temporary replacement pair, so replacement saves temporarily need roughly two additional pair sizes beyond the existing files. ENOSPC after publication leaves a durable recovery source but can prevent completion until space is freed. Do not delete that source to recover space automatically.
|
||||
|
||||
Directory locking is intentionally Linux-specific behavior verified on this host; network filesystems, external programs ignoring locks, older builds, and storage that lies about flush completion are outside the guarantee. Recovery writes to the selected directory and can fail on read-only media. Silent bit rot and deliberate journal tampering are not solved by fsync or structural validation.
|
||||
|
||||
This work recovers interrupted **explicit saves**. It does not recover unsaved edits that never entered this protocol and is not autosave. Autosave can later reuse the exact pair writer at a separate recovery destination, but it must carry explicit dirty-state and file-selection policy rather than overwrite the user's last saved document invisibly.
|
||||
@@ -0,0 +1,36 @@
|
||||
# Independent native-save design review
|
||||
|
||||
Reviewed 2026-09-08 against `docs/PERSISTENCE_DESIGN_2026-09-08.md`, reliability worktree `server/apply_files.rs`, `server/mod.rs`, `vernier-ui/src/compile/store.rs`, and the applicable AGENTS.md invariants. Read-only review of production code; this report is the only edit. No implementation or crash experiment was run.
|
||||
|
||||
**Verdict: revise before implementation acceptance.** Full-pair redo is appropriate and the normal save barriers are correctly ordered. Recovery, supported path types, and reserved-name ownership need the corrections below. The existing allocator/owner validator is explicitly not an identity handshake.
|
||||
|
||||
## Required corrections
|
||||
|
||||
1. **P1 — recovery must make journal publication durable before replay.** The design says recovery uses steps 6–9. A process can exit after the journal rename but before step 5's directory sync; the next process sees that journal, although its name is not yet durable. Recovery can then rename the new store and suffer a power loss. Under the design's allowed persisted-subsets model, the store rename can survive while journal publication does not, leaving an unrecoverable mixed pair. Reading and validating a visible journal does not establish durability. After validating it, recovery must successfully sync the parent directory before any live replacement. The journal's contents were already synced before publication by the writer; make that precondition explicit, or conservatively sync the journal handle too. A failed recovery barrier must preserve the running session and avoid live mutation.
|
||||
|
||||
**Decisive test:** stop a writer immediately after publication rename, retaining a volatile journal name; resume production recovery without simulating power loss first; cut power after its first live rename. Enumerate namespace persistence subsets. Removing recovery's publication barrier must produce a no-journal mixed pair. A child-exit test alone cannot establish this.
|
||||
|
||||
2. **P1 — leaf symlinks invalidate the cooperative locking claim.** Canonicalizing the parent unifies symlinked directories, but not a document or sidecar that is itself a symlink. Open currently follows both leaf names. An alias pair in directory A can refer to a real pair in B: Open locks A and reads the old document; a cooperating Save locks B and replaces the pair; Open reads the new sidecar. The directory lock has not serialized access, and a parameter-only mixed pair can pass validation. This does not require a malicious writer or directory replacement.
|
||||
|
||||
**Correction:** explicitly refuse leaf document/sidecar symlinks for Open, with a diagnostic directing the user to the actual native document, or design a canonical-pair resolution rule that verifies both referents belong to one pair and locks the actual parent before either read. Preserve the documented Save-as behavior of replacing destination symlinks if desired; Open and Save need not have identical policies. Do not claim all path aliases are protected by parent canonicalization alone.
|
||||
|
||||
**Decisive test:** create both aliases in another directory and attempt the old-document/new-store interleaving with two cooperating native-file instances/processes. The operation must refuse the alias before reads or contend on the same lock. Include a same-directory document alias whose sidecar points elsewhere.
|
||||
|
||||
3. **P2 — regular-file checks must cover live reads, not only staging/journal entries.** The proposal rejects directories as live save destinations, but does not require regular live document/store files before `read_to_string`. Opening a FIFO with no writer blocks the worker indefinitely; a device can yield an unbounded stream. Holding the directory lock also prevents other native operations in that directory. This gap is inherited from `apply_files.rs`, but the replacement reader must not preserve it under the requested hang-safety acceptance.
|
||||
|
||||
**Correction:** inspect live entry types before opening for reads and refuse FIFOs, sockets, devices, and directories; combine this with the leaf-symlink policy above. External replacement races remain outside the stated cooperative-writer threat model. Do not describe try-lock as making filesystem I/O universally time-bounded; stalled storage itself remains a limitation.
|
||||
|
||||
**Decisive test:** document FIFO and sidecar FIFO separately, with no producer, in bounded child processes. Require immediate typed refusal and unchanged running document; distinguish timeout from panic or the intended error.
|
||||
|
||||
4. **P2 — reserve auxiliary names across document choices.** Native documents have no required extension. With a pending transaction for `part`, a user can choose `part.transaction` as another document's Save-as destination. Its own journal is `part.transaction.transaction`; recovering that name does not discover the pending transaction for `part`. The second save overwrites `part.transaction`, destroying the first save's sole recovery source. Directory locking only serializes this loss. Similar collisions exist with replacement staging names and the existing `.names` convention.
|
||||
|
||||
**Correction:** define and enforce a reserved-leaf-suffix policy for native Save destinations (at least every journal/staging suffix, and explicitly decide the existing sidecar/backup suffixes). Refuse reserved names before recovery or mutation. Preserve legacy files and explain how to save them under an ordinary document name; do not automatically relocate or delete anything.
|
||||
|
||||
**Decisive test:** leave `part` interrupted after its store rename, then request Save-as to its journal and each staging pathname. Every collision must fail without changing any bytes, and normal recovery of `part` must still restore its exact intended pair.
|
||||
|
||||
## Identity, legacy, and acceptance notes
|
||||
|
||||
- Keep the proposed conservative `.names.bak` refusal. Neither a validator pass nor successful compilation proves which legacy store is authentic. State explicitly that fresh-identity opening requires Save-as to a different unblocked path if the backup remains; it is not automatic backup recovery.
|
||||
- Interpret “wrong store/document pair” tests as structurally detectable mismatches only. Also include a deliberately mixed pair that passes `validate_store`, proving replay uses both journal payloads rather than deciding validity by that validator. Do not add entropy or pretend this format prevents arbitrary external pair substitution.
|
||||
- Keep malformed journals fail-closed even next to a valid live pair, preserve outgoing memo/state until all reads validate, keep serialized pruning separate from in-memory redo, and retain the phase-specific durability-uncertainty wording. Those parts of the proposal address real defects in the current code.
|
||||
- Accept only with the proposed production-path fault seam, a volatile/durable filesystem model, bounded process tests, and the driven save/recover/export geometry gate. Require negative controls for **both** publication barriers (writer and recovery). Ordinary exits establish process-crash behavior; they do not establish power-loss durability.
|
||||
@@ -0,0 +1,59 @@
|
||||
# Independent native-save implementation review
|
||||
|
||||
Reviewed 2026-09-08 against baseline `b3f661f` and the uncommitted reliability worktree at `target/worktrees/reliability`, branch `codex/reliability-and-function-verification`. Scope: `server/native_files.rs`, `server/native_files_tests.rs`, `server/apply_files.rs`, their server integration, the persistence tests in `tests/m0_store_cut.rs`, and the recovery integration test in `tests/headless.rs`. Read the persistence design, independent design review and implementation record in the original checkout. No production or test files were modified by this review.
|
||||
|
||||
**Final verdict after focused re-review: no remaining blocker found under the stated cooperative Linux filesystem contract. The P2 application-state finding below was reproduced and corrected by the root agent, then independently re-reviewed.**
|
||||
|
||||
## Parallel-process lock lifetime follow-up — corrected and verified
|
||||
|
||||
The root's first full workspace run exposed `WouldBlock` while the symlink test reacquired its directory immediately after dropping the native guard. The proposed explanation was a concurrent child spawn temporarily retaining a duplicate of the directory's open-file description. Linux flock locks are shared by duplicated/fork-inherited descriptors and are released by explicit unlock or closure of all duplicates; a close-on-exec flag does not eliminate the interval before exec. The duplicate-handle regression verifies this relevant mechanism without needing nondeterministic process timing. [Linux flock documentation](https://man7.org/linux/man-pages/man2/flock.2.html)
|
||||
|
||||
`Disk::drop` in `server/native_files.rs:131-138` now explicitly unlocks its private directory file. The reviewer verified that the sole production `Disk { directory }` construction follows a successful `directory.try_lock()`, that no production code clones the handle or constructs a second owning guard around a clone, and that the lock remains held across the native operation's recovery and reads/writes. The crash harness moves the complete `Disk` into `CrashDisk`; it does not drop an intermediate guard or unlock early. `NativeFile` itself has no custom destructor, so this move remains valid. Process-exit injection still bypasses destructors and retains its original crash semantics.
|
||||
|
||||
`dropping_the_native_guard_releases_its_lock_even_with_an_inherited_descriptor` keeps a `try_clone()` handle alive, first confirms another native acquisition is blocked while the owner lives, then drops the owner and requires reacquisition to succeed before dropping the duplicate. `/tmp/vernier-native-unlock-red.log` fails at that reacquisition with `WouldBlock`; `/tmp/vernier-native-unlock-green.log` passes after explicit unlock. The reviewer independently reran the corrected exact test through the built app test executable successfully. The original full-run fork timing remains an attribution hypothesis, but its lock-lifetime mechanism is now deterministically reproduced and corrected.
|
||||
|
||||
## P2 disposition — corrected and verified
|
||||
|
||||
The root's correction at `crates/vernier-app/src/server/apply_files.rs:36-54` keeps the typed recovery failure until its phase has been inspected and marks the running document dirty for every phase after publication. Successful `Recovery::Replayed` also marks it dirty before starting the requested save, so a later prepublication refusal cannot retain the obsolete clean marker. Only a fully successful requested save clears it. Both branches identified below are covered.
|
||||
|
||||
`failed_save_after_pending_recovery_never_leaves_the_running_document_clean` in `crates/vernier-app/src/tests/m0_store_cut.rs:1086` begins from a successfully saved clean A and a distinct pending B. It exercises successful B replay followed by legacy-backup refusal, asserting B really reached disk while A stayed in memory; its second case obstructs staging during published recovery. Both require the failed Save to leave A dirty. The review inspected `/tmp/vernier-recovery-dirty-red.log`, which fails at that dirty-state assertion, and `/tmp/vernier-recovery-dirty-green.log`, which passes the corrected test. The reviewer also independently reran that exact test through the built app test executable successfully.
|
||||
|
||||
A refusal before publication with no recovered transaction still preserves a valid clean marker: acquisition/serialization errors do not mutate it, `Recovery::None` does not set it, and the `BeforePublication` save error branch leaves it unchanged. The full pair remains untouched in that path. This clean-case conclusion is a control-flow check, not a new dedicated integration test. No production or test edits were made by the reviewer during the re-review.
|
||||
|
||||
## Original P2 finding — include preliminary recovery in Save's dirty-state handling
|
||||
|
||||
Location: `crates/vernier-app/src/server/apply_files.rs:36-41` in the reliability worktree.
|
||||
|
||||
`SaveDocument` calls `native.recover()` with an immediate stringified `?`, before the handler which marks the running document dirty after a published-transaction failure. A recovery error therefore bypasses that protection, even when its `FileFailure.phase` is `Published`, `PairDurable` or `CleanupUncertain`. A clean running document can remain marked clean after Save reports that the on-disk pair may have changed. `DocumentServer::handle` does not repair this: its error path retains the arm's dirty flag, and only successful command/undo changes mark it dirty.
|
||||
|
||||
There is a second branch of the same defect. If preliminary recovery succeeds and installs a different pending generation, a subsequent `save_pair` failure with `BeforePublication` also leaves the running document clean. That phase describes only the new transaction; it does not undo the preceding recovery's disk changes. The current condition forgets `Recovery::Replayed`.
|
||||
|
||||
Reproduction for an integration regression: start with a clean running document A and a valid pending journal for different generation B at its native path. Fail a recovery installation or cleanup operation, then issue `SaveDocument` and assert an error, unchanged in-memory document/naming state and `scene.view.unsaved == true`. The first two assertions currently hold, but the dirty assertion fails by the control flow above. This can arise with another cooperating process's interrupted save and a subsequent filesystem failure; a malformed journal also reaches the bypass without fault injection. A deterministic fixture for the second branch is a valid B journal alongside a legacy `.names.bak`: recovery installs B, the new save refuses the backup, and the disk is B while the running A still carries a clean marker. The combined fixture is a regression instrument, not a claim that the new writer itself creates legacy backups.
|
||||
|
||||
Correction: handle the typed failure from preliminary recovery before converting it to a string; retain a dirty flag on post-publication recovery failures. Once recovery has replayed a pair, keep the running state dirty until the requested new save finishes successfully, including errors before the new journal is published. Add server-level assertions beginning with `unsaved == false`. The existing model's phase/error tests exercise `Transaction` directly and cannot observe this application-state defect. The malformed-journal server test covers Open from an already-dirty session, so it cannot expose it either.
|
||||
|
||||
This finding is based on traced production control flow; the review did not add a reproducer test to another agent's owned files.
|
||||
|
||||
## Protocol and instrument assessment
|
||||
|
||||
- Writer ordering is correct: journal file sync, publication rename and directory barrier, separately synced replacement payloads, both live renames and pair directory barrier, then journal removal and final directory barrier. Errors leave a replay source until the pair is durable. The final cleanup-sync failure is described as uncertainty, without falsely claiming a journal currently exists.
|
||||
- Recovery validates the full envelope before changing disk and syncs both the visible journal and its publication before touching live names. This addresses the prior design review's process-exit-then-power-loss counterexample. It installs the exact recorded strings, never recompiles or chooses a store according to allocator plausibility.
|
||||
- The directory lock is held across recovery and both native reads. Parent aliases canonicalize to the same lock. Leaf symlinks are refused for reads; save replaces the link entry, preserving its referent. Nonregular live and staging entries are refused before opening. Hard-linked files remain safe under cooperating writers because writes create new inodes and rename names, never overwrite the shared inode; a hard-link alias stays an old snapshot rather than observing half an in-place mutation. Hard-link behavior is code-inspected, not directly tested by this suite.
|
||||
- Reserved leaf suffixes prevent native Save/Open from selecting another document's journal, staging, sidecar or backup as a document. Stale regular staging entries are unlinked and recreated with `create_new`. A malformed published journal is preserved and fails closed. Legacy backups are never guessed into an identity pairing.
|
||||
- The fault model runs the actual generic production transaction sequence. File data and directory bindings have separate durability snapshots; unsynced namespace operations and file-content persistence are enumerated independently. Mutation controls omit the writer/recovery publication barriers, each writer payload-file sync, and pair-installation barrier and require a counterexample. They are useful negative controls, not tests against a second handwritten save algorithm.
|
||||
- Bounded real children exit after actual filesystem operations; their harness distinguishes expected exit 77, timeout, panic and signal. These prove process-interruption behavior, while the separate model supplies the power-loss ordering evidence. Model writes represent whole writes; partial unsynced writes are not explicitly enumerated, but production never publishes unsynced journal bytes or treats replacement staging bytes as authoritative.
|
||||
- Geometry tests use a shell with persisted references, a parameter-only edit whose mixed pair passes structural validation, and a topology-changing fillet. They compare exact document/store bytes and face identities, then recompile/export and import STEP into an independent kernel. The driven integration test genuinely clicks Save/Open/Export through the shell. Its journal layout is synthetic and correctly described as such; actual writer interruption is covered separately.
|
||||
|
||||
## Fresh verification
|
||||
|
||||
`CARGO_TARGET_DIR=/home/nilsb/Documents/Projects/VernierCAD/target cargo test -p vernier-app --lib server::native_files::tests -- --test-threads=1` passed: 15 tests, zero failures/ignored, including both omission-control tests and all bounded child cases. Log: `/tmp/vernier-persistence-review-tests.log`.
|
||||
|
||||
With the same target directory, `cargo test -p vernier-app --test headless palette_open_recovers_a_saved_shell_and_exports_its_exact_geometry -- --nocapture` passed (one test, 1.32 s), and `cargo test -p vernier-app --lib recovery_replays_real_parameter_and_topology_generations -- --nocapture` passed (one test, 0.21 s). Logs: `/tmp/vernier-persistence-review-headless.log` and `/tmp/vernier-persistence-review-generations.log`.
|
||||
|
||||
The first attempted run encountered the concurrently added sketch fixture missing `snap_tolerance_mm`; after the owning agent supplied that field, the focused run above compiled and passed. This was not a persistence failure.
|
||||
|
||||
## Residual limits
|
||||
|
||||
The guarantee assumes Linux advisory-lock cooperation, same-directory atomic rename and honest file/directory sync on supported storage. There is no physical power-cut, real ENOSPC, failed-media or filesystem-vendor certification here. Regular-file reads and filesystem calls themselves can stall; try-lock does not bound storage latency. External in-place writers, malicious directory replacement, old builds ignoring the journal, arbitrary valid-pair substitution and undetectable bit corruption remain outside this protocol. Session-level concurrent editing is not conflict detection. This is recovery of explicit saves, not recovery of edits never submitted to Save.
|
||||
|
||||
The full workspace gate and combined changes from other active lanes are outside this review's verification claim.
|
||||
@@ -0,0 +1,68 @@
|
||||
# Independent sketch reliability review
|
||||
|
||||
Reviewed 2026-09-08 against baseline `b3f661f` and the uncommitted `target/worktrees/reliability` checkout on `codex/reliability-and-function-verification`. Scope: R4 screen-derived drawing snaps and Alt override; R6 circle profile completion; R7 successful-extrusion Cut reset; the driver's value-card chip targeting; associated unit tests and driven acceptance evidence. This report is the review's only file edit.
|
||||
|
||||
**Final verdict after focused re-reviews: both P2 findings are corrected and verified. No remaining blocker was found within this review's sketch/targeting scope. Final combined workspace/release acceptance remains the root integration gate.**
|
||||
|
||||
## Final Cut-barrier verification
|
||||
|
||||
The remaining independent-Area edge is now corrected in `shell/value_card.rs`: the card Area explicitly receives `.enabled(ui.is_enabled())`, and its direct Enter/Escape reads run only when that UI is enabled. The disabled parent therefore blocks both actual chip widgets and raw-key commits while the ordered extrusion response is pending. Combined with the previously reviewed dispatch/viewport barrier, a later Cut choice cannot enter before the older response consumes its own mode. Close remains routed separately.
|
||||
|
||||
`an_inflight_extrusion_does_not_accept_card_input` exercises the real shell with a held worker channel. Its positive control clicks the visible idle Cut chip and verifies the boolean changes. It submits an extrusion, then clicks the same chip while its result is held and asserts that the choice stays unchanged. `/tmp/vernier-pending-card-red.log` fails at that exact disabled-Area assertion before the fix; `/tmp/vernier-pending-card-green.log` passes afterward. The reviewer independently reran the exact test through the built app test executable successfully. **The asynchronous Cut P2 is closed.**
|
||||
|
||||
## First focused re-review disposition (historical)
|
||||
|
||||
The root added an ordered `document_barrier` for extrusion, Open and Save. Submission records the response count through the operation, `apply_scene` decrements it, viewport mouse/edit-key routing is gated, and `dispatch` now refuses another action until the ordered response arrives. Close remains available. The new pending-worker regression `an_extrusion_waits_for_its_result_before_accepting_another_operation` reproduced the queued-command defect with pending count 2 before the fix, then passed with count 1. The reviewer inspected `/tmp/vernier-extrusion-barrier-{red,green}.log` and independently reran the exact test successfully.
|
||||
|
||||
**P2 edge found in that intermediate revision (now corrected above):** `run_shell_frame` at `app.rs:1203-1205` disabled its root `ui`, but `value_card.rs:557-560` created `egui::Area::new(...).show(ui.ctx(), ...)` with default enabled state. An Area creates a fresh root UI and only disables it when its own `.enabled(false)` is set (installed egui 0.36.1 `containers/area.rs:614-629`); `Ui::disable` only sets the original UI's local enabled flag (`ui.rs:496-501`). The card's Cut chip could therefore still change while the barrier was pending, and the older extrusion response cleared that newer state. The dispatch regression proved no second command was queued but did not click the chip. The final correction propagates disabled state and guards raw key reads, with the real chip test described above.
|
||||
|
||||
The stale-card fix now resets `ShellState.value_card_rect` at the start of every `shell::show`, records `area.response.rect` only when the current value-card painter's UI is visible, and copies that exact field into Headless's frame. All early returns and card suppression paths leave it absent; egui's invisible sizing pass does not create a target. The hidden-card regression uses a real face pick as its positive control and the real palette trigger to hide it; it checks both missing card text and missing bounds. `/tmp/vernier-card-rect-{red,green}.log` records the assertion failing before and passing after the fix. The reviewer independently reran the corrected headless test successfully. **This P2 is closed.**
|
||||
|
||||
The root also corrected the automatic-circle-Profile regression in session openers: `Selection::Profile` now retains Extrude while adding the same no-prior-pick actions available for `Selection::None`. A profile represents available extrusion input, so keeping builder openers available is consistent with that selection's role. The reviewer inspected the corresponding toolbar change and the `/tmp/vernier-pattern-circle-profile-green/report.json` pass: `m3-c-sketch-pattern`, cross-process deterministic, two distinct process IDs. The original script's single stale selection expectation now describes the completed circle as a profile. The other agent's unfinished solid-builder ribbon work was deliberately excluded from this review.
|
||||
|
||||
## Original P2 — an older extrusion result can erase a newer Cut choice
|
||||
|
||||
Location: `crates/vernier-app/src/app.rs:797-799`, the unconditional `extrusion_applied` reset in `apply_scene`.
|
||||
|
||||
The success echo correctly distinguishes a committed extrusion from a refused command or failed compile. It does not distinguish the choice submitted with that extrusion from a newer choice made while the worker computes it. The general controls remain interactive during pending geometry work: the shell is disabled for close/file/failure states, and `dispatch` gates only file commands on `pending_jobs`. The extrude card can therefore still change `shell.extrude_cut` before the outstanding extrusion response arrives. Applying that old response sets the new choice back to false. A sufficiently slow compile makes the next intended cut become a boss or join without the user's intended Cut selection surviving.
|
||||
|
||||
Reproduction sequence for a bounded test: hold extrusion A's worker response after submission; while A remains pending, let the user change the Cut chip to the desired state for the next operation (if A was already Cut, toggle off and back on); then deliver A's successful scene. The current code clears the newer choice. A second submission can also be queued before A returns, still using A's unconsumed mode. The new frame test immediately applies the response, and `cut-then-boss.json` waits idle before beginning the next operation, so neither exposes this interleaving.
|
||||
|
||||
Correction: either make extrusion completion a real interaction barrier, preventing new modeling/chip actions until its outcome arrives, or version the submitted operation's Cut choice so its completion cannot clear a newer choice. Merely checking whether the boolean currently equals the submitted value misses an off/on change. Include a delayed-response test with an intervening user choice, or prove the chosen barrier prevents it; keep the existing success/refusal checks.
|
||||
|
||||
This is a control-flow finding; the reviewer did not edit another lane's test files to add the interleaving reproducer.
|
||||
|
||||
## Original P2 — a remembered Area rectangle does not prove a card is painted now
|
||||
|
||||
Locations: `crates/vernier-app/src/headless.rs:452-455` and `crates/vernier-drive/src/run.rs:1348-1363`.
|
||||
|
||||
`FrameOutput.value_card_rect` is read from `memory.area_rect("vernier-value-card")`. The installed egui 0.36.1 implementation documents this API as obtaining the **previous** rectangle and returns stored area state without checking visibility (`egui/src/memory/mod.rs:1014-1016`). Hiding a card retains its geometry. Opening the command palette deliberately suppresses the card in the same frame, and deselecting or starting a sketch can also make it absent, but `value_card_rect` stays `Some(old_rect)`.
|
||||
|
||||
The chip resolver searches all currently painted text and only checks whether its bounds lie in that remembered rectangle. Any same-label text from a palette or popup overlapping the old card footprint can therefore be accepted as a card chip, even though no card exists. The new spatial restriction resolves the observed simultaneously painted ribbon/card `cut` collision, but it is not a current-card ownership check. An acceptance instrument must refuse a missing control instead of clicking another surface.
|
||||
|
||||
Reproduction for the target instrument: render and settle a card, hide it by opening the palette or removing its selection, then inspect the next `FrameOutput`: the current implementation still returns the old rectangle. With a matching label on another surface inside that footprint, `chip:<label>` resolves there. A small synthetic overlapping-label frame is appropriate for pinning the resolver, combined with a driven show/hide test that verifies the production frame exposes no card after it is hidden.
|
||||
|
||||
Correction: propagate the current frame's card rectangle from the actual painter result, or filter the actual card's current-frame layer/visibility. Do not use remembered bounds alone. Check carefully which egui visibility set represents the completed frame: `is_visible` includes current and previous sets, so it can retain the same one-frame ambiguity before end-of-pass cleanup. Test that a hidden card yields no chip target despite matching text in its former footprint.
|
||||
|
||||
## Snapping and profile assessment
|
||||
|
||||
- `draw_snap_tolerance_mm` uses the same camera ray and sketch frame as the actual click. Its two finite differences form the local screen-to-plane Jacobian; the smaller singular value provides a conservative isotropic radius against oblique foreshortening. The determinant-over-largest-eigenvalue expression avoids cancellation. Missing intersections, degenerate viewports and nonfinite/zero results disable snapping rather than fabricating a model coordinate.
|
||||
- The worker receives the click-time tolerance, keeping camera data off the document worker. All three prior generic/rectangle/centre-rectangle snap sites use it. `None` maps to zero, and the existing `snap_point` early return disables coincidence, axis and grid snapping entirely. The real app also suppresses inference when Alt is held, so the override does not merely turn off one snapping tier.
|
||||
- Circle rim clicks remain distance measurements and are not snapped to a nearby point. On completion, the created circle is passed through `extend_chain`, which recognizes it as its own profile while preserving the existing construction-geometry refusal. A new centre clears the preceding completed profile, and ordinary endpoint chains retain their existing identity-based joining rule.
|
||||
- `Scene.extrusion_applied` is set only for a dispatched extrusion whose command and compile succeeded. Failed commands, failed compiles and unrelated scene updates do not spend Cut. The problem above is the lifetime of a later UI choice, not the success predicate itself.
|
||||
|
||||
## Acceptance evidence checked
|
||||
|
||||
The reviewer read the four reports under `/tmp/vernier-small-profile-green`, `/tmp/vernier-small-profile-80-green`, `/tmp/vernier-snap-alt-override` and `/tmp/vernier-cut-then-boss-green`. Each reports pass on llvmpipe, cross-process determinism and two distinct process IDs. The corresponding scripts drive actual canvas clicks and value-card typing, assert profile completion, and inspect exported STEP geometry. The two small-profile scripts assert a 1 x 1 x 2 mm solid at camera distances 40 and 80. The cut/boss script types radii to remove pixel quantization and checks separate analytic negative and positive volume deltas.
|
||||
|
||||
The Alt negative-control log `/tmp/vernier-snap-without-alt.log` fails at its own step 9, expecting a profile where normal snapping collapsed it. The positive Alt script uses a stated 30 percent volume tolerance for its submillimetre pixel-quantized rectangle; topology/profile existence and that negative control distinguish it from a swallowed click. It does not establish exact freehand dimensions. Export helper steps are correctly described as geometry measurement; actual export-control acceptance is a separate script.
|
||||
|
||||
`/tmp/vernier-app-combined.log` reports 277 app library tests, 19 headless integration tests and 6 edge/hover integration tests passing. The reviewer independently reran four exact tests through the built app test executable; all passed: `drawing_snap_tracks_zoom_and_stays_within_a_screen_radius_on_oblique_planes`, `explicit_snap_override_preserves_small_geometry_and_off_grid_coordinates`, `a_completed_extrusion_spends_cut_mode_but_a_refusal_keeps_the_choice`, and `two_clicks_and_a_button_turn_a_circle_into_a_solid`.
|
||||
|
||||
## Limits and follow-up scope
|
||||
|
||||
The six-pixel conversion is a local approximation on a perspective plane, not an exact global screen-distance guarantee at grazing angles. Existing snapping applies axis/grid tolerances independently, so combined two-axis displacement can exceed the isotropic six-pixel circle by up to roughly sqrt(2) locally. The unit test surveys the isotropic radius at the viewport center for several camera angles/distances; it does not test every resulting `snap_point` displacement, off-center horizon case or high-DPI logical-pixel interpretation. This does not undermine the demonstrated small-profile fix, but the documentation should retain the local/conservative qualification.
|
||||
|
||||
`DRAW_SNAP_MM` still exists for `MoveSketchPoint` in `apply_curve_edit.rs`; this lane changes drawing-click snapping, not every sketch manipulation. Its old declaration comment describes the historical drawing default and is now stale explanatory text. No change to drag snapping is claimed here.
|
||||
|
||||
The root's original driven-script regression sweep is ongoing independently. Its final outcome, combined edits from other lanes, and the full workspace gate are not claimed by this review.
|
||||
@@ -0,0 +1,60 @@
|
||||
# Independent solid-builder ribbon review
|
||||
|
||||
Reviewed 2026-09-08 against baseline `b3f661f` and the uncommitted reliability worktree. Scope is the fixed-slot `session_group` / `session_width` / `fixed_slot_ready` change and its two new tests in `crates/vernier-ui/src/shell/ribbon.rs`, plus the three `functions-{revolve,sweep,loft}.json` scripts and their verification handoff. Earlier file-command changes sharing `ribbon.rs` were excluded. This report is the only review edit.
|
||||
|
||||
**Final verdict after re-review: the P2 readiness mismatch is corrected, and the updated scripts now explicitly verify undo/redo and Cancel exit. No remaining blocker was found in this bounded solid-builder scope.**
|
||||
|
||||
## Final re-review — 2026-09-08
|
||||
|
||||
The builder agent confirmed the final production/scripts handoff was frozen before this review. `FeatureInput::accepts` in `vernier-ui/src/shell/mod.rs` now owns the slot's pick-space and cardinality rule: exact two-point axes, one-face sketch planes, valid-length fillet face lists, sufficient loft sections and nonempty curve inputs. Empty SweepGuide remains intentionally valid. `CreateSession::ready` checks every slot with that predicate; `fixed_slot_ready` accepts an Edit only when one of its deliberately filled slots passes it. The app's `slot_command` applies the same predicate before extracting picks. Its removed local length guards therefore do not permit overfull axes: the shared guard runs first. Create-session construction also checks `session.ready` before extracting either axis endpoint.
|
||||
|
||||
The new `a_revolve_axis_needs_exactly_two_points_to_paint_commit` test holds the create profile filled and checks one, two and three axis picks for both Create and Edit. Commit is painted only for two. The reviewer freshly ran all 9 ribbon overflow tests (including this test and width comparison): all passed, 0.33 s, log `/tmp/vernier-solid-builders-final-review-tests.log`. The reviewer also ran `cargo test -p vernier-app --lib tests::sessions -- --test-threads=1`: all 18 matched app session tests passed, 0.35 s, log `/tmp/vernier-solid-builders-final-review-sessions.log`. These runs used the shared target directory from the earlier command. **The P2 is closed.**
|
||||
|
||||
The final scripts now send actual `ctrl+z` and `ctrl+y` after creating each solid. They assert the timeline falls from two rows to the sole sketch after Undo and returns to two after Redo, then export/check the recreated solid. Every script also asserts selection `none` after the final Cancel, as well as an unchanged two-feature timeline and no error. These changes close the two earlier evidence gaps; keyboard undo/redo does not establish the separate painted title-button route.
|
||||
|
||||
The reviewer independently inspected the final script steps, traces and aggregate reports in `/tmp/vernier-functions-revolve-review/report.json`, `/tmp/vernier-functions-sweep-review/report.json` and `/tmp/vernier-functions-loft-review/report.json`. All pass in separate processes with `deterministic: true`, at 88, 96 and 82 steps respectively. Original/redone STEP digests match for each: Revolve `0xba2ad81f55edaa83`, Sweep `0x7b0cc95e60106851`, Loft `0xb338986ad120fefc`. The Revolve/Sweep additional parameter-edit-and-restore checks remain present. The reviewer inspected these agent-produced release reports; the fresh reviewer executions were the focused Rust suites stated above.
|
||||
|
||||
What follows records the initial review and its then-current scripts for provenance; the disposition and evidence in this section supersede its open-finding wording.
|
||||
|
||||
## Original P2 — nonempty picks are not sufficient to offer Commit
|
||||
|
||||
Location: `crates/vernier-ui/src/shell/ribbon.rs:925-928`.
|
||||
|
||||
`fixed_slot_ready` says it determines whether Commit would produce an edit, but an Edit session is treated as ready when any `filled()` slot exists, and a Create session uses `CreateSession::ready()`, which only requires each slot to be nonempty. Required pick cardinalities are checked later. In particular, `create_session_edit` in `crates/vernier-app/src/sessions.rs:315-317` requires exactly two Revolve axis points. The Edit path also filters `filled()` through `slot_is_committable` before constructing `Edit::CommitSession` (`edit.rs:1239-1255`).
|
||||
|
||||
Reproduction: open Revolve, pick only one axis point, then fill the profile slot. The new primary Commit button is painted as ready, but pressing it constructs no edit and reports that Commit did nothing. Editing an existing Revolve axis with one point has the same mismatch. Three axis points are also nonempty but invalid. This is not a kernel failure that cannot reasonably be predicted by the UI: the slot explicitly asks for two points, and the UI has their count. The coarse CreateSession readiness predates this change, but the new primary renderer adopts it as its promise that a control can produce an edit.
|
||||
|
||||
Correction: share a slot-completeness predicate for known arities between readiness consumers; use it in Create/Edit readiness and width calculation, preserving the intentional empty armed SweepGuide removal. Keep deeper geometric validity and stale-identity checks at app/kernel dispatch. At minimum test a complete profile with a one-/two-/three-point Revolve axis and an Edit axis singleton. Existing new tests cover empty slots and a fully populated Sweep; they do not cover a nonempty but incomplete axis.
|
||||
|
||||
This finding comes from production control-flow inspection; the reviewer did not modify another lane's production or test files.
|
||||
|
||||
## Renderer, cancellation and width assessment
|
||||
|
||||
The added `commit_group` call is in the correct shared Edit/Create branch. The pre-existing Loft and SketchOp early returns each call it once, so the change does not duplicate those builders' controls. Cancel is always rendered. Its existing click handler invokes `session_click(Action::CancelEdit)`, which clears only the open session and returns no document edit. Commit routes through the existing app action and command path; no second mutation implementation was introduced.
|
||||
|
||||
The fixed-slot width now includes the input group, `SESSION_GAP` and the same `commit_group_width` whose ready predicate is used by the painter. The 16 px gap matches the ribbon's sibling layout. The group is accounted for in `plan_row`'s fixed width before other groups collapse. No additional width or overflow defect was found in this diff. The width instrument covers both fixed-slot variants and existing builder variants, though its fixed-slot examples are not ready; the actual ready Revolve/Sweep scripts establish visibility at the tested 1600 x 1000 size. Arbitrarily narrow windows with a large non-collapsing session group are not established by those scripts.
|
||||
|
||||
Fresh reviewer command:
|
||||
|
||||
```sh
|
||||
CARGO_TARGET_DIR=/home/nilsb/Documents/Projects/VernierCAD/target \
|
||||
cargo test -p vernier-ui shell::ribbon::overflow_tests -- --nocapture
|
||||
```
|
||||
|
||||
Result: all 8 tests passed, zero failures/ignored, 0.38 s. Log: `/tmp/vernier-solid-builders-review-tests.log`.
|
||||
|
||||
## Driven evidence cross-check
|
||||
|
||||
The reviewer inspected all three scripts, `docs/SOLID_BUILDERS_VERIFICATION_2026-09-08.md`, and their aggregate reports under `/tmp/vernier-functions-{revolve,sweep,loft}-final`. Each report says pass, uses two distinct process IDs and reports cross-process determinism. The scripts genuinely draw the inputs, open the corresponding Solid ribbon builder, pick its slots/sections, and click `ribbon:commit`. Direct `export_step` is correctly disclosed as a geometry measurement helper, not proof of the separate export UI.
|
||||
|
||||
| Function | Geometry evidence | Edit/repeat evidence |
|
||||
|---|---|---|
|
||||
| Revolve | Annular cylinder `1200*pi`, then half-revolution `600*pi`, one solid, relative tolerance `1e-9`. | Angle card 360 → 180 → 360. `full.step` and `full-again.step` both hash to `0xba2ad81f55edaa83`. |
|
||||
| Sweep | Radius 3, length 30 cylinder `270*pi`; terminal scale 2 frustum `630*pi`. Radius is typed through its actual card before the sweep. | Scale card 1 → 2 → 1. `straight.step` and `straight-again.step` both hash to `0x7b0cc95e60106851`. The scaled tolerance is explicitly `1e-6` for the kernel approximation, while the straight cylinder uses `1e-9`. |
|
||||
| Loft | 4 x 4 and 12 x 12 square sections separated by 10 produce `2080/3`, one solid and six faces, tolerance `1e-9`. | No parameter edit/restore cycle. A second incomplete builder is opened and Cancel is clicked. |
|
||||
|
||||
The script lengths in the inspected reports are 74, 82 and 68 steps respectively. Revolve and Sweep restoration is a second explicit parameter edit, not undo/redo or repeat creation. **None of these three scripts sends Undo or Redo.** No undo/redo claim should be attributed to this lane; the root's broader tests may supply separate evidence.
|
||||
|
||||
All three scripts click Cancel in a partially filled second builder, then assert unchanged feature count and no error. That proves the click did not commit a feature, but an inert Cancel button would also pass. Add `expect_selection: none` after Cancel (or another explicit session-absence assertion) to establish that the builder really closed. The existing handler is correct by inspection, so this is a test-evidence gap rather than a second production-code finding.
|
||||
|
||||
The root reported a full workspace pass, release headless pass and clean clippy including this renderer. Those broad runs are integration evidence supplied by the root; this independent review freshly reran only the focused ribbon suite above. No commit or production change was made by the reviewer.
|
||||
@@ -0,0 +1,132 @@
|
||||
# Revolve, Sweep, and Loft driven verification — 2026-09-08
|
||||
|
||||
Baseline: `b3f661f`, with the function-verification fixes on branch
|
||||
`codex/reliability-and-function-verification`. This pass covers only Revolve,
|
||||
Sweep, and Loft. Each probe starts from an empty document, authors its sketches
|
||||
through painted egui controls, opens the builder from the Solid ribbon, fills
|
||||
the builder through viewport picks and its own controls, and commits through
|
||||
the painted `commit` button. The `export_step` driver step is used only as a
|
||||
measured geometry oracle after the user-created operation; it bypasses the
|
||||
separate Export STEP command surface and is not evidence for that file control.
|
||||
|
||||
All three scripts pass in release mode at 1600 x 1000 on
|
||||
`llvmpipe (LLVM 22.1.8, 256 bits)` with `--require-adapter llvmpipe`. The driver
|
||||
ran two separate processes for each script and reported
|
||||
`determinism_mode: cross-process` and `deterministic: true`.
|
||||
|
||||
| Function | Real control route and result | Exported geometry oracle | Repeat / refusal evidence |
|
||||
|---|---|---|---|
|
||||
| Revolve | Line + Rectangle -> Solid / revolve -> axis picks -> `Edit profile` -> four curve picks -> `commit`. PASS. | Full annular cylinder: `pi * (8^2 - 2^2) * 20 = 1200*pi = 3769.9111843077517 mm^3`, one solid, relative tolerance `1e-9`. | Ctrl+Z removes the created Revolve; Ctrl+Y repeats that same operation and exports the original geometry and digest. The painted angle card then produces exactly `600*pi` at 180 degrees and restores the full solid at 360. An incomplete second Revolve is cancelled through `ribbon:cancel`; selection becomes `none` and the timeline stays at two features. |
|
||||
| Sweep | Circle + Line -> type exact radius 3 in the painted radius card -> Solid / sweep -> profile pick -> `Edit path` -> line pick -> `commit`. PASS. | Straight cylinder: `pi * 3^2 * 30 = 270*pi = 848.2300164692442 mm^3`, one solid, relative tolerance `1e-9`. | Ctrl+Z removes the created Sweep; Ctrl+Y repeats that same operation and exports the original geometry and digest. The painted scale card then produces the circular frustum `630*pi` at scale 2 and restores the cylinder at scale 1. The frustum uses `1e-6` relative tolerance because OCCT `SetLaw` approximates it with a NURBS surface at its `1e-4 mm` default tolerance; the kernel test measured `2.6e-7` relative error. An incomplete second Sweep is cancelled through `ribbon:cancel`; selection becomes `none` and the timeline stays at two features. |
|
||||
| Loft | Two Rectangles -> Solid / loft -> four curve picks -> `add section` at offset 10 -> four curve picks -> type offset 20 and Enter -> `commit`. PASS. | Ruled square frustum: `10/3 * (16 + sqrt(16*144) + 144) = 2080/3 = 693.3333333333334 mm^3`, one solid, six faces, relative tolerance `1e-9`. | Ctrl+Z removes the created Loft; Ctrl+Y repeats that same operation and exports the original geometry and digest. A second Loft with one completed section is cancelled through `ribbon:cancel`; selection becomes `none` and the timeline stays at two features. |
|
||||
|
||||
The Sweep radius is typed after drawing because a world-coordinate click is
|
||||
quantized through the viewport projection and had produced a measured radius
|
||||
of `3.001263960220924 mm`. Typing 3 through the actual radius card makes the
|
||||
fixture analytic rather than weakening the volume assertion around input
|
||||
quantization.
|
||||
|
||||
## Defect found and repaired
|
||||
|
||||
The first complete Revolve probe failed at its primary completion gesture:
|
||||
|
||||
```text
|
||||
step 35 (click): the ribbon paints no "commit"; it is showing
|
||||
["inputs", "Edit profile · 4", "Edit axis", "", "plane", "", "create",
|
||||
"", "on this face", "", "combine", "selection", "4 picked"]
|
||||
```
|
||||
|
||||
The same omission affected Sweep. `session_group` in
|
||||
`crates/vernier-ui/src/shell/ribbon.rs` called the shared `commit_group` for
|
||||
the bespoke SketchOp and Loft builder arms, but the fixed-slot
|
||||
`OpenSession::Edit` and `OpenSession::Create` arm stopped after painting the
|
||||
input buttons. `session_width` mirrored that omission, so simply painting the
|
||||
buttons without updating measurement would have made the ribbon overflow plan
|
||||
under-reserve the session width.
|
||||
|
||||
The repair renders the shared Commit/Cancel group after every fixed-slot
|
||||
session. Commit is painted only when the operation can produce an edit:
|
||||
`CreateSession::ready()` for a create, or at least one filled input for an
|
||||
edit (including an armed empty optional Sweep guide, whose empty value removes
|
||||
the guide). Cancel is always painted. `session_width` uses the same readiness
|
||||
function and adds the measured group plus the session gap.
|
||||
|
||||
The focused test was red before the repair with only these painted labels:
|
||||
|
||||
```text
|
||||
ready create: ["inputs", "Edit path · 1", "Edit profile"]
|
||||
incomplete create:["inputs", "Edit axis · 0", "Edit profile"]
|
||||
```
|
||||
|
||||
After the repair, `a_ready_fixed_slot_session_paints_commit_and_cancel`
|
||||
passes for both Create and Edit sessions,
|
||||
`an_incomplete_fixed_slot_session_paints_only_cancel` passes, and
|
||||
`the_measured_width_of_every_painter_is_the_width_it_paints` passes with the
|
||||
new controls included. The release Revolve and Sweep probes now use
|
||||
`ribbon:commit` and `ribbon:cancel`; no command-palette workaround remains.
|
||||
|
||||
An independent review then found that the first readiness predicate still
|
||||
treated every non-empty slot as committable. The app constructor requires a
|
||||
Revolve axis to contain exactly two points; a singleton or overfull axis
|
||||
therefore painted an inert Commit. The corrected implementation puts the
|
||||
entity-space and arity rule in `FeatureInput::accepts`, used by
|
||||
`CreateSession::ready`, the ribbon's Edit readiness, and the app's
|
||||
`slot_command`. It covers non-empty profile/path chains, exactly two points
|
||||
for Revolve and mirror axes, even non-empty face pairs for fillets, exactly
|
||||
one face for a sketch plane, the minimum Loft section count, and the optional
|
||||
empty Sweep guide. The create handler extracts its two axis points only after
|
||||
that shared predicate succeeds, so it does not carry a second arity rule.
|
||||
|
||||
`a_revolve_axis_needs_exactly_two_points_to_paint_commit` exercises both
|
||||
Create and Edit sessions: one and three axis picks do not paint Commit, while
|
||||
two do. The existing app session group remains green (20 tests), including
|
||||
creation and repointing for Revolve, Sweep, and Loft.
|
||||
|
||||
## Reproduction and evidence
|
||||
|
||||
Focused UI tests:
|
||||
|
||||
```sh
|
||||
CARGO_TARGET_DIR=/home/nilsb/Documents/Projects/VernierCAD/target \
|
||||
cargo test -p vernier-ui fixed_slot_session -- --nocapture
|
||||
CARGO_TARGET_DIR=/home/nilsb/Documents/Projects/VernierCAD/target \
|
||||
cargo test -p vernier-ui revolve_axis_needs_exactly_two_points -- --nocapture
|
||||
CARGO_TARGET_DIR=/home/nilsb/Documents/Projects/VernierCAD/target \
|
||||
cargo test -p vernier-ui \
|
||||
shell::ribbon::overflow_tests::the_measured_width_of_every_painter_is_the_width_it_paints \
|
||||
-- --exact --nocapture
|
||||
CARGO_TARGET_DIR=/home/nilsb/Documents/Projects/VernierCAD/target \
|
||||
cargo test -p vernier-app sessions -- --nocapture
|
||||
```
|
||||
|
||||
Release driven gates, each run twice in separate processes by default:
|
||||
|
||||
```sh
|
||||
CARGO_TARGET_DIR=/home/nilsb/Documents/Projects/VernierCAD/target \
|
||||
cargo run --release -q -p vernier-drive -- \
|
||||
scripts/drive/functions-revolve.json \
|
||||
--out /tmp/vernier-functions-revolve-review --skip-png \
|
||||
--require-adapter llvmpipe
|
||||
CARGO_TARGET_DIR=/home/nilsb/Documents/Projects/VernierCAD/target \
|
||||
cargo run --release -q -p vernier-drive -- \
|
||||
scripts/drive/functions-sweep.json \
|
||||
--out /tmp/vernier-functions-sweep-review --skip-png \
|
||||
--require-adapter llvmpipe
|
||||
CARGO_TARGET_DIR=/home/nilsb/Documents/Projects/VernierCAD/target \
|
||||
cargo run --release -q -p vernier-drive -- \
|
||||
scripts/drive/functions-loft.json \
|
||||
--out /tmp/vernier-functions-loft-review --skip-png \
|
||||
--require-adapter llvmpipe
|
||||
```
|
||||
|
||||
The aggregate reports record:
|
||||
|
||||
| Script | Result | Steps / frames | Determinism | Stable artifacts |
|
||||
|---|---|---:|---|---|
|
||||
| `functions-revolve` | pass | 88 / 171 | cross-process, true | `full.step`, the Ctrl+Y `full-redone.step`, and `full-again.step` all `0xba2ad81f55edaa83`; `half.step` `0x36397a10dd90c9b0` |
|
||||
| `functions-sweep` | pass | 96 / 179 | cross-process, true | `straight.step`, the Ctrl+Y `straight-redone.step`, and `straight-again.step` all `0x7b0cc95e60106851`; `scaled.step` `0xaa920ed663bcb0b9` |
|
||||
| `functions-loft` | pass | 82 / 179 | cross-process, true | `loft.step` and the Ctrl+Y `loft-redone.step` both `0xb338986ad120fefc`; saved document and naming sidecar also matched across processes |
|
||||
|
||||
The Vulkan enumerator logged one failed non-selected adapter initialization per
|
||||
process before choosing llvmpipe; the required adapter check passed and every
|
||||
command exited zero.
|
||||
Binary file not shown.
Reference in New Issue
Block a user