diff --git a/docs/implementation-plans/linear-static-3d-euler-beam-implementation-report.md b/docs/implementation-plans/linear-static-3d-euler-beam-implementation-report.md index 130c8ce..8943d70 100644 --- a/docs/implementation-plans/linear-static-3d-euler-beam-implementation-report.md +++ b/docs/implementation-plans/linear-static-3d-euler-beam-implementation-report.md @@ -326,3 +326,36 @@ - Focused re-review returned Critical 0 and Important 0. Its sole remaining minor requested an explicit negative transverse-shear context assertion; that table case was added and passes without a production change. + +### Step 12 Root Review Fix Round 1 — same-token target ambiguity + +- finding: `resolveNodeTarget` returned one matching node set before computing + the direct numeric source-label candidates. A token such as `1` therefore + silently selected `NSET=1` even when node source label `1` was also a valid + interpretation. +- regression_scope: `RejectsUnsupportedAndInvalidPortfolio` now constructs an + `NSET` named `1` containing node `2` while node label `1` also exists, and + exercises the same token independently through both `*BOUNDARY` and + `*CLOAD`. It requires input-category `unresolved-reference` with exact + keyword, entity identity `1`, and original data-row file/line location. +- RED-build: `cmake --build .harness/build --config Debug --target fesa_tests` + exited 0 after registering the two regression paths. +- RED-test: `ctest --test-dir .harness/build -C Debug -R "InpDomainMapping.RejectsUnsupportedAndInvalidPortfolio" --output-on-failure` + exited 8. The test failed at `same-token-boundary` + because `result.hasValue()` was true instead of the required structured + failure. +- fix: target resolution computes both name-matched node sets and numeric + direct-node candidates before returning either. Multiple candidates within + an interpretation or one valid candidate from each interpretation use the + existing input/`unresolved-reference` ambiguity contract; unique set-only + and direct-only paths remain unchanged. +- GREEN: the targeted build exited 0 and `ctest --test-dir .harness/build -C Debug -R InpDomainMapping --output-on-failure` + exited 0 with 5/5 passed. +- VERIFY: approved explicit-dependency MSVC x64 configure and full Debug build + exited 0; discovery found 19 tests including all five mapper names; full + CTest passed 19/19. Backend/upward/core-upward/distributed-load/unapproved + `nu` scans were 0, exactly five mapper tests remained, `git diff --check` + exited 0, and reference diff/status were clean. +- deferred_scope: the separate known-but-misplaced keyword diagnostic Minor was + deliberately not changed in this fix round. No phase-index change and no + commit were made. diff --git a/src/fesa/io/abaqus/domain_mapper.cpp b/src/fesa/io/abaqus/domain_mapper.cpp index 22e01ac..66a7867 100644 --- a/src/fesa/io/abaqus/domain_mapper.cpp +++ b/src/fesa/io/abaqus/domain_mapper.cpp @@ -2026,19 +2026,10 @@ private: matchingSets.push_back(&set); } } - if (matchingSets.size() == 1U) { - return matchingSets.front()->nodeIndices; - } - if (matchingSets.size() > 1U) { - inputFailure( - "unresolved-reference", location, keyword, target, - "The node-set target is ambiguous across identity instances."); - return std::nullopt; - } + std::vector matchingNodes; std::int64_t label = 0; if (tryPositiveInteger(target, label)) { - std::vector matchingNodes; for (std::size_t index = 0U; index < definition_.nodes.size(); ++index) { @@ -2046,9 +2037,34 @@ private: matchingNodes.push_back(static_cast(index)); } } - if (matchingNodes.size() == 1U) { - return matchingNodes; - } + } + + // A token is resolved only after both approved interpretations have + // been considered; declaration order never gives a node set priority + // over an equally valid direct source-label target. + if (matchingSets.size() > 1U) { + inputFailure( + "unresolved-reference", location, keyword, target, + "The node-set target is ambiguous across identity instances."); + return std::nullopt; + } + if (matchingNodes.size() > 1U) { + inputFailure( + "unresolved-reference", location, keyword, target, + "The direct node-label target is ambiguous across identity instances."); + return std::nullopt; + } + if (matchingSets.size() == 1U && matchingNodes.size() == 1U) { + inputFailure( + "unresolved-reference", location, keyword, target, + "The target is ambiguous between a node-set name and a direct node label."); + return std::nullopt; + } + if (matchingSets.size() == 1U) { + return matchingSets.front()->nodeIndices; + } + if (matchingNodes.size() == 1U) { + return matchingNodes; } inputFailure( "unresolved-reference", location, keyword, target, diff --git a/tests/unit/io/abaqus/domain_mapper_test.cpp b/tests/unit/io/abaqus/domain_mapper_test.cpp index 7489da4..6c471ef 100644 --- a/tests/unit/io/abaqus/domain_mapper_test.cpp +++ b/tests/unit/io/abaqus/domain_mapper_test.cpp @@ -573,6 +573,41 @@ TEST(InpDomainMapping, RejectsUnsupportedAndInvalidPortfolio) { EXPECT_FALSE(diagnostic->location.file.empty()); EXPECT_GT(diagnostic->location.line, 0U); } + + struct SameTokenAmbiguityCase { + std::string name; + std::string deck; + std::string expectedKeyword; + std::size_t expectedLine; + }; + const std::string sameTokenBase = replaceOnce( + base, + "*Elset, elset=BeamSet", + "*Nset, nset=1\n2\n*Elset, elset=BeamSet"); + const std::vector sameTokenCases{ + {"boundary", replaceOnce(sameTokenBase, "Root, 1, 6", "1, 1, 6"), + "BOUNDARY", 27U}, + {"cload", replaceOnce(sameTokenBase, "Tip, 2, -1.", "1, 2, -1."), + "CLOAD", 32U}}; + + for (const auto& testCase : sameTokenCases) { + SCOPED_TRACE("same-token-" + testCase.name); + auto result = mapText("same-token-" + testCase.name, testCase.deck); + ASSERT_FALSE(result.hasValue()); + EXPECT_EQ( + result.status().failureCategory(), + fesa::FailureCategory::input); + const auto* diagnostic = + findDiagnostic(result.status(), "unresolved-reference"); + ASSERT_NE(diagnostic, nullptr); + EXPECT_EQ(diagnostic->severity, fesa::Severity::error); + EXPECT_EQ(diagnostic->keyword, testCase.expectedKeyword); + EXPECT_EQ(diagnostic->entityIdentity, "1"); + EXPECT_EQ(diagnostic->location.line, testCase.expectedLine); + EXPECT_EQ( + diagnostic->location.file.filename().string(), + "fesa-domain-mapper-same-token-" + testCase.name + ".inp"); + } } TEST(InpDomainMapping, RejectsDloadWithoutDistributedLoadObject) {