[19] RC detail CAD constructibility review - #90
Conversation
…ismic PR19 becomes RC detail CAD constructibility review. The seismic INPRES-CIRSOC 103 work that held this position was lost with the 2026-07-29 workstation data loss and no Git object survived, so it is deferred rather than resumed. docs/poc/rc-footing-cad-review.md - the POC design record, canonical for the shared contract. Audits what PR15-PR18 actually provide (BarPath/BarSegment with stored arc centres, BarMark, roles, layer ids, cover and clear-spacing rules, family records, DocumentModel, DXF/XLSX) and finds only two real gaps: clear spacing is applied by name inside detectCollisions rather than exported, and there is no STEP/GLB writer. The finding that shapes the design: Stabileo ALREADY does arc-exact bar-to-bar collision with a broad-phase spatial hash and cover containment, and documents the sagitta error that linear arc interpolation caused. So CAD is not where clash detection should move. The POC is export + inspect + cross-check, not export + outsource, and disagreement with Stabileo's own verdict is itself the finding worth having. Proposes RcCadHandoffV1 field by field with owner, requiredness, unit, coordinate space, validation and absent-behaviour for each, plus the eight checks, two fixtures and the visible acceptance journey. Six architecture choices are presented with alternatives, licensing and affected files for the user to decide. No production CAD code is written. docs/handoffs/deferred-seismic-103-2026-07-29.md - specification-only record of the lost seismic work. Every section classified measured-fact, reconstructed- specification, planned-never-implemented, or lost. The four lost commit subjects survive and are a sound slice order; the diffs do not. Na/Nv, wall-system blockers and the reachability audit are recorded as LOST with no reconstruction, because inventing them would be worse than admitting the gap. No seismic branch, tag or code is created. docs/CODEX_WORKFLOW_STABILEO.md - corrects the branch-backup policy. The earlier rule told us to open a draft PR for every branch, which produced a PR on an integration branch that must never merge and another on a deliberately-red evidence snapshot. A draft PR is a review surface; remote backup is a push. The rule is now scoped by branch kind: deliverable branches get draft PRs, QA and evidence and exploratory fork branches get pushes only. Solver boundary untouched. No Netgen/NGSolve. Modal FEA and text-to-cad PR #22 are out of scope.
…eliverable branch The first version of the branch-backup policy was committed as 88abc89 onto qa/main-plus-pr18 - a QA integration branch - so it was reachable from no PR branch and from main. Retiring that QA branch would have orphaned a real, G-signed, tracked-documentation commit. That misplacement is an instance of the very problem the 2026-07-30 correction addresses: tracked documentation is deliverable work and belongs on a deliverable branch, not on an integration branch that exists to be deleted. Preserve its text verbatim here, clearly marked superseded, so the history is durable on PR19 and the obsolete QA branch can be retired without losing anything. The scoped rules remain the ones in force; rule 2 is the part the correction narrows to deliverable branches only. No rewrite and no force-push of 88abc89 - it is preserved by quotation, not by moving it.
Converts the six choices in rc-footing-cad-review.md into a decision packet with enough measured detail to decide from, rather than a summary. Each decision carries exact data flow, failure boundary, browser constraints, reproducibility, licensing, dependency cost and POC effort, plus a compact table of visible UX, files touched on each side, engineering risk, upstream acceptance risk and expansion value. Grounded in measured values rather than description: COLLISION_CHORD_TOLERANCE = 0.0005 m; placement tolerance zero by default, because a hardcoded 10 mm was once silently deducted from every measured clearance; spatial-hash cell max(0.05, 2r + requiredClear + placement + 0.02); EPS 1e-12; the documented sagitta errors of 5,9 mm on a dia-8 90 degree corner and 12,3 mm on a 135 degree hook; CoverBreach against SectionPrism; and requiredClearFor arriving as a caller-supplied callback, which is exactly why the clear-spacing accessor is missing. Decision 5 gets the most detail because it decides whether the POC is evidence or illustration. Stabileo stays the authority; the recommendation is an independent OCCT cross-check with the agreement band stated at 0.5 mm to match Stabileo's own sampler, and disagreement modelled as its own issue kind rather than resolved toward either side. Moving collision authority into text-to-cad is rejected on domain and licence grounds: the classification rules are CIRSOC-specific and therefore AGPL-derived. Flags one thing NOT verified rather than assuming it: SectionPrism models a prismatic member and a footing pad is not that shape, so whether PR18's footing path is covered by that containment check must be confirmed before fixtures are frozen. Three bundles - conservative demonstrator, clash-aware vertical slice, integrated product direction - with scope, what the user sees, deferrals, phases, repos, test strategy and risk. Bundle B is recommended and deliberately not chosen. Answers the six explicit questions: Bundle B needs neither PR #22 nor Netgen/NGSolve; the AGPL/MIT boundary holds if Stabileo exports numbers rather than rules; the first footing needs only production PR18 data; the two missing pieces are a read-only clear-spacing accessor and a STEP/GLB writer. No production code, no CAD artifacts, no new dependencies, no QA or server changes.
Architecture decision brief added — awaiting your decisions
Recommendations, one line each
Bundle B — clash-aware vertical slice — is recommended and deliberately not chosen. Why Decision 5 got the most space
So moving collision authority to CAD is rejected: it would replace a regulation-aware, arc-exact, "Clash-aware RC detail" is achieved under both 5A and 5B; only 5B adds independent Two gaps, and one thing I did not assumeMissing: a read-only required-clear-spacing accessor ( Not verified, flagged rather than glossed: ConfirmedBundle B needs neither PR #22 nor Netgen/NGSolve — the cross-check needs the same OCCT that No production code, no CAD artifacts, no new dependencies, no QA or server changes. |
…evidence
Both were to be settled before freezing fixtures. One shrinks, one opens a
decision.
Required clear spacing - RESOLVED, and my own decision brief was too pessimistic.
It claimed a read-only accessor must be added because detectCollisions receives
requiredClearFor as a caller-supplied callback. Tracing that callback to its
source shows the rule is already implemented and already exported: classifyPair
in engine/detailing/classify.ts returns PairClassification { pairClass,
requiredClear, reportable, refs: ClauseRef[], labelKey }, and
minClearSpacingFor / minClearBetweenLayers are public in
codes/cirsoc201/spacing.ts. Three production callers already use them, and the
floor coordinator builds requiredClearFor as a one-line closure over
minClearSpacingFor - so the callback is an injection seam, not a hiding place.
That is better than an accessor: classifyPair also yields the clause reference,
the reportable flag and an i18n label key, so the manifest gets real provenance,
the CAD side learns which shortfalls Stabileo deliberately does not report, and
EN/ES text reuses PR16 messages. Only serialisation is new. No rule code, no new
engine accessor.
Footing containment - NOT covered, and neither is anything else. checkCover has
seven references in web/src: its own definition and six call sites in
bar-geometry.test.ts. Zero production callers. It is implemented and unit-tested
but unwired, for footings and for beams and columns alike. So Stabileo has no
post-hoc geometric containment verdict in production at all.
Cover is achieved by construction instead: it is a placement input, as
floor-design.ts shows with available = footingThickness - footingCover - 0.05
and inset = panel.cover + d/2. Bars are placed at the right cover
arithmetically; nothing afterwards measures whether the realised geometry sits
inside the concrete.
That makes RcCadHandoffV1's required stabileoVerdict undefined for containment,
which is a material decision. Three options are set out with scope, risk and
product consequence: declare containment unsupported; wire checkCover into the
footing path as new product behaviour; or export cover as placement intent and
have CAD audit the achieved geometry. Recommendation is the third with the
first's honesty framing and the second deferred to its own PR, because it keeps
the approved Bundle B scope and turns a gap into an independent audit of an
assumption nothing currently checks.
Stopping before that choice. Everything else in the manifest is unblocked and
listed. Footing / Pedestal already persist B, L, thickness, rotationDeg,
eccentricities, cover and material ids, so no second footing model is needed.
No production code, no solver changes, no new dependencies.
Bundle B selected — clash-aware vertical sliceThe user has approved Bundle B with all six recommendations from
Seismic is not revived. Its surviving knowledge stays in Foundation questions resolved before implementation —
|
| Scope | Value | Risk | |
|---|---|---|---|
| 2-A declare containment unsupported | smallest, exporter only | honest; CAD adds a capability Stabileo lacks | a CAD bug looks like a design defect |
2-B wire checkCover into the footing path |
materially larger — changes product behaviour; needs a pad abstraction, SectionPrism is prismatic |
best long-term; a real internal check | may start failing mature fixtures; outside the approved slice |
| 2-C export cover as placement intent, CAD measures achieved cover | exporter only | strongest cross-check — audits the cover-by-construction assumption nothing currently verifies | will surface genuine discrepancies needing triage |
Recommendation: 2-C, with 2-A's honesty framing, and 2-B deferred to its own PR.
Stopped before this choice — the exporter's stabileoVerdict shape and
unsupportedConditions[] wording depend on it.
Everything else is unblocked: envelope, identity, revisions, units/frame, concrete geometry from the
persisted Footing/Pedestal (B, L, thickness, rotationDeg, eccentricities,
cover, material ids — no second footing model needed), bars with exact arc centres, marks,
roles, layerId, ownerElementIds, required cover, required clear spacing + clause refs, and the
already-wired collision verdict.
Still draft, 0 reviewers. No production code, no solver changes, no new dependencies.
…ed cover Records the approved containment decision - 2-C with 2-A's honesty - and lands the canonical JSON Schema it implies. 2-B is NOT implemented here; generalized authoritative cover validation is deferred to a probable PR20 with its own handoff. Cover is deliberately not a global scalar. requirements.cover is a list of coverRequirement, each keeping requirementId, elementId, elementType, optional surface (face/region/note), optional appliesToBarIds, distance, unit, category and provenance as separate concepts. An absent surface means Stabileo does not model a per-surface distinction - a stated limitation rather than a wildcard - and face is an open string, not a closed enum, because the production model has no face roles yet and fixing an enum now would encode guesses. category admits only placementInput, codeDerived and userSpecified, the three the model actually distinguishes today. Results are per check rather than one aggregate boolean, so bar collisions, clear spacing, cover and containment each keep their own authority, evaluation status, provenance and result. authority is stabileo or none; evaluationStatus is EVALUATED or NOT_EVALUATED; notEvaluatedReason is REQUIRED when not evaluated, enforced by an if/then in the schema so a consumer cannot quietly omit the reason. Footing containment will therefore travel as authority none, NOT_EVALUATED, with the concrete reason that checkCover has no production caller. Arc geometry keeps its centre as the exact representation and carries an explicit arcApproximated flag for the chord fallback, so a degraded arc is declared rather than silently exact. clearSpacingRequirement carries reportable, so a consumer respects the pair classes Stabileo intentionally suppresses instead of raising conflicts Stabileo does not. The PR20 migration needs no schema redesign: only authority, evaluationStatus and the presence of findings change, and requirementId keys stay stable across it. Schema only - no serialiser, no exporter wiring, no product behaviour change, no cover UI, and no hard-coded 50 mm anywhere.
Containment decision recorded: 2-C with 2-A's honesty —
|
| Rule | Implemented as |
|---|---|
| Stabileo exports the cover requirement it genuinely owns | Footing.cover / footingCover, value preserved — 50 mm never hard-coded |
Footing containment reported NOT_EVALUATED |
authority: "none", with a required notEvaluatedReason naming the cause: checkCover has zero production callers |
| CAD measures achieved cover independently | as a geometric observation |
| A CAD measurement is not a regulatory verdict or certificate | review artifact must label it so |
CAD may never turn NOT_EVALUATED into PASS |
distinct NOT_COMPARABLE state |
Schema landed — web/src/lib/export/rc-cad-handoff.schema.json
Cover is not a global scalar. requirements.cover is a list; each entry keeps
requirementId, elementId, elementType, optional surface (face/region/note),
optional appliesToBarIds, distance, unit, category and provenance as separate
concepts. An absent surface means Stabileo does not model a per-surface distinction — a stated
limitation, not a wildcard. face is an open string, not a closed enum, because the production
model has no face roles yet and fixing an enum now would encode guesses. category admits only
placementInput | codeDerived | userSpecified — the three the model actually distinguishes.
Results are per check, never one aggregate boolean. Each check carries its own checkId,
checkKind (barCollision | barClearSpacing | concreteCover | reinforcementContainment),
authority, evaluationStatus, requirementIds, scope and findings. A JSON Schema
if/then makes notEvaluatedReason required when not evaluated, so a consumer cannot quietly
omit the reason.
The PR20 migration needs no redesign — only authority, evaluationStatus and the presence of
findings change; requirementId keys stay stable.
Also: BarSegment keeps the arc centre as the exact representation plus an explicit
arcApproximated flag, and clearSpacingRequirement carries reportable so a consumer respects
the pair classes Stabileo intentionally suppresses.
Gates on QA 77705cb7d
typecheck 490/490 no new · unit 4560 passed / 12 skipped · build PASS · production build
green · Playwright 123 passed / 4 skipped · WASM rebuilt, 0 tracked. Serving
http://127.0.0.1:4000/.
Blocked — one decision needed
Implementation of the serialiser is paused on a listed stop-condition (unavailable footing
geometry): no committed fixture JSON contains a footing. grep -l '"footings"' across all 56
fixtures returns nothing, and the existing run-footing-design.test.ts builds its footing from the
newFooting() factory with synthetic reactions. Since the acceptance criteria require the footing to
come through the production model path with no test-only shadow footing, how the demonstration
footing is sourced is a material choice. Options and a recommendation are in the report.
Still draft, 0 reviewers. Schema only — no serialiser, no exporter wiring, no cover UI.
…exporter The user selected F-2, a dedicated committed project fixture loaded through real snapshot restoration. Tracing the code shows the two fixture formats are not interchangeable, and only one of them restores a snapshot. templates/fixtures/*.json is replayed by loadFixture, which calls addNode, addElement and friends with id remapping - it never touches modelStore.restore. A .ded project is a DedalFile carrying a snapshot, produced by serializeProject and read back by deserializeProject, which does call modelStore.restore. So the canonical F-2 fixture must be a DedalFile; that is also the path the user's own Open and Save actions use. Base model is rc-design-qa-8.json, already used by the detailing suites, with four fixed3d supports at nodes 1 to 4 - exactly the ground nodes a footing attaches to. It is loaded, extended in memory and serialised to a NEW file; the shared fixture is not modified. The footing is added with the same store actions FoundationsPanel.svelte calls and footing-persistence.test.ts already exercises: addSoilProfile, updateSoilProfile, addFooting, updateFooting. DetailingAssembly is derived state and is not part of ModelSnapshot, so the fixture persists only the project and the test recomputes the assembly through the production detailing action. Detailing output is never seeded into the fixture. Cover stays a model value read from Footing.cover; 50 mm is not hard-coded anywhere. Recipe verified, fixture not yet generated, exporter not yet written. Stopping at a clean checkpoint rather than starting a multi-file exporter that could not be finished and tested in the same pass.
…red through production paths
The F-2 fixture, generated only through production actions and read back through
the real snapshot restoration path.
Built by: modelStore.loadExample('rc-design-qa-8') - the production example
loader - then addSoilProfile, updateSoilProfile, addFooting and updateFooting,
the same store actions FoundationsPanel.svelte calls, then serializeProject().
Read back by deserializeProject(), which is the only path that reaches
modelStore.restore(). templates/fixtures/*.json is replayed by loadFixture and
never restores a snapshot, which is why the canonical fixture is a .ded
DedalFile.
rc-design-qa-8.json is not modified. The founding node is chosen dynamically as
the lowest fully-fixed support, because loadFixture remaps ids and hard-coding
node 1 would silently drift.
Lives under export/__fixtures__ rather than templates/fixtures because it is a
project, not a loadable template: fixture-validation.test.ts rightly requires
everything in templates/fixtures to be registered in fixture-index.ts, and a
DedalFile is not a JSONModel that loadExample could load. The gate caught the
wrong placement; the gate was right.
Footing and soil inputs are illustrative in-domain values, not project-derived
and not regulatory: 2.0 x 2.0 x 0.5 m pad, 0.05 m cover, founding elevation
-1.2 m, allowable bearing 250 kPa, soil provenance recorded as 'assumed'. Cover
is carried on the model and read from there by the exporter; nothing downstream
hard-codes it.
Four tests: production build and optional regeneration behind WRITE_FIXTURE;
restoration proving the footing, its node, its surviving fixed support and its
soil-profile reference; an assertion that the snapshot persists NO derived
detailing state, so an assembly can never be seeded instead of recomputed; and a
regeneration check comparing model content with the production save timestamp
excluded, since nothing was patched to force a fixed timestamp.
Fixture sha256 50ded28d0fd701159c2b72d7e96a4210c0ecb5f7280753edb1ac66ac04cf5ce0,
11752 bytes.
Gates: focused 287 passed; full unit 4550 passed / 12 skipped; build project
PASS; typecheck 490/490 no new; production build green.
Checkpoint: canonical F-2 footing fixture —
|
…tailingAssembly
Executable evidence for the risk boundary, driving only production actions:
committed .ded -> deserializeProject() -> modelStore.restore()
-> modelStore.solveCombinations3DParallel() real WASM solver
-> resultsStore.setCombinationResults3D() production results sink
-> detailingStore.generateFloors() the production footing entry point
-> RunFootingDesignResult + DetailingAssembly
Nothing seeded: no reactions, no solver results, no bars, no marks, no
conflicts, and the footing is not reconstructed after restore.
Two real findings, both fixed or recorded rather than worked around.
First, the footing initially reported footing.run.noColumn and produced no
entry, because the fixture never set columnElementId. Fixed in the generator by
selecting the vertical element rising from the founded node - chosen dynamically,
since loadFixture remaps ids - and setting it through updateFooting, the same
production action the panel uses. The footing is now verified: entriesByLevel 1,
trace '1 de 1 zapata(s) verificada(s)'.
Second, detailingStore.assemblies read 0 while the run and the persisted model
both held the assembly. That getter is a , and production code already
notes a does not recompute inside the synchronous call that wrote it, so
outside a Svelte component it reads stale. The tests read the run result and
modelStore.model.detailing instead. This was a harness artefact, not a
production failure.
Measured production assembly: FLOOR--1.200 'Nivel -1.20 m', 1 element, state
VERIFIED, maturity IMPLEMENTED_PROVISIONAL, detailingRevision 1, demandRevision
0, 0 unsupported at assembly level.
Bars are 0, and that is asserted with its production reason rather than hidden:
the column carries no provided reinforcement in this fixture, so the run reports
footing.run.noColumnBars and generates no dowels or starters. A test asserts that
zero bars must be explained by that reason, so the gap cannot go quiet.
Fixture regenerated with columnElementId, sha256
d8522083651e3330cfa502836b1d242f3ac6e3ade55d0f1c2d3e2ba07777f38b.
Gates: focused suites green; full unit and build projects green; typecheck 490/490
no new; production build green.
…o never The previous commit pushed five new tsc errors: casting the assembly and bar arrays through 'never' made every property access on them invalid, so the typecheck gate reported new errors against its 490 baseline. Caught by running the gate after the push rather than trusting the earlier run. Import DetailingAssembly and BarPath and use them directly. Also replaced the optional resultsStore.clearAll?.() with the real clear(), which is the method the store actually exposes - the optional call silently did nothing and would have left results between cases. Typecheck is back to 490 reported against baseline 490, no new errors. The restored-project-to-assembly suite stays at 7 passed.
…ting assembly
The chain that was missing. Driven only through production commands:
deserializeProject -> modelStore.restore
-> solveCombinations3DParallel real WASM
-> resultsStore.setCombinationResults3D
-> designRunStore.computeDemands production command 1
-> designRunStore.runCodeCheck production command 2
-> designRunStore.designAll production command 3
-> detailingStore.generate beam/column detailing
-> detailingStore.generateFloors footing detailing
Nothing seeded: no forces, no reinforcement, no candidates, no bars. Every case
restores and clears modelStore, resultsStore, detailingStore and the design marks
first, so nothing leaks between them.
Measured result. The footing's referenced column, element 1, is designed to
8 dia-16 longitudinal with dia-6 stirrups at 0.175 m, and the outcome carries
accepted provided reinforcement. footing.run.noColumnBars disappears; the run
trace reads '1 de 1 zapata(s) verificada(s), 1 condicion(es) no soportada(s)',
the remaining condition being footing.run.serviceLateralExcluded, which is an
assumption about lateral service cases rather than a blocker.
The footing assembly is now non-empty: 14 bars, 2 marks, roles longitudinal and
transverse, every bar carrying a stable id, positive diameter, owning elements,
segments and a cutting length. 38 arc segments, and all 38 carry their centre -
so exact arc geometry is available to the exporter and no chord approximation is
forced.
Both the negative and positive cases are asserted, so the earlier structured
blocker cannot regress into silence: without design, zero bars AND
footing.run.noColumnBars; with design, non-empty. A regeneration case pins bar,
mark and assembly counts across two independent runs.
Gates run before pushing: focused export/store/detailing suites 1310 passed;
full unit 4562 passed / 12 skipped; build project PASS; typecheck 490 against
baseline 490 with no new errors; production build green. No solver, Rust, Cargo
or WASM files staged.
… cage Completes the Stabileo side of the PR19 POC: the production Foundations export that turns a real detailed footing into a versioned, deterministic CAD handoff. What the manifest describes, and what it deliberately does not -------------------------------------------------------------- The subject is the footing column-transfer cage: the column dowels and starter ties connecting the footing to the supporting column, the footing pad, a stub of that column, and the interface between the two. It is NOT complete footing reinforcement. PR18's documented scope gives footings real dowels and starter ties while the bottom and top mats remain drawing requirements rather than bar geometry, so `assembly.completeness` is `partialConnectionOnly`, `FOOTING_MAT_GEOMETRY_NOT_MODELED` is declared as an unsupported condition, and semantic validation refuses any document that claims otherwise. The enum carries a `completeFootingReinforcement` member the producer never emits precisely so that refusal is a real check rather than a vacuous one. Nothing is invented ------------------- Every number is read from the restored production model or the production assembly. The column footprint is the section b/h of the element the footing references, read exactly as `collectFootingColumns` reads it — 0,40 x 0,40 m here, never a generic size. The stub base is `foundingElevation + thickness`, the identical expression `run-footing-design` hands the dowel generator as `footingTopZ`. The stub top is the highest point of the production cage's steel, clamped to the column top. Two derivations had a wrong answer available. Taking the maximum segment endpoint z understates the cage top and leaves the upper starter tie protruding through the concrete, so extents are grown by the bar radius AND measured on the arc-aware production sampler at `COLLISION_CHORD_TOLERANCE` — which finds an arc extreme the endpoints miss by ~2 mm. That is the same class of error the stored arc `centre` exists to prevent. And the stub's upper face is a truncation, not a surface: it carries `truncatedFaces: ['top']` and the cover requirement excludes it, because a cut plane admits no cover measurement. A real discontinuity is surfaced rather than smoothed: element 1's base node sits 0,700 m above the footing top, so the analysis model's column does not reach its own footing. The stub is taken from the footing top because that is where the production bars are, and the gap is exported with all three numbers. The interface is not an exposed face ------------------------------------ The footing top beneath the column is an intentional concrete-to-concrete contact. It is emitted explicitly, marked `exposure: 'internal'`, excluded from the cover requirement's measurable surfaces, and carries the crossing bars as `intentionalBarPassage` with the clause that makes them intentional. A consumer therefore has three independent reasons not to read a dowel penetration as a breach. Cover, as decided ----------------- Stabileo exports the footing cover placement intent it already owns (`Footing.cover`, never a hard-coded 50 mm) and reports containment as `NOT_EVALUATED`, because `checkCover` has zero production callers. Column cover is `OUT_OF_SCOPE`: the footing value is not applied to the stub and no column cover requirement is emitted at all. `consumerObservationPolicy` is what makes this enforceable — `MAY_OBSERVE_NOT_COMPARABLE` permits a measurement and forbids presenting it as a verdict, `OUT_OF_SCOPE` forbids the measurement. Collapsing the two would let a consumer report an unevaluated property as passing. Clear spacing keeps two shapes that are not interchangeable. The code-derived rule is a pure function of edition, member kind and diameter, so it is exported per role pair from `minClearSpacingFor` with its clause refs. A per-pair verdict is not: `classifyPair` needs the collision detector's closest-approach data, and re-deriving it here would produce a different class than production. So per-pair entries exist only for the pairs Stabileo actually reported, and that limitation is stated rather than filled in by guessing. The twelve conflicts are real ----------------------------- The assembly reaches COORDINATED with 12 `prohibitedOverlap` conflicts: all eight dowel hooks turn toward the column centre and interfere at the bottom of the footing. They are exported as findings under `barCollision` with Stabileo authority — not smoothed into a pass. Validation, in two layers ------------------------- Schema validation runs against the shipped JSON Schema through a validator restricted to the keywords that schema uses. Its real risk is silent under-validation, so `assertSupportedSchema` walks the schema first and throws on any keyword it does not implement: a schema that grows past the validator fails loudly instead of checking less than it claims. No new dependency. Semantic validation is where the honesty lives, since a document can be perfectly shaped and still say something false. It refuses duplicate ids, unresolving references, an interface plane that is not where the two solids meet, a box that is not a solid, a non-finite coordinate, an arc that drops its centre without declaring the approximation, a footing cover requirement scoped onto the stub, a cover requirement that does not exclude the internal contact, any containment verdict, and a cross-check invitation where no verdict exists. It lives in its own module free of JSON imports so the browser journey can run it against a downloaded file. Where the exporter refuses -------------------------- Rather than guess: no column reference, an undimensioned or rotated footing, a pedestal, a cage with no dowels, a bar whose family cannot be stated, a missing assembly, and a stale one. The eccentricity refusal is the notable one — `Footing.eccentricityB/L` is documented as the footing centroid's offset while `run-footing-design` uses it as the column centre. At zero they coincide; at a non-zero value they are different places and choosing one would invent where the column stands. Fixture ------- The committed project is now saved as the PRO project it is: `analysisMode` moves from `3d` to `pro`, so it reopens into the mode that owns the Foundations editor instead of landing the user in Basico where their footing is unreachable. The model snapshot is byte-identical. This is what lets the browser journey open a real committed project through the production file input rather than a test-only load hook. A golden manifest is committed alongside it as the shared contract sample, in its own test file so its revision is the one a first export actually reaches. Tests ----- 166 new assertions across four suites plus eight browser journeys, all from the committed .ded through the real WASM solve, the production design commands and the panel's own buttons. Nothing is injected: no assembly, no cage, no manifest. Gates: typecheck 490/490 with no new errors, full Vitest 4664 passed, production build, full Playwright 131 passed.
POC / IMPLEMENTATION PHASE — Stabileo producer complete, CAD consumer not started
Still a draft. Do not review or merge. The Stabileo half of the POC is now real production
code with real gates; the text-to-cad consumer (STEP/GLB, the independent cross-check, the review
artifact) has not been started.
Builds directly on PR15–PR18
7d0ab24ef937cd0d7e5f78808d806adcabdab80cb13c81ce843e5fe5ca14619a8f2f1b441df22c8f86b8b3acea2d2630e48e05e776a1e95c04c6a50cd19588ef3ecd21b4bc1bd8d054fdbaca961ed040Not based on a QA branch. No solver / Rust / Cargo / WASM changes.
What this branch now produces
A production Foundations → Export CAD handoff action that turns a really-detailed footing into
a versioned, deterministic
RcCadHandoffV1manifest.The subject is a connection, and it says so
This is not complete footing reinforcement, and the manifest is built so it cannot be read as
such.
assembly.kind = footingTransferCage,assembly.completeness = partialConnectionOnly, andFOOTING_MAT_GEOMETRY_NOT_MODELEDis a declared unsupported condition with a stable code.Semantic validation refuses a document that claims
completeFootingReinforcementwhile thatcondition is present — the enum member exists so the refusal is a real check rather than a vacuous
one, and it equally refuses a transfer cage that omits the condition.
Nothing is invented
Footing.B/.L/.thickness/.foundingElevationfoundingElevation + thickness— the same expressionrun-footing-design.ts:596hands the dowel generator asfootingTopZb/hof element 1, read exactly ascollectFootingColumnsreads itFooting.cover. 50 mm is never hard-coded anywhereNo generic column size. No arbitrary stub height. No invented anchor bolts, hooks, tolerances, BIM
semantics or fabrication approval.
Two derivations that had a wrong answer available
The stub top is a surface extent, not a centreline extent. Reading the maximum segment endpoint
zgives 0,4423 m — and then the top starter tie protrudes through the concrete by its own radius.Two corrections were needed and only the first is obvious: grow by
d/2, and measure on thearc-aware production sampler (
samplePathatCOLLISION_CHORD_TOLERANCE= 0,5 mm), which findsa true arc extreme the endpoints miss. Endpoint arithmetic would have understated the stub by ~2 mm
and left steel outside the concrete — the same class of error the stored arc
centreexists toprevent.
The stub's upper face is a truncation, not a surface. It carries
truncatedFaces: ['top']andthe cover requirement sets
excludeTruncatedFaces. A cut plane admits no cover measurement.A real discontinuity, surfaced rather than smoothed
Element 1's base node sits at z = 0, which is 0,700 m above the footing top. The analysis
model's column does not reach its own footing. The stub is taken from the footing top anyway,
because that is where the production dowels and starter ties actually are, and the gap is exported
as
COLUMN_ELEMENT_BASE_ABOVE_FOOTING_TOPwith all three numbers. No concrete is invented to closeit.
The interface is not an exposed face
The footing top beneath the column is an intentional concrete-to-concrete contact. A dowel crossing
it is the detail working. It is emitted explicitly, marked
exposure: 'internal', listed in thecover requirement's
excludeInterfaceIds, and carries the crossing bars asintentionalBarPassagewith §16.3.4. A consumer has three independent reasons not to read apenetration as a breach, and a test asserts no cover or containment finding is ever raised against
a crossing bar.
Per-check authority — and the field that makes it enforceable
barCollisionstabileoEVALUATED— 12 findingsMAY_CROSS_CHECKbarClearSpacingstabileoEVALUATED— 0 findingsMAY_CROSS_CHECKconcreteCover(footing)noneNOT_EVALUATEDMAY_OBSERVE_NOT_COMPARABLEconcreteCover(column stub)noneNOT_EVALUATEDOUT_OF_SCOPEreinforcementContainmentnoneNOT_EVALUATEDMAY_OBSERVE_NOT_COMPARABLEconsumerObservationPolicyis new and it is what turns the 2-C decision into something checkable.The distinction between its members is the whole point:
MAY_OBSERVE_NOT_COMPARABLEpermits ameasurement and forbids presenting it as a verdict;
OUT_OF_SCOPEforbids the measurement itself.Collapsing the two would let a consumer report an unevaluated property as passing. Cover is two
checks scoped to two bodies, so the stub cannot inherit the footing's status by omission.
barClearSpacingisEVALUATEDwith zero findings, and that is a result rather than anabsence: 78 pairs were evaluated and every reported one was a prohibited overlap, judged by rule 1
of the classifier before any spacing clause applied.
The twelve conflicts are real, and they are exported
The production assembly reaches
COORDINATEDwith 12prohibitedOverlapconflicts andconstructibility
CONFLICTED: all eight dowel hooks turn toward the column centre and interfere atthe bottom of the footing. A genuine finding about this fixture's geometry, exported as twelve
findings carrying Stabileo's own
pairClass— not smoothed into a pass, not hidden behind asummary boolean.
Clear spacing: two shapes that are not interchangeable
classifyPairtakessurfaceClearanceand the tangents at closest approach, so it isgeometry-dependent. Re-running it in the exporter without the collision detector's data would
produce a different
pairClassthan production — exactly the divergence that makes a secondimplementation worse than none. So the manifest carries:
minClearSpacingForwith its clauserefs (column Ø16 → 40 mm, absolute floor; two beam-rule pairs → 26,67 mm, aggregate term);
pairClass.Pairs Stabileo did not report get no per-pair entry. Stated as a limitation, not filled in.
Cover remains deferred
Stabileo exports the cover placement intent it owns and reports containment as
NOT_EVALUATEDwith a mandatory reason, because
checkCoverhas zero production callers — for any element type.CAD may measure achieved cover as an independent geometric observation and may never turn
NOT_EVALUATEDintoPASS. Generalized authoritative cover validation stays indocs/handoffs/deferred-cover-validation-pr20.md. No production behaviour changed outside thenew export.
Validation, in two layers
Schema — the shipped JSON Schema, checked by a validator restricted to the keywords that schema
uses. A hand-written validator's real risk is not a wrong rejection but silent
under-validation, so
assertSupportedSchemawalks the schema first and throws on any keyword itdoes not implement. A schema that grows past the validator fails loudly instead of checking less
than it claims. No new dependency in the browser bundle.
Semantic — where the honesty lives, since a document can be perfectly shaped and still say
something false. 36 rules: unique ids; resolving references; valid interface participants; an
interface plane that must actually be where the two solids meet; positive dimensions and diameters;
finite coordinates; an arc that drops its centre must declare the approximation; no footing cover
requirement on the stub; no cover requirement that fails to exclude the internal contact; no
containment verdict at all; no cross-check invitation without a verdict; every body needs its own
cover check. It lives in a module free of JSON imports so the browser journey can run it against a
downloaded file.
Where the exporter refuses rather than guessing
NO_COLUMN_REFERENCE·FOOTING_NOT_DIMENSIONED·FOOTING_ROTATION_NOT_RESOLVED·FOOTING_ECCENTRICITY_NOT_RESOLVED·PEDESTAL_NOT_SUPPORTED·NO_TRANSFER_CAGE·NO_COLUMN_DOWELS·UNCLASSIFIED_CAGE_BAR·NO_ASSEMBLY·STALE_ASSEMBLY·INVALID_MANIFESTThe eccentricity refusal is the one worth reading.
Footing.eccentricityB/Lis documented as theoffset of the footing centroid from the node, while
run-footing-designuses node + eccentricityas the column centre. At zero the two coincide and nothing is ambiguous. At a non-zero value
they are different places, and choosing one would invent where the column stands.
Every refusal is shown verbatim in the panel, in EN and ES. No disabled button without an
explanation.
The artifact
web/src/lib/export/__fixtures__/rc-footing-cad-poc.handoff.jsonrc-cad-handoff-Z1-det3-dem2.json795e9de26f2eb8ce8d51f2ac7130336702fc534588f390071e3bd40bc03aa0e7RcCadHandoffV1,schemaVersion115ce4e150919bf8f91ef1e3fae36dcde584b770fea45861465742654153e3e79Committed as the shared contract sample (Decision 2A), in its own test file so its revision is the
one a first export actually reaches rather than whatever a suite of earlier cases left a
monotonic counter at.
Contents: 8 column dowels Ø16 · 6 starter ties Ø6 · 14 bars · 2 marks (F1 ×6, F2 ×8) · 90
segments including 38 arcs, all 38 with exact centres and none approximated · 2 concrete bodies
· 1 interface · 12 collision findings · 5 checks · 3 unsupported conditions · 4 assumptions.
The filename carries identity and revisions and no timestamp: a timestamp makes two identical
exports look different and a stale one look fresh.
Fixture change
The committed
.dedis now saved as the PRO project it is —analysisModemoves3d→pro.The old value produced a file that, reopened, landed the user in Básico where the footing they
created is neither visible nor reachable; that was an artifact of the generator test's default, not
a decision. The model snapshot is byte-identical; only the saved mode and the serializer's
timestamp changed. This is what lets the browser journey open a real committed project through the
production file input instead of a test-only load hook.
Tests
105 unit test cases (260 assertions) across four suites, plus eight browser journeys. Every one starts from the
committed
.ded, restores it throughdeserializeProject→modelStore.restore, solves with thereal WASM solver, and runs the production design and detailing commands. Nothing is
injected — not the assembly, not the cage, not the manifest.
rc-cad-handoff-manifest.test.ts(47) — component extraction, stub derivation, interface geometry,transfer-cage classification, exact arcs, ID resolution, cover scoping, containment
NOT_EVALUATED, column cover not evaluated, interface passage never a failure, collisionpreservation pair-for-pair against the assembly, determinism, fixture reconciliation
rc-cad-handoff-validation.test.ts(43) — 43 rejections, each mutating exactly one thing on the realmanifest, including the validator's own refusal to under-enforce a schema
rc-cad-handoff-golden.test.ts(3) — byte-exact golden reproductionrc-cad-export-prerequisites.test.ts(12) — every refusal, including a stale assembly after a realdemand-revision bump
e2e/rc-cad-handoff.spec.ts(8) — the project is opened through the toolbar's file picker, thecage is generated by the panel's own button, and the manifest is downloaded by the panel's own
export button. The downloaded bytes are then re-validated against the committed schema file. EN
and ES, including a check that no exported sentence is a bare i18n key.
Gates, all green before this push
npm run typechecknpm run test(unit)npm run test(build)npm run buildnpx playwright testNot started
cad-review.jsoncross-check-disagreementas an issue kindQA note
http://127.0.0.1:4000/is still serving the previous green QA headdb311d674(
qa/main-plus-pr19-cad) and does not contain this commit. Main, PR15 and PR16 are beingupdated in parallel, so no propagation or QA rebuild was attempted. QA is knowingly stale with
respect to this branch.