[15] Rebuild RC design verification and batch workflow - #78
Merged
Conversation
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.
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.
…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.
…tion-trust-repair
…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.
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 inverificationStorewas derived from provided reinforcement (auto-verify.tsnever readselem.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/Vypair while the generator selected the governing axis per member. Measured on the 408-member flagship frame: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
resolveDesignAxesfeeds 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.v1→v2Statuses 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/ratioinversion 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·UNSUPPORTEDOnly
VERIFIEDmay 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 asSEARCH_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 becausecomputeBeamCriticalSectionsscanned every element twice per beam end.reinforcementTransactiongives 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 reassignedmodel.elementsdirectly 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 (
setConcretewiped the unified results); the viewport colour-map effect missed itsdesign/providedRevisiondependencies; and a deadmodifiedElementsderived recomputed verification for every element on each elements-map reassignment.Fixture-load and section corrections
rc-design-frameauthored 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 movedqY → 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-8fixture — 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 returnUNSUPPORTEDand 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/engineis now store-free.Component decomposition
ProDesignTab.svelte1,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=1at build time (the bundler eliminates the dynamic import, so a normal build cannot expose them even with?e2e=1) and?e2e=1at 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
e2eCI job builds the WASM engine, caches browsers, runs smoke on every PR and the slow suite onmain/run-e2elabel, and uploads artifacts on failure.Evidence
npm run wasm"npm run build__stabileoVITE_E2E=1buildgit diff --checkengine/·web/src/lib/wasm/·*.rs·Cargo.*changedManual 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 exactlyIz/Iy ≈ 2.0024. It reproduced after stashing all PR15 work, so it was written up as a pre-existing upstream defect and shipped with anit.failsexpected-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 currentengine/Rust source. On that binary the same probe returns theIzresult — the app-convention-correct answer. The authoring machine'sdedaliano_engine_bg.wasmwas dated 7 June, nine days older than the newestengine/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
6a276be4withdraws it:solver-3d.test.ts—it.failsremoved; 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.Practical note for reviewers: if
solver-3d.test.ts › horizontal displacement at topor the Y-direction boundary test fails locally with roughly twice the expected displacement, yourweb/src/lib/wasm/is stale — runnpm 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.truncatedsurfaced per member); frame-line grouping inheritstraceChain's arbitrary choice where > 2 same-axis members meet a node (flaggedambiguous, requires confirmation); Linux screenshot baselines not yet committed — the authoring machine has no container/VM runtime, so the CI job writes them and uploads alinux-screenshot-baselinesartifact (both comparisons areexpect.softand the CI step iscontinue-on-error, so this can never fail CI); legacyProVerificationTab.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:
03002d21store: reinforcement transactions —model.svelte.ts,history.svelte.ts. Touches undo/redo, tab switching, autosave and share links. The new silent history path is strictly additive (notifyMutationdefaults to the old behaviour), but this is the deepest seam in the PR.485dd4e3store: verification revisions —verification.svelte.tshas six consumers.getMaxRatio/getStatuskeep their signatures but change their source from the baseline to provided reinforcement. This is the behavioural heart.ca82c848verifier axis correction —station-design-forces.ts. Changes published verification numbers for every existing RC model. Intended and approved.de6707f0candidate search — the new capability; check the honesty invariants incandidate-search.tsandoutcome.ts.eefd74abfixture 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 atHEAD.Merge note: the base branch's two newer commits also touch
web/src/lib/i18n/locales/{en,es}.tsnear 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
Open http://127.0.0.1:4000/app/pro, choose the RC Design tab in the PRO panel.
rc-design-frame(408 members) → Solve → Design all. Expect 408 VERIFIED, worst utilization 0.993, ~0.4 s.24Ø32(blocked with a reason), then a valid change (preview → apply → one undo).continuous-beam(no combinations) → the surface refuses honestly.