test: strengthen core harness guardrails
This commit is contained in:
@@ -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 |
|
||||
|
||||
+27
-1
@@ -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
|
||||
|
||||
|
||||
+28
-13
@@ -53,6 +53,11 @@ inline bool containsDiagnostic(const std::vector<Diagnostic>& diagnostics, const
|
||||
});
|
||||
}
|
||||
|
||||
inline Diagnostic makeDiagnostic(Severity severity, std::string code, std::string message, std::string keyword,
|
||||
std::string file = "<domain>", 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<GlobalId> numericTarget(const std::string& target) {
|
||||
return parseInt64(target);
|
||||
}
|
||||
|
||||
inline std::vector<GlobalId> resolveNodeTarget(const Domain& domain, const std::string& target, std::vector<Diagnostic>* diagnostics = nullptr) {
|
||||
inline std::vector<GlobalId> resolveNodeTarget(const Domain& domain, const std::string& target, std::vector<Diagnostic>* 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<GlobalId> 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<Diagnostic> validateDomain(const Domain& domain) {
|
||||
std::vector<Diagnostic> 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;
|
||||
}
|
||||
|
||||
@@ -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" },
|
||||
|
||||
+34
-5
@@ -3,10 +3,17 @@
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <type_traits>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
static_assert(std::is_same_v<fesa::Real, double>, "Real must remain double");
|
||||
static_assert(std::is_same_v<fesa::GlobalId, std::int64_t>, "GlobalId must remain int64");
|
||||
static_assert(std::is_same_v<fesa::LocalIndex, std::int64_t>, "LocalIndex must remain int64");
|
||||
static_assert(std::is_same_v<fesa::EquationId, std::int64_t>, "EquationId must remain int64");
|
||||
static_assert(std::is_same_v<fesa::SparseIndex, std::int64_t>, "SparseIndex must remain int64");
|
||||
|
||||
namespace {
|
||||
|
||||
using TestFn = std::function<void()>;
|
||||
@@ -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<fesa::GlobalId>::is_signed);
|
||||
FESA_CHECK(std::numeric_limits<fesa::LocalIndex>::is_signed);
|
||||
FESA_CHECK(std::numeric_limits<fesa::EquationId>::is_signed);
|
||||
FESA_CHECK(std::numeric_limits<fesa::SparseIndex>::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<int>(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) {
|
||||
|
||||
Reference in New Issue
Block a user