Skip to content

[15] Rebuild RC design verification and batch workflow - #78

Merged
diegokingston merged 35 commits into
mainfrom
pr/15-result-invalidation-trust-repair
Jul 31, 2026
Merged

[15] Rebuild RC design verification and batch workflow#78
diegokingston merged 35 commits into
mainfrom
pr/15-result-invalidation-trust-repair

Conversation

@Batuis

@Batuis Batuis commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

Stacked on pr/14-dxf-plan-upgrade. Review only the 19 commits in this PR; the base branch has 2 newer commits of its own.

77 files · +13,777 / −4,042 · 19 signed commits · manual QA approved


Why

Manual QA rejected the previous PR15: every committed reinforcement edit called verificationStore.clear(), which emptied the design table, collapsed the working context, removed the summary and viewport overlay, and forced a full re-run after each individual change. Multi-member reinforcement iteration was unusable.

Investigating that turned up something worse. clear() was fixing a bug that did not exist — nothing in verificationStore was derived from provided reinforcement (auto-verify.ts never reads elem.reinforcement) — while the real trust bug went unnoticed, and underneath that the verifier itself was not trustworthy.

Authoritative, axis-correct verification

Beam and column checks were hardcoded to the Mz/Vy pair while the generator selected the governing axis per member. Measured on the 408-member flagship frame:

before after
Generator self-assessment ok 112 / warn 92 / fail 204
Authoritative verifier on the accepted design ok 4 / warn 28 / FAIL 376 VERIFIED 408
Worst provided utilization 7.69 0.993
Beams with any top steel 96 / 248 248 / 248
Hogging checks generated on Y-beams 0 all
Bar-fit / anchorage failures 252 / 248 0 / 0
Generator bar choice Ø32 for 100%, up to 17Ø32 in a 300 mm web fit-constrained at generation

128 X-beams were passing at utilization 0.62 while their real 1113 kN·m gravity moment went unchecked. Columns were verified against 6 kN·m while carrying 973 kN·m.

Fixed: a single resolveDesignAxes feeds both generation and verification; missing reinforcement in a loaded region is now an explicit failure instead of a skipped check; column ties check both shear components; the slenderness magnifier reaches the verifier; minimum/maximum steel, stirrup-spacing zones and section-layout issues are first-class checks.

⚠ Verifier migration cirsoc201.provided.v1v2

Statuses from earlier versions are not comparable — some previous passes were false. Every certificate records verifierId; the UI carries a migration notice (design.cert.migrationNotice) in EN and ES; the CHANGELOG documents it. No user workflow depended on the incorrect behaviour, so the genuine correction was preferred over compatibility.

The trust bug: provided-first status

The table summary and the viewport overlay read the auto-design baseline, which is designed-to-pass by construction. Weakening a member's rebar left the viewport green.

Status everywhere now derives from the provided reinforcement, with three honest display states: current, stale (desaturated status colour + hatch + glyph) and unavailable (grey, never green). Non-passing states are never folded into the pass count. Utilization is demand/capacity everywhere (warn 0.95 < u ≤ 1.00); the ad-hoc 1/ratio inversion is gone.

Outcome contract and certificates

Five honest outcomes, with runtime invariant guards asserted on every result:

VERIFIED (carries a certificate) · SECTION_INADEQUATE (exhaustive, with a recommendation) · DEMAND_UNAVAILABLE · SEARCH_EXHAUSTED · UNSUPPORTED

Only VERIFIED may carry assigned reinforcement and a certificate; a certificate requires util ≤ 1.00, ≥ 1 check and ≥ 1 recorded checked axis; an exhausted envelope may never be reported as SEARCH_EXHAUSTED. A failing best candidate is retained as clearly provisional — never assigned, never certified, never counted as passing, always listed in the review panel.

Candidate search and optimization

The previous generator sized one moment for the whole beam, wrote top steel only when the section happened to need compression steel, invented the span stirrup spacing as 1.5× the support value, selected bars with no knowledge of the section width, and rated columns with a straight-line interaction whose moment capacity ignored axial force entirely.

The new search designs each beam region independently, enforces bar fit at generation time, recomputes the effective depth from the actual layer centroid, and verifies every candidate with the same verifier the UI displays. Per-member bounds are count-based only — a wall-clock cutoff made the outcome class depend on machine load, breaking determinism. Infeasibility is claimed only after the whole permitted envelope is enumerated.

Optimization is lexicographic on constructability (layers → distinct diameters → standard steps), then a bounded weighted scalar, then the enumeration index, so results are reproducible and explainable. Results are described as "best found within a bounded search", never optimal.

Section-inadequacy advice

Closed-form screen → standard 50 mm increments → explicit approval dialog → mandatory re-solve. Always labelled preliminary, names the limiting constraint, warns when the section is shared by other members. Guarded by iteration count, strict area growth, a > 25 % demand-growth abort and hard dimensional caps, so it cannot loop forever. Never applied silently.

Live provided-rebar verification and undoable transactions

Verification is memoised per element on (rebarHash, demandRevision) — an edit costs one cache miss, not a re-verification of every row. The previous derived was O(N·E) ≈ 333k element iterations per keystroke because computeBeamCriticalSections scanned every element twice per beam end.

reinforcementTransaction gives one undo step per action, one reactive commit, no model-version bump and zero structural solves. Reinforcement edits were previously not undoable at all — they reassigned model.elements directly and pushed no history. Elements are now replaced rather than mutated in place, which also removes an aliasing hazard where an in-place edit could corrupt an undo snapshot.

Three independent freshness axes replace the previous conflation: analysisRevision, demandRevision, baselineRevision. The mutation hook is unconditional so a model edit can never fail to advance the analysis revision.

Commands, batch and group editing, selection

One "Run Design" button — which conflated checking, generating and accepting, and performed the accept as an un-undoable mutation of every un-detailed member — became three explicit commands plus Design all. Auto-design defaults to the selection; all-un-designed is an explicit menu action. Long runs report progress, honour cancellation, and a partial run is reported as aborted with the count not reached.

Grouping is derived and labelled as such — the model stores no storeys or grid lines, so elevation bands read L3 +10.20 m, and a grouping the geometry cannot support honestly is refused with a reason. Also available: structural planes, frame lines (with a collinearity gate and an ambiguity flag), section, material, kind and connectivity.

Batch edits overwrite the selected fields by default; Protect manual overrides is opt-in. Incompatible members are listed with a reason and excluded from the apply count rather than silently skipped. The preview computes before/after utilization through the authoritative verifier without touching the model. The whole apply is one undo step; large selections require the count to be typed; a truncated preview always reports how many members it is not showing.

Table ⇄ viewport selection is synchronised both ways. Two real defects the browser tests caught were fixed rather than worked around: an empty viewport selection wiping checked rows when the elements map is reassigned, and row-focus clobbering a multi-row batch selection.

Also fixed: the Selected filter returned every row regardless of selection; opening the report dialog erased the whole design table (setConcrete wiped the unified results); the viewport colour-map effect missed its design/providedRevision dependencies; and a dead modifiedElements derived recomputed verification for every element on each elements-map reassignment.

Fixture-load and section corrections

rc-design-frame authored the gravity load of its 120 Y-direction beams in the local y component, which is horizontal for every horizontal member — those beams were loaded sideways and racked the whole frame. 240 load entries moved qY → qZ.

Peak X-beam moment 2261 → 450 kN·m; peak column moment 2155 → 357 kN·m. Most of the apparent section inadequacy was an artifact of this mis-authored load.

The fixture also shipped with no load combinations, so provided-rebar verification produced no checks at all on the flagship example. Five CIRSOC ULS combinations added. Sections grow to 500×500 columns and 350×650 beams so the example demonstrates a complete design (450 mm columns already reach 408/408; 500 was chosen for margin and is the size the advice engine proposes).

New rc-design-qa-8 fixture — eight members, adequate sections, combinations present — for fast deterministic unit and browser tests.

Design-code adapter seam

DesignCodeAdapter + registry. CIRSOC 201 implemented; ACI 318/AISC 360, Eurocode 2/3, NDS, TMS 402 and AISI S100 registered as honestly unsupported — they can rate a given arrangement but have no reinforcement model, so they always return UNSUPPORTED and can never certify. A conformance suite runs over the whole registry (60 parameterised tests), so a future Eurocode adapter is validated by the same tests with no RC-design UI change. lib/engine is now store-free.

Component decomposition

ProDesignTab.svelte 1,724 → 424 lines, below the repo's 600-line ceiling, with ten focused components. Not a pure mechanical extraction — the tab was rebuilt on the new command layer — so it is split across three commits (components, dialogs, orchestrator) rather than claimed as a no-op move.

Playwright and CI

Chromium on SwiftShader against a production preview build on an isolated 127.0.0.1:4173, one worker, no sleeps — every wait polls real application state.

Test hooks are gated twice: VITE_E2E=1 at build time (the bundler eliminates the dynamic import, so a normal build cannot expose them even with ?e2e=1) and ?e2e=1 at runtime. Both objects frozen; the query surface is read-only. A test runs both real builds and greps the output to prove it. A WASM readiness gate fails loudly if the stub solver is ever in play — which would otherwise make every design assertion pass vacuously.

19 browser tests: 15 blocking smoke + 4 slow. New e2e CI job builds the WASM engine, caches browsers, runs smoke on every PR and the slow suite on main/run-e2e label, and uploads artifacts on failure.


Evidence

Check Result
Vitest (CI, freshly built WASM) 2,927 passed · 0 failed · 12 skipped (144 files)
Vitest (local, stale WASM) 2,925 passed · 2 failed — both emit "rebuild with npm run wasm"
Playwright 19/19 passed (15 smoke + 4 slow)
Browser console / page errors 0 (asserted in every test teardown)
npm run build ✅ · 0 dist files expose __stabileo
VITE_E2E=1 build ✅ hooks present
git diff --check clean
engine/ · web/src/lib/wasm/ · *.rs · Cargo.* changed 0 · 0 · 0 · 0
408-member design run 421 ms (budget 20 s)
50-member pass 171 ms · 564 verifier calls
Verifier calls, 408 members 1,796 · max 24/member (cap 300)
Single-member re-verification < 20 ms, 1 cache miss

Manual QA: approved.


ℹ️ Suspected solver defect — investigated and withdrawn (FYI @diego, no action needed)

While authoring this PR, a raw-solver probe (no explicit localY) appeared to bend a global-Y member about local y (Iy) instead of local z (Iz) — a clean axis swap, ratio exactly Iz/Iy ≈ 2.0024. It reproduced after stashing all PR15 work, so it was written up as a pre-existing upstream defect and shipped with an it.fails expected-failure marker plus escalation wording.

CI disproved it, and the claim is withdrawn.

web/src/lib/wasm/ is gitignored and CI rebuilds the engine from the current engine/ Rust source. On that binary the same probe returns the Iz result — the app-convention-correct answer. The authoring machine's dedaliano_engine_bg.wasm was dated 7 June, nine days older than the newest engine/ commit (16 June), and no Rust/wasm-pack toolchain was available locally to rebuild it. The "defect" was a stale local WASM artifact, not a solver bug.

There is no solver defect and no solver change is needed. Commit 6a276be4 withdraws it:

  • solver-3d.test.tsit.fails removed; the original assertion runs normally again and now names the stale-binary symptom.
  • design/__tests__/orientation-boundary.test.ts — the raw-path block asserts the correct behaviour for both directions and explicitly guards against the historical misdiagnosis. Escalation wording replaced with an account of what happened.
  • rc-design-gates.test.ts — the source gate pins the corrected shape instead of the expected-failure marker.
  • CHANGELOG updated.

Practical note for reviewers: if solver-3d.test.ts › horizontal displacement at top or the Y-direction boundary test fails locally with roughly twice the expected displacement, your web/src/lib/wasm/ is stale — run npm run wasm. Both tests now say so in the assertion message. No solver code was touched by this PR (engine/, web/src/lib/wasm/, *.rs, Cargo.* all unchanged).

The orientation diagnostic and the certification refusal stay: they earned their place by catching the real fixture-load defect (120 Y-beams loaded sideways), which was never solver-related.


Supported / unsupported

Supported: CIRSOC 201, rectangular sections, beams and columns, flexure/shear/axial/biaxial, min-max steel, bar fit, spacing, cover, congestion, anchorage, tie rules, slenderness inputs, derived grouping, batch editing, section advice.

Explicitly unsupported (declared, not silently dropped): other design codes (registered as UNSUPPORTED), non-rectangular sections, walls, slabs, foundations, torsion and side-face reinforcement, deep beams, transfer beams, bundled bars, prestressing, headed/mechanical anchorage design, seismic detailing (INPRES-CIRSOC 103), IFC export, legal sign-off.

Other limitations: bounded search (searchStats.truncated surfaced per member); frame-line grouping inherits traceChain's arbitrary choice where > 2 same-axis members meet a node (flagged ambiguous, requires confirmation); Linux screenshot baselines not yet committed — the authoring machine has no container/VM runtime, so the CI job writes them and uploads a linux-screenshot-baselines artifact (both comparisons are expect.soft and the CI step is continue-on-error, so this can never fail CI); legacy ProVerificationTab.svelte (2,431 LOC, unreachable) not deleted; pre-existing UI strings outside the RC design surface remain unlocalised — all new strings are in EN + ES (183 keys each, key-set equality gated).

Deferred to PR16 — continuity-aware detailing

Full continuous-beam optimization, coordinated bar cutoff/laps across spans, beam-column joint design, seismic joint checks, strong-column/weak-beam design. This PR uses connectivity only for grouping, critical sections, anchorage context and optional group standardisation.


Reviewer guide

Highest risk — read these first:

  1. 03002d21 store: reinforcement transactionsmodel.svelte.ts, history.svelte.ts. Touches undo/redo, tab switching, autosave and share links. The new silent history path is strictly additive (notifyMutation defaults to the old behaviour), but this is the deepest seam in the PR.
  2. 485dd4e3 store: verification revisionsverification.svelte.ts has six consumers. getMaxRatio/getStatus keep their signatures but change their source from the baseline to provided reinforcement. This is the behavioural heart.
  3. ca82c848 verifier axis correctionstation-design-forces.ts. Changes published verification numbers for every existing RC model. Intended and approved.
  4. de6707f0 candidate search — the new capability; check the honesty invariants in candidate-search.ts and outcome.ts.
  5. eefd74ab fixture correction — changes a shipped example's loads and sections.

Low risk: b1db32e3 (i18n), d5581f54 (changelog), 096b14b3 (presentational components).

Where the guarantees live: web/src/lib/engine/__tests__/rc-design-gates.test.ts (29 source gates), web/src/lib/store/__tests__/rebar-invalidation.test.ts (15 store contract tests), web/src/lib/engine/design/__tests__/autodesign-regression.test.ts (408-member regression + performance budgets).

Bisectability: commits are organised for review, not for git bisect. The engine seam and the component rewrite land in different commits, so a few intermediate commits will not build in isolation. The tree is green at HEAD.

Merge note: the base branch's two newer commits also touch web/src/lib/i18n/locales/{en,es}.ts near the end of the file, where this PR appends ~190 keys. A small conflict is possible on rebase/merge; both changes are additive.


Local QA

git fetch origin
git checkout pr/15-result-invalidation-trust-repair
cd web && npm ci

# unit + integration (expect: 2927 passed, 0 failed, 12 skipped)
npm run test

# production build — must NOT expose the test hooks
npm run build
grep -rl "__stabileo" dist/assets/*.js | wc -l     # expect 0

# browser suite (one-time: npx playwright install --with-deps chromium)
npm run test:e2e:smoke      # 15 blocking
npm run test:e2e:slow       # 4, includes the 408-member run

# dev server for manual QA
npm run dev

Open http://127.0.0.1:4000/app/pro, choose the RC Design tab in the PRO panel.

  • Load rc-design-frame (408 members) → Solve → Design all. Expect 408 VERIFIED, worst utilization 0.993, ~0.4 s.
  • Expand any row: certificate, checked axes, and the authoritative check table.
  • Weaken a bar count/diameter → the table stays mounted, the row goes red live, and the viewport follows. Ctrl+Z restores it in one step.
  • Select several beams → Batch edit… → try 24Ø32 (blocked with a reason), then a valid change (preview → apply → one undo).
  • Load continuous-beam (no combinations) → the surface refuses honestly.

Batuis added 21 commits July 22, 2026 14:45
Stale advanced, moving-load, and verification state could survive model or reinforcement edits. Clear all advanced 3D state, abort in-flight moving-load work, broaden mutation invalidation, and clear verification after user rebar commits.
…ebar hashing

Three pure foundations for the RC design rebuild.

design-axes resolves which force components actually govern a member instead of
assuming one. Beam flexure/shear and column P-M were previously evaluated on a
hardcoded Mz/Vy pair while the generator chose per element, so every member whose
gravity bending lands in My was checked on a nearly unloaded axis.

outcome defines the five honest results (VERIFIED, SECTION_INADEQUATE,
DEMAND_UNAVAILABLE, SEARCH_EXHAUSTED, UNSUPPORTED), the demand/capacity utilization
convention with its warn band, the certificate shape, and runtime invariant guards so
a non-VERIFIED result can never carry reinforcement or be counted as a pass.

rebar-hash provides the stable identity a memo cache and a certificate are bound to:
key-order and float-noise invariant, sensitive to any real change.
Bumps the verifier to cirsoc201.provided.v2. Results are not comparable with v1,
which issued false passes.

- Beam and column checks consume the resolved governing axis instead of a hardcoded
  Mz/Vy pair. Measured on the 408-member frame, 128 X-beams were passing at
  utilization 0.62 while their real 1113 kN.m gravity moment went unchecked, and
  columns were verified against 6 kN.m while carrying 973 kN.m.
- Missing reinforcement in a loaded region is now an explicit FAILURE. It was a
  skipped check, so a beam with no top steel at a hogging support reported nothing.
- Utilization is demand/capacity everywhere, with warn for 0.95 < u <= 1.00. It was
  capacity/demand here and demand/capacity in the baseline, reconciled by an ad-hoc
  inversion in the UI.
- Column ties are checked against both shear components; the slenderness magnifier
  reaches the verifier; minimum/maximum steel, stirrup-spacing zones and section
  layout issues are reported as first-class checks.
- The result reports which axes were actually evaluated, so 'nothing was checked' can
  no longer read as a pass.
…d grouping

member-context assembles the plain-data bundle an adapter needs, so adapters and the
verifier never touch a store. It also memoises the critical-section map, which was
recomputed per row per keystroke at O(N.E).

orientation-diagnostic detects members whose force orientation is inconsistent with
their geometry, including gravity authored in a horizontal local component. A flagged
member cannot be certified.

member-grouping derives elevation bands, structural planes, frame lines with a
collinearity gate, and attribute/connectivity groups from geometry alone. The model
stores no storeys or grid lines, so every group is labelled as derived and a grouping
the geometry cannot support honestly is refused with a reason instead of guessed.
…n-service pure

DesignCodeAdapter is the seam a future Eurocode implementation plugs into without
touching the RC design UI. CIRSOC 201 is implemented; ACI/AISC, Eurocode, NDS, TMS and
AISI are registered as honestly unsupported: they can rate a given arrangement but
have no reinforcement model, so they always return UNSUPPORTED and can never certify.

verification-service no longer writes to a store and getCodeDetail takes the record
directly, so lib/engine carries no store dependency and the seam is real rather than
decorative.
…ction advice

Auto-design now produces reinforcement that verifies.

The previous generator sized one moment for the whole beam, wrote top steel only when
the section happened to need compression steel, invented the span stirrup spacing as
1.5x the support value, selected bars with no knowledge of the section width, and
rated columns with a straight-line interaction whose moment capacity ignored axial
force entirely.

The search designs each beam region independently, enforces bar fit at generation
time, recomputes the effective depth from the actual layer centroid, and verifies
every candidate with the authoritative verifier the UI displays. Per-member bounds are
count-based only: a wall-clock cutoff made the outcome class depend on machine load.

The optimizer is lexicographic on constructability, then a bounded weighted scalar,
then the enumeration index, so results are reproducible. Infeasibility is only claimed
after the whole permitted envelope has been enumerated; a budget stop reports
SEARCH_EXHAUSTED instead.

Section advice is a closed-form screen that orders the search and proposes standard
50 mm increments, always labelled preliminary, with iteration, growth, demand-growth
and dimensional-cap guards so the loop cannot run forever.
…d verification

Analysis freshness and reinforcement-verification freshness were conflated, which is
why a rebar edit destroyed the analysis-derived state it depended on.

- analysisRevision, demandRevision and baselineRevision are now independent. The
  mutation hook is unconditional so a model edit can never fail to advance the
  analysis revision.
- Station demand data is retained independently of presentation, so an edited member
  re-verifies immediately with no structural solve.
- Provided-reinforcement verification is memoised per element on
  (rebarHash, demandRevision): an edit costs one cache miss instead of re-verifying
  every row.
- Display status is PROVIDED-first. Reading the auto-design baseline was the actual
  trust bug: it is designed to pass by construction, so weakening a member's rebar
  left the summary and viewport green.
- setConcrete no longer wipes the unified results, which is why opening the report
  dialog used to erase the whole design table.
- unavailable and stale are first-class states and are never counted as passing.
…lysis

Reinforcement edits were not undoable at all: they reassigned model.elements directly
from the component and pushed no history. Routing them through the normal history path
was impossible because both existing paths fire the results-invalidation hook.

reinforcementTransaction pushes exactly one history snapshot through a new silent
path, reassigns the elements map once, and never bumps modelVersion, so one user
action is one undo step and the structural results survive. Elements are replaced
rather than mutated in place, which also removes an aliasing hazard where an in-place
edit could corrupt an undo snapshot sharing the same reinforcement object.

rebar-edit is the single entry point every editor control uses.
…olve counting

Splits the single Run Design button, which conflated checking, generating and
accepting, and performed the accept as an un-undoable mutation of every un-detailed
member.

Compute demands, Run code check and Auto-design are now separate, with Design all as
the convenience chain. Auto-design defaults to the selection. Only VERIFIED results
are written to the model; a failing best candidate is retained as clearly provisional,
never certified and never counted as passing. Long runs report progress and honour
cancellation, and a partial run is reported as aborted with the count not reached.

A small solve counter records app-side structural-analysis dispatches so a browser
test can assert directly that a reinforcement-only edit triggers none.
…detail panel

New components for the design surface. The table is keyed by element id and its
scroll container persists, so expansion, filters, scroll position and selection
survive editing and re-verification.

Row status, flags and badges always pair a glyph and text with colour, so stale and
unavailable can never be mistaken for a pass. Group pickers label everything as
derived and show the refusal reason when the geometry cannot support a grouping.

The schematics read the axis-corrected section, the same one the capacity check uses;
previously the drawing and the check could describe different sections for the same
rotated member. The detail panel shows the certificate, or states explicitly that
there is none, together with the checked axes and the search statistics.
…ce approval

Batch edits overwrite the selected fields on every compatible selected member by
default; protecting manual overrides is opt-in. Incompatible members are listed with a
reason and excluded from the apply count rather than silently skipped, the preview
computes before and after utilization through the authoritative verifier without
touching the model, and the whole apply is one undo step. Large selections require the
count to be typed and warn that they are a single undo step, and a truncated preview
always reports how many members it is not showing.

Section changes are never applied silently: the approval dialog states that the
recommendation is preliminary, names the limiting constraint, warns when the section
is shared, and shows the iteration guard. Applying one invalidates the analysis, which
is what forces the mandatory re-solve.
Fixes the reported regression. A committed reinforcement edit called
verificationStore.clear(), which emptied the design table and also destroyed the
required-steel data the live provided-rebar verification needs as input.

The tab is now layout and state orchestration only, 1724 lines down to 424, below the
repo's 600-line component ceiling. Every rebar write goes through the reinforcement
transaction. Focus and batch selection are separate concepts, so inspecting a row no
longer clobbers a multi-row selection, and an empty viewport selection no longer
clears the checked rows when the elements map is reassigned.

Also fixes the Selected filter, which returned every row regardless of selection, and
removes the dead modified-elements derived that recomputed verification for every
element on each elements-map reassignment.
… states

The overlay and its labels read the auto-design baseline, which is designed to pass
by construction, so a member whose rebar the user had weakened stayed green.

They now read the provided-reinforcement status and distinguish current, stale
(desaturated status colour plus hatch and glyph) and unavailable (grey, never green).
The colour-map effect also depends on the unified results and the provided-verification
revision; without those a baseline-only or reinforcement-only change left the viewport
painted with the previous state.
183 keys per locale covering commands, counts, statuses, badges, banners, outcome
reasons, limiting constraints, grouping, batch editing, certificates and the
verifier-version migration notice.

Adds tp(), a translate helper with placeholder interpolation, because the design
messages carry element ids, utilizations and dimensions and t() has no parameter
support. A missing parameter is left as the literal placeholder so an omission is
visible rather than silently blank.
…-qa-8

rc-design-frame authored the gravity load of its 120 Y-direction beams in the local y
component, which is horizontal for every horizontal member, so those beams were loaded
sideways and racked the frame. 240 load entries move from qY to qZ. The correction
drops the peak X-beam moment from 2261 to 450 kN.m and the peak column moment from
2155 to 357 kN.m, which shows that most of the apparent section inadequacy was an
artifact of the mis-authored load.

The fixture also shipped with no load combinations, so provided-rebar verification
produced no checks at all on the flagship example. Five CIRSOC ULS combinations are
added.

Sections grow to 500x500 columns and 350x650 beams so the example demonstrates a
complete design: 408 of 408 members verified, worst certified utilization 0.993.
450 mm columns already reach 408 of 408; 500 was chosen for margin and is the size the
advice engine proposes.

rc-design-qa-8 is a new eight-member fixture with adequate sections and combinations
present, for fast deterministic unit and browser tests.
…e source gates

Design engine: axis resolution, rebar hashing, outcome invariants, optimizer ordering,
section advice and its termination guards, generator bounds and fit, search
determinism under identical feedback, adapter conformance parameterised over the whole
registry, and the 408-member regression with the audited performance budgets.

Store: an edit keeps the table populated, edited members re-verify immediately,
unaffected members are cache hits by object identity, no structural solve and no
model-version bump, one transaction is one undo step, snapshots are not aliased, and
missing reinforcement fails explicitly.

Orientation boundary: proves the app path treats X- and Y-direction members
identically, and pins the raw-solver divergence with escalation wording.

Source gates: no design component may clear the verification store, reinforcement may
only be written through the transaction, the mutation hook stays unconditional, the
verifier stays axis-agnostic, lib/engine stays store-free, the verifier version and
migration notice exist, the locales agree, and the components stay under the size
ceiling.
…ed failure

With no explicit localY, the solver bends a global-Y member about local y under a
global-X tip load, while the app convention requires local z. The measured
displacement ratio is exactly Iz/Iy, about 2.0024, so it is an axis swap rather than a
stiffness scaling error. The defect is present on the untouched baseline.

Marked it.fails rather than skipped or deleted: the original assertion and inputs are
unchanged, so when the solver is corrected the test becomes an unexpected pass, which
is reported as a failure and prompts removal of the marker.

Normal app solves are unaffected because buildSolverInput3D supplies localY on every
frame element. Correcting the solver is out of scope here and needs solver-owner
review.
…rowser suite

Chromium on SwiftShader against a production preview build on an isolated
127.0.0.1:4173, one worker, no sleeps: every wait polls real application state.

The test hooks are gated twice. VITE_E2E=1 is a build-time flag that the bundler uses
to eliminate the dynamic import entirely, so a normal build cannot expose them even
with ?e2e=1, and ?e2e=1 is still required at runtime. Both objects are frozen and the
query surface is read-only. A test runs both real builds and greps the output to prove
it. A WASM readiness gate fails loudly if the stub solver is ever in play, which would
otherwise make every design assertion pass vacuously.

Fifteen blocking smoke tests and four slow tests cover the table staying mounted
across an edit, live status and summary updates, zero structural solves on a
reinforcement-only edit, selection sync, filters and grouping, batch preview and
validation and apply and cancel, overwrite versus protect, one-step undo, generated
designs verifying under the selected code, honest refusal without combinations,
overlay states, the report dialog leaving design state intact, context preservation,
Design all at 408 members, and responsive layout.

Two visual comparisons are soft and their CI step is continue-on-error for this first
landing. Linux baselines could not be produced on the authoring machine, which has no
container or VM runtime; CI writes and uploads them instead of committing renamed
Darwin images. Details in e2e/__screenshots__/README.md.
Notes the breaking change from cirsoc201.provided.v1 to v2. Statuses from earlier
versions are not comparable because some previous passes were false, so the UI carries
a migration notice and every certificate records the verifier id.
…M binary

CI disproved the escalation written during authoring. web/src/lib/wasm/ is
gitignored and CI rebuilds it from the current engine source; on that binary the raw
global-Y probe returns the Iz result, i.e. the app-convention-correct answer. The
authoring machine's binary predated the newest engine commit by nine days, and no Rust
toolchain was available locally to rebuild it, so the stale artifact looked like an
upstream axis swap.

Consequences:
- solver-3d.test.ts: the it.fails marker is removed and the original assertion runs
  normally again. It now also names the stale-binary symptom.
- orientation-boundary.test.ts: the raw-path block asserts the CORRECT behaviour for
  both directions and explicitly guards against the historical misdiagnosis. The
  escalation wording is replaced by an account of what actually happened.
- The source gate now pins the corrected shape instead of the expected-failure marker.
- CHANGELOG withdraws the claim.

The orientation diagnostic and the certification refusal stay: they earned their place
by catching the real fixture-load defect, which was never solver-related.
…stays alive

The two production builds used execFileSync, which blocks the worker's event loop
for their whole duration (~30 s locally, ~48 s on CI). That starves the worker's RPC
heartbeat, and the run died with

  Error: [vitest-worker]: Timeout calling "onTaskUpdate"

after all 142 test files had already passed — a green suite reported as a red job.

Awaiting execFile keeps the loop responsive. Behaviour and assertions are unchanged.
@Batuis Batuis added the run-e2e Run the slow Playwright suite and generate Linux visual baselines label Jul 26, 2026
@Batuis Batuis closed this Jul 26, 2026
@Batuis Batuis reopened this Jul 26, 2026
Batuis added 2 commits July 25, 2026 21:27
Generated by the CI e2e job on ubuntu-latest with the pinned Chromium and
SwiftShader, downloaded from the linux-screenshot-baselines artifact and committed
unmodified. The authoring machine is macOS arm64 with no container or VM runtime, so
these could not be produced locally.

  overlay-legend.png  699 x 28
  batch-dialog.png    900 x 386

The heights differ from the darwin/ set (696 x 34 and 900 x 392) because font
rasterisation differs per platform — which is precisely why a Darwin image must never
be renamed into linux/. Both images were inspected before committing: the legend shows
the utilization scale plus the current/stale/not-verified states, and the dialog shows
the batch preview with before and after utilizations.

CI reads linux/ and now compares against these instead of writing them. Both
comparisons stay soft and the step stays continue-on-error for this first landing;
the functional browser assertions remain blocking.
…ing them

The visual step still passed --update-snapshots, which was correct while the Linux
baselines were missing but is wrong now that they are committed: it rewrites them every
run, so the pixel gate would be permanently inert and would silently accept any visual
regression.

It now compares. An intentional refresh is an explicit opt-in via the UPDATE_SNAPSHOTS
repository variable. The step stays continue-on-error and both assertions stay
expect.soft for this first landing; the artifact upload is kept because on a mismatch
it carries the actual and diff images.

Verified before switching: the CI-regenerated PNGs are byte-identical to the committed
ones (overlay-legend 8363 B, batch-dialog 42270 B), so the comparison starts green.
@diegokingston
diegokingston changed the base branch from pr/14-dxf-plan-upgrade to main July 27, 2026 19:16
…sign

Verifier defects found in review, all in station-design-forces.ts:

- Column uniaxial P-M capacity analyzed about the wrong axis for
  My-governed rectangular columns (capAxis followed the moment name, not
  the flex-rotated section): checked at the strong depth — measured false
  pass util 0.68 vs true 1.51 on 0.6x0.3 with 8 dia-20. Now primary->h,
  secondary->b.
- Opposite-sign demand silently unchecked (coverage regression vs the
  pre-regional sweep): hogging in span (cantilever/pattern load/uplift)
  and sagging at supports were filtered out of every region. New sweep
  checks them against the steel that reaches each region (curtailment/
  continuity) and fails explicitly when none does; the generator seeds +
  escalates support top steel for span hogging and span bottom steel for
  support sagging. Flagship still designs 408/408 VERIFIED.
- Shear capacity got the axial force in solver sign (+ = tension) while
  expecting compression-positive: compression weakened, tension
  strengthened. Both call sites now pass -N. Column ties use a per-axis
  effective depth (was primary depth for both).

Store/UI hardening:
- stirrup/tie legs clamped to [2,6], column face bars to 6 (typed input
  bypassed editor max; legs multiply shear capacity unchecked).
- designOne keeps provisionalIds in sync with the outcome.
- DesignTable ignores keys from the expanded row's editors (Enter used
  to collapse the row being edited).

Tests: design/__tests__/review-fixes.test.ts (9 — rectangular-column P-M
golden, cantilever-style span hogging, support sagging, shear axial
sign, tie per-axis depth); store/__tests__/rebar-review-fixes.test.ts
(4 — clamps, provisional sync). rc-design-gates source gate updated for
the per-axis tie depth. Full web suite 2940 passed; build green.
… column verification

The biaxial branch of verifyProvidedReinforcement forwarded Muy/Muz to
computeBiaxialCapacity BY NAME (muy = the moment named My, muz = the moment
named Mz). computeBiaxialCapacity pairs Muz with depth h and Muy with depth b
internally, and the section here always arrives flex-rotated (b=bFlex,
h=hFlex), so that mapping is only correct when Mz happens to be the primary
axis. When My is primary, both moments hit the wrong bending depth — the same
class of bug the uniaxial branch already fixed via primary->'z',
secondary->'y' (never moment-name->axis).

Muz now always carries the primary moment and Muy the secondary one,
regardless of which of My/Mz is named primary. Verified via a mirror-symmetry
probe: the same physical column described once with My primary and once with
Mz primary now yields identical utilization (was 1.0 vs 0.595 pre-fix).
resolveDesignAxes computes axes.biaxial for beams/walls (secondary moment
> 10% of primary), but verifyProvidedReinforcement's beam/wall branch only
ever checked the primary axis (Mz/Vy stayed entirely unchecked when My
governed, and vice versa). A beam with e.g. Mz = 0.5*My could be certified
VERIFIED having never checked Mz/Vy against the provided steel.

Re-deriving the beam branch's full region/curtailment/critical-section
pipeline for a rotated secondary section is a large undertaking with its
own risk profile, so this takes the contained, honest option instead: when
axes.biaxial is true for a beam or wall, push an explicit failing check
(limiting: 'biaxial', same refusal pattern as the O6 orientation refusal)
rather than certify an axis nobody checked. checkedAxes still only reflects
what was actually verified.

On the flagship 408-member fixture this reveals 22 BEAM-Y members (wind-combo
Mz/Vy at 10.4%-17.1% of My/Vz) that were previously false-passed without ever
checking their secondary axis; they now honestly refuse (SEARCH_EXHAUSTED,
limiting: biaxial) instead of certifying. autodesign-regression.test.ts is
updated to assert 386/408 VERIFIED + 22 refused, with the refused set checked
for elementType/outcome/limiting/no-certificate, documenting the old (false)
408/408 baked into that test.
Batuis added a commit that referenced this pull request Jul 30, 2026
Propagates Diego's PR #78 review commit e20707a (P-M axis, opposite-sign
coverage, shear sign) additively into the PR16 stack so the RC chain builds on
the reviewed PR15 state. No rebase, no force-push; PR16's own history is
unchanged and this merge only adds the review delta.
Batuis added a commit that referenced this pull request Jul 30, 2026
Carries Diego's PR #78 review commit e20707a (P-M axis, opposite-sign
coverage, shear sign) up the RC chain from PR16 into PR17. Additive merge only;
PR17's own history is unchanged, nothing rebased or force-pushed.
…ssertion

review-fixes.test.ts's shearCapacityWithAxial declared a number return but produced
check!.capacity, which is number | undefined — tsc reported
review-fixes.test.ts(204,5) TS2322. The non-null assertion hid it rather than
handling it.

Narrow it explicitly instead: read capacity through the optional chain and throw a
named error when it is absent. A Shear Span check carrying no capacity is a real
defect, and this reports it as one instead of letting undefined reach the numeric
comparisons as NaN.

This file arrived with e20707a, which is PR15's head, so PR15 is the earliest
branch where the file exists and the diagnostic reproduces. Landing the guard here
lets it propagate additively up the RC chain rather than being patched per branch.

Raw tsc at this head: 457 -> 456 errors, the removed one being exactly TS2322.
No baseline file exists on PR15; the typecheck gate arrives with PR18.
Full web suite 2940 passed, 12 skipped; production build green.
Batuis added a commit that referenced this pull request Jul 30, 2026
Diego's PR #78 review (e20707a) made the shear check take axial force
compression-positive. Members 5 and 6 are compression-governed at their supports,
so they stop being under-credited on phi*Vn and report worstUtilization 0.922
instead of 0.993. Being at or under the 0,95 design target, the repair loop then
breaks one arrangement earlier, and every operational count fell with it:
candidatesConsidered 12->8, verifierCalls 7->5, memoHits 8->7,
perMember [4,0,1,0]->[2,0,1,0].

Investigated before changing anything. Forcing the generator past the design
target yields 12 distinct arrangements for member 5, utilisation descending
1.0800 -> 0.4600, so arrangements 3 and 4 remain reachable at 0.8990 and 0.8720.
Nothing was pruned: the only stop is the documented 0,95 break in
final-geometry-feedback.ts, a condition on the consumer rather than a limit on
the generator. The new opposite-sign seeds only add fields and widen top-steel
availability via max(MuStart, MuSpanHog). Reproduced identically three times.

So the counts are characterisation, not contract. Replaced:

- worstUtilization keeps its exact value, now 0.922, beside the independent
  hard bound <= 1.00.
- candidatesConsidered becomes an upper bound of 12 - the corrected search may
  never do MORE work than the version it replaced.
- memoHits becomes 'reuse happens on both paths' instead of a magic floor.
- verifierCalls becomes 'at least the per-member total', with the aggregate
  documented as a superset: the loop also verifies each member's current
  arrangement, so memoHits != candidatesConsidered - verifierCalls.
- perMember asserts the shape that always mattered - first of each identical
  pair does real work, second does none.

Added a reachability contract: every certificate is at or under the design
target AND at least four distinct arrangements stay reachable when the generator
is pulled past it. The old magic numbers could not tell an early stop from a
truncated envelope; this can.

Full trace, stopping-condition proof and assertion classification in
docs/audits/pr17-review-correction-characterization.md.

No solver/Rust/Cargo/WASM changes. Typecheck baseline not raised.
Batuis added a commit that referenced this pull request Jul 30, 2026
Carries Diego's PR #78 review (e20707a), the PR15 type-guard repair (7d0ab24)
and the PR17 characterization repair (b2d5a28) up the chain. The corrected
design-feedback-loop contracts arrive FROM PR17 — they are not reapplied here.

Additive merge; nothing rebased or force-pushed. No solver changes.
…eserving the analysis

Ctrl+Z on a rebar-only edit went through historyStore.undo() -> modelStore.restore(),
which unconditionally fires _onMutation (resultsStore.clear() +
verificationStore.invalidateAnalysis()) regardless of what kind of edit is being
undone. So the "reinforcement edits are undoable WITHOUT invalidating the analysis"
guarantee only held forward: undoing (or redoing) one wiped the whole analysis.

Tag each history entry with the kind of transaction that produced it ('structural'
vs 'reinforcement', threaded through _setHistoryPush/pushState). undo()/redo() now
branch on that tag: a 'reinforcement' entry goes through the new
modelStore.restoreReinforcementOnly(), which restores only the changed elements'
`reinforcement` field and drops only their cached provided-rebar verification (via
the existing _onReinforcementCommit hook) -- no modelVersion bump, no _onMutation,
so results/demand data survive. A 'structural' entry keeps the original full-restore
behaviour. Mixed sequences (structural edit, then rebar edit, then undo twice) fall
back to full-restore semantics on the second undo, since the popped entry's own tag
governs the branch regardless of what came before it.

Tab-switch history serialization (getStacks/setStacks) now carries the kind tags
too, defaulting missing entries to 'structural' (the safe, pre-fix behaviour) for
backward compatibility with any TabState that predates this change.
… stale

analysisRevision's only writer was invalidateAnalysis() (a structural mutation),
so isBaselineStale / DisplayStatus 'stale' could never fire from a mere re-solve --
dead code. Live hole: toggling a solve-affecting setting (self-weight, axis
convention) and re-solving (live-calc.ts) publishes fresh forces via
setResults3D/setCombinationResults3D while the retained MemberContexts (and the
provided-rebar verification memoised against them) stay put. The design table and
viewport verification colors kept presenting numbers computed from the PREVIOUS
forces as current.

Add a solve-generation counter to verificationStore, bumped by
resultsStore._onResultsPublish on every setResults3D/setCombinationResults3D
(wired, decoupled, in store/index.ts -- same pattern as _onMutation).
MemberContext now stamps this counter at build time (member-context.ts); a
mismatch between a context's stamped generation and the store's current one means
a solve has superseded it without contexts being rebuilt. getDisplayStatus (via
the new isContextStale helper) returns 'stale' on that mismatch.

A real structural mutation still yields 'unavailable', not 'stale': invalidateAnalysis()
clears the contexts map outright, so getDisplayStatus's existing "no context ->
unavailable" check fires first, before the generation comparison is ever reached.
globalSolve3D's combo path (live-calc.ts) awaits
modelStore.solveCombinations3DParallel(...) and then unconditionally publishes via
resultsStore.setResults3D/setCombinationResults3D. If the model was mutated while
that worker-based solve was in flight -- which clears resultsStore via the wired
_onMutation hook -- the stale solve's continuation still fired and repopulated the
stores with forces computed from the already-superseded model, silently
resurrecting pre-edit results as current.

Version guard: capture modelStore.modelVersion (the existing mutation-epoch
counter, already bumped on every real model edit) immediately before dispatching
the async combo solve, inside runComboSolve. After the await, if the epoch
changed, discard the publish and return null (the same "no error" sentinel the
success path returns) instead of falling back to a redundant re-solve or toasting
an error -- the next live-calc pass or manual solve will pick up the now-current
model on its own.

This was the only async publish site in live-calc.ts: runSingleSolve has no await
inside its own body, so nothing can mutate the model between its solve and its
publish call.
PR78 added the ~190 design.* keys (RC Design panel: commands, status/badge/
filter/group/batch-edit copy, and the orientation/reason/advice diagnostic
sentences) to en.ts and es.ts only. The other 12 locales had zero design.*
keys, so t()'s silent fallback to dicts.en meant the whole RC Design surface
silently rendered in English for those users. Translate all 190 keys into
ar/de/fr/hi/id/it/ja/ko/pt/ru/tr/zh (2,280 strings), verifying exact key-set
and {placeholder}-token parity against en before splicing each block in.

Add locale-parity.test.ts to keep this from regressing a third time. Scoped
to design.* for now: a full-parity check surfaced ~790 pre-existing missing
non-design (mostly landing.*) keys in every one of the 12 locales, plus 31 in
es — drift that predates this PR and is unrelated to it. That gap is flagged
via an explicit it.todo rather than silently repaired or silently ignored.
…repair' into pr/15-result-invalidation-trust-repair
…tion-trust-repair

# Conflicts:
#	CHANGELOG.md
#	web/package.json
#	web/src/lib/engine/solver-service.ts
@diegokingston
diegokingston merged commit 6c3369d into main Jul 31, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

run-e2e Run the slow Playwright suite and generate Linux visual baselines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants