From 99445d43bbf909633895a23b79fea925a6b4b6c6 Mon Sep 17 00:00:00 2001 From: NINI Date: Mon, 4 May 2026 12:46:37 +0900 Subject: [PATCH] test: strengthen core harness guardrails --- PLAN.md | 15 ++++--- PROGRESS.md | 28 ++++++++++++- include/fesa/fesa.hpp | 41 +++++++++++++------ .../index.json | 2 +- tests/test_main.cpp | 39 +++++++++++++++--- 5 files changed, 97 insertions(+), 28 deletions(-) diff --git a/PLAN.md b/PLAN.md index 67c47e5..9395bfb 100644 --- a/PLAN.md +++ b/PLAN.md @@ -13,7 +13,7 @@ Every new agent session must read this file together with `PROGRESS.md` before p - If an item becomes obsolete, move it to `PROGRESS.md` with a short reason instead of silently deleting it. ## Current Objective -Continue the new Phase 1 rebaseline plan in `phases/1-linear-static-mitc4-rebaseline`, starting with P1R-02 core harness guardrails. The old `phases/1-linear-static-mitc4` path is historical and superseded by the paper-based MITC4 formulation reset. +Continue the new Phase 1 rebaseline plan in `phases/1-linear-static-mitc4-rebaseline`, starting with P1R-03 parser/domain subset revalidation. The old `phases/1-linear-static-mitc4` path is historical and superseded by the paper-based MITC4 formulation reset. ## Required Reading For New Agents 1. `AGENTS.md` @@ -36,7 +36,7 @@ Continue the new Phase 1 rebaseline plan in `phases/1-linear-static-mitc4-rebase ## Active Phase Files - Active phase directory: `phases/1-linear-static-mitc4-rebaseline` - Execute with: `python scripts/execute.py 1-linear-static-mitc4-rebaseline` -- Step numbering is zero-based. `step0.md` is complete and recorded in `phases/1-linear-static-mitc4-rebaseline/step0-audit.md`; `step1.md` is complete and created the `quad_02_phase1.inp` normalized reference path; `step15.md` is the independent evaluator closeout. +- Step numbering is zero-based. `step0.md` is complete and recorded in `phases/1-linear-static-mitc4-rebaseline/step0-audit.md`; `step1.md` is complete and created the `quad_02_phase1.inp` normalized reference path; `step2.md` is complete and revalidated core harness guardrails; `step15.md` is the independent evaluator closeout. - Every step file contains a sprint contract with objective, required reading, scope, allowed files, explicit non-goals, tests to write first, reference artifacts, acceptance command, evaluator checklist, and handoff requirements. - Historical phase directory: `phases/1-linear-static-mitc4` - Historical phase status: blocked/superseded. Do not resume the old P1-15/P1-16 path unless the user explicitly requests recovery of that exact phase. @@ -75,7 +75,7 @@ Each gate should be satisfied before moving to the next implementation band unle | G0 - Planning readiness | partial | Readiness task R-011 is resolved by `quad_02_phase1.inp`; R-010 and R-013 remain open. | Updated docs, PLAN.md, PROGRESS.md | | G1 - Build and validation | satisfied | Build system, test framework, and `scripts/validate_workspace.py` run real checks. | Validation command output | | G2 - Parser and domain | pending rebaseline | Must be revalidated through steps 3 and 4 against the current parser subset and stored-reference compatibility policy. | Future parser and validation tests | -| G3 - DOF/math/results infrastructure | pending rebaseline | Must be revalidated through steps 2, 5, 6, and 12. | Future unit and integration tests | +| G3 - DOF/math/results infrastructure | partial | Core aliases, DOF mapping, validation harness, and model diagnostic context were revalidated in step 2; DofManager/results/assembly remain for steps 5, 6, and 12. | P1R-02 validation output | | G4 - MITC4 element readiness | reopened | MITC4 formulation was rewritten from local papers; element implementation must be rebuilt or revalidated through steps 7 through 11. | Revised `docs/MITC4_FORMULATION.md`, future element tests | | G5 - End-to-end solver | reopened | Linear static path must be revalidated through steps 13 and 14 after the MITC4 rebuild and `quad_02` compatibility path. | Future integration/reference regression output | @@ -84,12 +84,11 @@ All milestones are intended to become one or more self-contained sprint contract | ID | Status | Owner | Objective | Depends On | Acceptance Focus | |---|---|---|---|---|---| -| P1R-02 | pending | core generator | Revalidate build/test harness, core aliases, DOF enum, and diagnostics. | P1R-00 | Validation command and core tests | -| P1R-03 | pending | parser generator | Revalidate Phase 1 parser and immutable Domain subset. | P1R-02 | Supported keywords accepted; unsupported features rejected | +| P1R-03 | pending | parser generator | Revalidate Phase 1 parser and immutable Domain subset. | none | Supported keywords accepted; unsupported features rejected | | P1R-04 | pending | validation generator | Rebuild validation and singular diagnostic coverage. | P1R-03 | Missing-reference and singular-prone negative tests | -| P1R-05 | pending | DOF generator | Rebuild six-DOF DofManager, constrained/free mapping, equation numbering, and full-vector reconstruction. | P1R-02 | DOF mapping and reaction foundation tests | -| P1R-06 | pending | results generator | Rebuild minimum results model and displacement CSV comparator. | P1R-02 | U/RF schema tests and CSV comparator tests | -| P1R-07 | pending | MITC4 generator | Implement MITC4 geometry, node order, tying points, directors, and local bases. | P1R-02 | Shape/basis/diagnostic tests | +| P1R-05 | pending | DOF generator | Rebuild six-DOF DofManager, constrained/free mapping, equation numbering, and full-vector reconstruction. | none | DOF mapping and reaction foundation tests | +| P1R-06 | pending | results generator | Rebuild minimum results model and displacement CSV comparator. | none | U/RF schema tests and CSV comparator tests | +| P1R-07 | pending | MITC4 generator | Implement MITC4 geometry, node order, tying points, directors, and local bases. | none | Shape/basis/diagnostic tests | | P1R-08 | pending | MITC4 generator | Implement degenerated-continuum displacement, covariant strain rows, and MITC shear tying. | P1R-07 | Finite-difference and tying interpolation tests | | P1R-09 | pending | MITC4 generator | Implement material matrix, transform, and `2 x 2 x 2` integration scaffolding. | P1R-08 | Material/integration tests | | P1R-10 | pending | MITC4 generator | Assemble MITC4 stiffness/internal force with six-DOF transform and drilling stabilization. | P1R-09, P1R-05 | Symmetry, rigid body, drilling sensitivity tests | diff --git a/PROGRESS.md b/PROGRESS.md index d687f0d..c27b8ea 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -13,10 +13,36 @@ Every new agent session must read this file together with `PLAN.md` before plann - Do not remove history unless the user explicitly asks for archival cleanup. ## Current Status -Phase 1 has a new rebaseline phase definition in `phases/1-linear-static-mitc4-rebaseline`. Steps 0 and 1 are complete. `quad_02_phase1.inp` is now the normalized Phase 1-compatible input path for the stored `quad_02` S4 reference pair, while the original `quad_02.inp` remains preserved unsupported provenance. The old `phases/1-linear-static-mitc4` path is historical and superseded after the MITC4 formulation reset. P1-01 through P1-14 have an initial C++17 implementation, but it is no longer authoritative until each layer is revalidated against the revised paper-based `docs/MITC4_FORMULATION.md`. +Phase 1 has a new rebaseline phase definition in `phases/1-linear-static-mitc4-rebaseline`. Steps 0 through 2 are complete. `quad_02_phase1.inp` is now the normalized Phase 1-compatible input path for the stored `quad_02` S4 reference pair, while the original `quad_02.inp` remains preserved unsupported provenance. Core numeric aliases, DOF mapping, validation harness, and model diagnostic context have been revalidated. The old `phases/1-linear-static-mitc4` path is historical and superseded after the MITC4 formulation reset. ## Completed Work +### 2026-05-04 - P1R-02 core harness guardrails completed +Author: Codex + +Changed files: +- `include/fesa/fesa.hpp` +- `tests/test_main.cpp` +- `phases/1-linear-static-mitc4-rebaseline/index.json` +- `PLAN.md` +- `PROGRESS.md` + +Summary: +- Strengthened C++ guardrail tests for centralized numeric aliases: `Real`, `GlobalId`, `LocalIndex`, `EquationId`, and `SparseIndex`. +- Expanded DOF mapping coverage to prove all six shell DOFs round-trip with Abaqus DOF numbers `1..6`, labels, and invalid DOF rejection. +- Added compile-time type checks so the core aliases remain `double` and signed `std::int64_t`. +- Added model-validation diagnostic context through a shared `makeDiagnostic` helper and populated source keywords for element, shell section, boundary, cload, analysis model, and missing target diagnostics. +- Kept the existing CMake/CTest validation path as the real repository validation command. + +Verification: +- First ran `python scripts/validate_workspace.py` after adding tests; it failed as expected because model diagnostics did not yet populate source keywords. +- After implementing the diagnostic guardrail, `python scripts/validate_workspace.py` configured CMake, built `fesa_core` and `fesa_tests`, and ran CTest successfully. +- CTest result: 1 test executable passed. + +Follow-up: +- Continue with P1R-03 parser/domain subset revalidation. +- Leave MITC4 stiffness and parser feature expansion to later dedicated steps. + ### 2026-05-04 - P1R-01 quad_02 reference onboarding completed Author: Codex diff --git a/include/fesa/fesa.hpp b/include/fesa/fesa.hpp index 7c08f82..9154075 100644 --- a/include/fesa/fesa.hpp +++ b/include/fesa/fesa.hpp @@ -53,6 +53,11 @@ inline bool containsDiagnostic(const std::vector& diagnostics, const }); } +inline Diagnostic makeDiagnostic(Severity severity, std::string code, std::string message, std::string keyword, + std::string file = "", LocalIndex line = 0) { + return {severity, std::move(code), std::move(message), {std::move(file), line, std::move(keyword)}}; +} + inline std::string trim(std::string text) { auto is_space = [](unsigned char c) { return std::isspace(c) != 0; }; text.erase(text.begin(), std::find_if(text.begin(), text.end(), [&](unsigned char c) { return !is_space(c); })); @@ -624,11 +629,13 @@ inline std::optional numericTarget(const std::string& target) { return parseInt64(target); } -inline std::vector resolveNodeTarget(const Domain& domain, const std::string& target, std::vector* diagnostics = nullptr) { +inline std::vector resolveNodeTarget(const Domain& domain, const std::string& target, std::vector* diagnostics = nullptr, + const std::string& diagnostic_keyword = "node target") { if (auto node_id = numericTarget(target)) { if (domain.nodes.count(*node_id) == 0) { if (diagnostics != nullptr) { - diagnostics->push_back({Severity::Error, "FESA-VALIDATION-MISSING-NODE", "Missing node target: " + target, {}}); + diagnostics->push_back( + makeDiagnostic(Severity::Error, "FESA-VALIDATION-MISSING-NODE", "Missing node target: " + target, diagnostic_keyword)); } return {}; } @@ -637,7 +644,8 @@ inline std::vector resolveNodeTarget(const Domain& domain, const std:: auto set_it = domain.node_sets.find(Domain::key(target)); if (set_it == domain.node_sets.end()) { if (diagnostics != nullptr) { - diagnostics->push_back({Severity::Error, "FESA-VALIDATION-MISSING-NSET", "Missing node set: " + target, {}}); + diagnostics->push_back( + makeDiagnostic(Severity::Error, "FESA-VALIDATION-MISSING-NSET", "Missing node set: " + target, diagnostic_keyword)); } return {}; } @@ -660,44 +668,51 @@ inline const ShellSection* shellSectionForElement(const Domain& domain, GlobalId inline std::vector validateDomain(const Domain& domain) { std::vector diagnostics; if (domain.elements.empty()) { - diagnostics.push_back({Severity::Error, "FESA-SINGULAR-NO-ACTIVE-ELEMENTS", "No active elements exist in the current model", {}}); + diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-SINGULAR-NO-ACTIVE-ELEMENTS", + "No active elements exist in the current model", "analysis model")); } if (domain.boundary_conditions.empty()) { - diagnostics.push_back({Severity::Warning, "FESA-SINGULAR-NO-BOUNDARY", "No boundary constraints are defined", {}}); + diagnostics.push_back(makeDiagnostic(Severity::Warning, "FESA-SINGULAR-NO-BOUNDARY", "No boundary constraints are defined", "boundary")); } for (const auto& [id, element] : domain.elements) { for (GlobalId node_id : element.node_ids) { if (domain.nodes.count(node_id) == 0) { - diagnostics.push_back({Severity::Error, "FESA-VALIDATION-ELEMENT-MISSING-NODE", "Element references missing node", {}}); + diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-ELEMENT-MISSING-NODE", + "Element " + std::to_string(id) + " references missing node " + std::to_string(node_id), + "element")); } } const ShellSection* section = shellSectionForElement(domain, id); if (section == nullptr) { - diagnostics.push_back({Severity::Error, "FESA-VALIDATION-MISSING-PROPERTY", "Element has no assigned shell section", {}}); + diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-MISSING-PROPERTY", + "Element " + std::to_string(id) + " has no assigned shell section", "element")); } } for (const ShellSection& section : domain.shell_sections) { if (domain.element_sets.count(Domain::key(section.element_set)) == 0) { - diagnostics.push_back({Severity::Error, "FESA-VALIDATION-MISSING-ELSET", "Shell section references missing element set: " + section.element_set, {}}); + diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-MISSING-ELSET", + "Shell section references missing element set: " + section.element_set, "shell section")); } auto material_it = domain.materials.find(Domain::key(section.material)); if (material_it == domain.materials.end()) { - diagnostics.push_back({Severity::Error, "FESA-VALIDATION-MISSING-MATERIAL", "Shell section references missing material: " + section.material, {}}); + diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-MISSING-MATERIAL", + "Shell section references missing material: " + section.material, "shell section")); } else if (material_it->second.elastic_modulus <= 0.0) { - diagnostics.push_back({Severity::Error, "FESA-VALIDATION-INCOMPLETE-MATERIAL", "Material has no valid elastic constants: " + section.material, {}}); + diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-INCOMPLETE-MATERIAL", + "Material has no valid elastic constants: " + section.material, "material")); } } for (const BoundaryCondition& boundary : domain.boundary_conditions) { - (void)resolveNodeTarget(domain, boundary.target, &diagnostics); + (void)resolveNodeTarget(domain, boundary.target, &diagnostics, "boundary"); } for (const NodalLoad& load : domain.loads) { - (void)resolveNodeTarget(domain, load.target, &diagnostics); + (void)resolveNodeTarget(domain, load.target, &diagnostics, "cload"); } const bool any_nonzero_load = std::any_of(domain.loads.begin(), domain.loads.end(), [](const NodalLoad& load) { return std::fabs(load.magnitude) > 0.0; }); if (!any_nonzero_load) { - diagnostics.push_back({Severity::Warning, "FESA-SINGULAR-NO-NONZERO-LOAD", "No nonzero load is defined", {}}); + diagnostics.push_back(makeDiagnostic(Severity::Warning, "FESA-SINGULAR-NO-NONZERO-LOAD", "No nonzero load is defined", "cload")); } return diagnostics; } diff --git a/phases/1-linear-static-mitc4-rebaseline/index.json b/phases/1-linear-static-mitc4-rebaseline/index.json index c952dd1..0d9080c 100644 --- a/phases/1-linear-static-mitc4-rebaseline/index.json +++ b/phases/1-linear-static-mitc4-rebaseline/index.json @@ -4,7 +4,7 @@ "steps": [ { "step": 0, "name": "rebaseline-audit", "status": "completed" }, { "step": 1, "name": "reference-onboarding", "status": "completed" }, - { "step": 2, "name": "core-harness-guardrails", "status": "pending" }, + { "step": 2, "name": "core-harness-guardrails", "status": "completed" }, { "step": 3, "name": "parser-domain-subset", "status": "pending" }, { "step": 4, "name": "validation-singular-diagnostics", "status": "pending" }, { "step": 5, "name": "dof-manager-reaction-foundation", "status": "pending" }, diff --git a/tests/test_main.cpp b/tests/test_main.cpp index a05ae39..f7f4ddb 100644 --- a/tests/test_main.cpp +++ b/tests/test_main.cpp @@ -3,10 +3,17 @@ #include #include #include +#include #include #include #include +static_assert(std::is_same_v, "Real must remain double"); +static_assert(std::is_same_v, "GlobalId must remain int64"); +static_assert(std::is_same_v, "LocalIndex must remain int64"); +static_assert(std::is_same_v, "EquationId must remain int64"); +static_assert(std::is_same_v, "SparseIndex must remain int64"); + namespace { using TestFn = std::function; @@ -103,11 +110,24 @@ fesa::Domain parsedPhase1Domain() { FESA_TEST(core_types_and_dof_mapping_are_stable) { FESA_CHECK(sizeof(fesa::Real) == 8); FESA_CHECK(sizeof(fesa::GlobalId) == 8); + FESA_CHECK(sizeof(fesa::LocalIndex) == 8); FESA_CHECK(sizeof(fesa::EquationId) == 8); - FESA_CHECK(fesa::abaqusDofNumber(fesa::Dof::UX) == 1); - FESA_CHECK(fesa::abaqusDofNumber(fesa::Dof::RZ) == 6); - FESA_CHECK(fesa::dofFromAbaqus(3).value() == fesa::Dof::UZ); - FESA_CHECK(std::string(fesa::dofLabel(fesa::Dof::RY)) == "RY"); + FESA_CHECK(sizeof(fesa::SparseIndex) == 8); + FESA_CHECK(std::numeric_limits::is_signed); + FESA_CHECK(std::numeric_limits::is_signed); + FESA_CHECK(std::numeric_limits::is_signed); + FESA_CHECK(std::numeric_limits::is_signed); + + const auto dofs = fesa::allDofs(); + FESA_CHECK(dofs.size() == 6); + for (std::size_t i = 0; i < dofs.size(); ++i) { + const int abaqus_number = static_cast(i + 1); + FESA_CHECK(fesa::abaqusDofNumber(dofs[i]) == abaqus_number); + FESA_CHECK(fesa::dofFromAbaqus(abaqus_number).value() == dofs[i]); + FESA_CHECK(std::string(fesa::dofLabel(dofs[i])) == fesa::displacementComponentLabels()[i]); + } + FESA_CHECK(!fesa::dofFromAbaqus(0).has_value()); + FESA_CHECK(!fesa::dofFromAbaqus(7).has_value()); } FESA_TEST(parser_accepts_phase1_subset) { @@ -176,11 +196,20 @@ FESA_TEST(domain_validation_reports_missing_property_and_targets) { domain.nodes[2] = {2, {1, 0, 0}}; domain.nodes[3] = {3, {1, 1, 0}}; domain.nodes[4] = {4, {0, 1, 0}}; - domain.elements[1] = {1, fesa::ElementType::MITC4, {1, 2, 3, 4}, ""}; + domain.elements[1] = {1, fesa::ElementType::MITC4, {1, 2, 3, 99}, ""}; + domain.shell_sections.push_back({"MISSING_ELSET", "MISSING_MAT", 0.1}); domain.loads.push_back({"MISSING", 3, 1.0}); auto diagnostics = fesa::validateDomain(domain); + FESA_CHECK(fesa::containsDiagnostic(diagnostics, "FESA-VALIDATION-ELEMENT-MISSING-NODE")); FESA_CHECK(fesa::containsDiagnostic(diagnostics, "FESA-VALIDATION-MISSING-PROPERTY")); + FESA_CHECK(fesa::containsDiagnostic(diagnostics, "FESA-VALIDATION-MISSING-ELSET")); + FESA_CHECK(fesa::containsDiagnostic(diagnostics, "FESA-VALIDATION-MISSING-MATERIAL")); FESA_CHECK(fesa::containsDiagnostic(diagnostics, "FESA-VALIDATION-MISSING-NSET")); + for (const auto& diagnostic : diagnostics) { + FESA_CHECK(!diagnostic.code.empty()); + FESA_CHECK(!diagnostic.message.empty()); + FESA_CHECK(!diagnostic.source.keyword.empty()); + } } FESA_TEST(dof_manager_owns_equation_numbering_and_reconstruction) {