From 0e0afc922470ae00ce561d0aba7806c123e8e9b7 Mon Sep 17 00:00:00 2001 From: David Waltermire Date: Sun, 28 Dec 2025 17:13:31 -0500 Subject: [PATCH 1/9] docs: add PRD for TargetedReportConstraint support (issue #592) Add Product Requirements Document and implementation plan for adding constraint processing support for TargetedReportConstraint. Key design decisions: - Report constraints default to INFORMATIONAL level - Kind mapping: ERROR/CRITICAL -> FAIL, others -> INFORMATIONAL - Semantic: Report = "This MUST NOT be true" (opposite of expect) Closes #592 --- CLAUDE.md | 1 + .../PRD.md | 238 +++++++++++++++++ .../implementation-plan.md | 242 ++++++++++++++++++ 3 files changed, 481 insertions(+) create mode 100644 PRDs/20251228-targeted-report-constraint/PRD.md create mode 100644 PRDs/20251228-targeted-report-constraint/implementation-plan.md diff --git a/CLAUDE.md b/CLAUDE.md index c3c08dff8..d181684b3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -294,6 +294,7 @@ PRDs are stored in `PRDs/-/` with: | PRD | Description | Status | |-----|-------------|--------| | `PRDs/20251206-build-cleanup/` | Build warnings and deprecation removal | In Progress | +| `PRDs/20251228-targeted-report-constraint/` | Add constraint processing support for TargetedReportConstraint (issue #592) | In Progress | ### Completed PRDs diff --git a/PRDs/20251228-targeted-report-constraint/PRD.md b/PRDs/20251228-targeted-report-constraint/PRD.md new file mode 100644 index 000000000..65a646f35 --- /dev/null +++ b/PRDs/20251228-targeted-report-constraint/PRD.md @@ -0,0 +1,238 @@ +# PRD: Add Constraint Processing Support for TargetedReportConstraint + +## Document Information + +| Field | Value | +|-------|-------| +| **PRD ID** | REPORT-592 | +| **Status** | In Review | +| **Author** | David Waltermire | +| **Created** | 2025-12-28 | +| **Last Updated** | 2025-12-28 | +| **GitHub Issue** | [#592](https://github.com/metaschema-framework/metaschema-java/issues/592) | +| **Milestone** | v3.0.0 Milestone 2 | + +--- + +## 1. Overview + +### 1.1 Problem Statement + +The `TargetedReportConstraint` binding class was added in PR #589 as part of the Metapath-based targeting for binding configurations. However, the constraint processing system has not been updated to handle this new constraint type. + +Currently, the constraint loader (`ConstraintBindingSupport`) and validation infrastructure do not: +- Recognize and parse `TargetedReportConstraint` instances +- Evaluate the target Metapath expression to identify target nodes +- Apply the report constraint logic to matched targets + +This leaves the `TargetedReportConstraint` binding class orphaned - it exists but cannot be used in constraint validation. + +### 1.2 Goals + +1. Create a core `IReportConstraint` interface following the existing constraint interface pattern +2. Implement `DefaultReportConstraint` with builder support +3. Update `ConstraintBindingSupport` to parse `TargetedReportConstraint` from binding configuration +4. Integrate report constraints into the validation pipeline +5. Add comprehensive unit tests for the new constraint type + +### 1.3 Non-Goals + +- Modifying the Metaschema module definition (already complete) +- Changing the generated binding class `TargetedReportConstraint.java` (already generated correctly) +- Adding CLI commands for report constraints +- Adding schema generation support for report constraints + +### 1.4 Success Metrics + +| Metric | Current | Target | +|--------|---------|--------| +| TargetedReportConstraint recognized by loader | No | Yes | +| Report constraints validated at runtime | No | Yes | +| Unit test coverage for new code | 0% | 80%+ | +| All existing tests pass | Yes | Yes | + +--- + +## 2. Background + +### 2.1 Current State + +The constraint processing system supports these targeted constraint types: +- `TargetedAllowedValuesConstraint` +- `TargetedExpectConstraint` +- `TargetedMatchesConstraint` +- `TargetedIndexConstraint` +- `TargetedIndexHasKeyConstraint` +- `TargetedIsUniqueConstraint` +- `TargetedHasCardinalityConstraint` + +Each targeted constraint type has: +1. A binding class (generated from Metaschema module) +2. A core interface (e.g., `IExpectConstraint`) +3. A default implementation (e.g., `DefaultExpectConstraint`) +4. Loader support in `ConstraintBindingSupport` +5. Validation logic in `DefaultConstraintValidator` +6. Visitor pattern method in `IConstraintVisitor` + +The `TargetedReportConstraint` binding class exists but lacks items 2-6. + +### 2.2 Technical Context + +**Report vs Expect Semantics:** + +The report constraint is semantically different from expect: +- **Expect constraint:** Fails validation if the test expression evaluates to FALSE +- **Report constraint:** Generates an informational finding when the test expression evaluates to TRUE + +This means report constraints produce findings/reports but do not cause validation failures. They are used for informational purposes, warnings, or advisory messages. + +**Key Files:** + +| Component | Location | +|-----------|----------| +| Binding class | `databind/.../binding/TargetedReportConstraint.java` | +| Constraint interfaces | `core/.../model/constraint/` | +| Constraint implementations | `core/.../model/constraint/impl/` | +| Constraint loader | `databind/.../impl/ConstraintBindingSupport.java` | +| Constraint validator | `core/.../constraint/DefaultConstraintValidator.java` | +| Visitor interface | `core/.../constraint/IConstraintVisitor.java` | + +--- + +## 3. Requirements + +### 3.1 Functional Requirements + +#### FR-1: IReportConstraint Interface +Create an `IReportConstraint` interface that: +- Extends `IConfigurableMessageConstraint` +- Provides access to the test expression via `getTest(): IMetapathExpression` +- Follows the same pattern as `IExpectConstraint` + +#### FR-2: DefaultReportConstraint Implementation +Create a `DefaultReportConstraint` class that: +- Implements `IReportConstraint` +- Provides a builder via `IReportConstraint.builder()` +- Supports all standard constraint properties (id, level, target, message, remarks) + +#### FR-3: Constraint Loading Support +Update `ConstraintBindingSupport` to: +- Handle `TargetedReportConstraint` in all `parse()` method overloads +- Create `IReportConstraint` instances via a new `newReport()` factory method + +#### FR-4: Validation Pipeline Integration +Update the validation system to: +- Call report constraint validation at appropriate points +- Generate findings when report test expressions evaluate to TRUE +- Default level is INFORMATIONAL (unlike expect's default of ERROR) +- Map Kind based on severity: ERROR/CRITICAL → FAIL, WARNING → PASS, others → INFORMATIONAL + +#### FR-5: Visitor Pattern Support +Update `IConstraintVisitor` to: +- Include `visitReportConstraint()` method +- Update all visitor implementations + +### 3.2 Non-Functional Requirements + +#### NFR-1: API Consistency +The new constraint types must follow established patterns for naming, structure, and builder usage consistent with existing constraints like `IExpectConstraint`. + +#### NFR-2: Test Coverage +All new code must have unit tests following TDD practices with minimum 80% coverage. + +#### NFR-3: Documentation +All public interfaces and methods must have complete Javadoc per project standards. + +--- + +## 4. Implementation Phases + +This is a single-phase implementation as all components are interdependent. + +### Phase 1: Complete TargetedReportConstraint Support + +Implement all components in a single cohesive PR: +1. Core interface and implementation +2. Constraint loading support +3. Validation integration +4. Comprehensive unit tests + +See [Implementation Plan](./implementation-plan.md) for detailed breakdown. + +--- + +## 5. Testing Strategy + +### 5.1 Test Approach + +All development follows TDD: +1. Write failing tests for each new component +2. Implement the component to pass tests +3. Refactor while maintaining green tests + +### 5.2 Verification Checklist + +- [ ] `IReportConstraint` interface created with appropriate methods +- [ ] `DefaultReportConstraint` implementation with builder +- [ ] `ConstraintBindingSupport.parse()` handles `TargetedReportConstraint` +- [ ] `newReport()` factory method creates valid constraints +- [ ] Report constraints generate findings when test is TRUE +- [ ] Report constraints default to INFORMATIONAL level +- [ ] Report constraints at ERROR/CRITICAL level cause validation failures (Kind.FAIL) +- [ ] Visitor pattern updated with `visitReportConstraint()` +- [ ] All new code has Javadoc +- [ ] All unit tests pass +- [ ] Build succeeds with `mvn clean install -PCI -Prelease` + +--- + +## 6. Risks and Mitigations + +| Risk | Impact | Likelihood | Mitigation | +|------|--------|------------|------------| +| Report semantics differ from expect | Medium | Low | Clear documentation, separate validation method | +| Breaking existing constraint handling | High | Low | Comprehensive test coverage, incremental changes | +| Missing visitor implementations | Medium | Medium | Search for all IConstraintVisitor implementations | + +--- + +## 7. Design Decisions + +### DD-1: Report Constraint Severity and Kind Mapping + +**Decision:** Report constraints support all severity levels with INFORMATIONAL as the default. + +| Configured Level | Kind when test=TRUE | +|------------------|---------------------| +| INFORMATIONAL (default) | INFORMATIONAL | +| DEBUG | INFORMATIONAL | +| WARNING | INFORMATIONAL | +| ERROR | FAIL | +| CRITICAL | FAIL | + +**Rationale:** This allows authors to use report constraints for both informational messages and hard errors when detecting problematic patterns. + +### DD-2: Semantic Distinction from Expect + +**Decision:** Report and expect are opposite assertions: + +| Constraint | Meaning | Generates finding when... | +|------------|---------|---------------------------| +| Expect | "This MUST be true" | Test = FALSE | +| Report | "This MUST NOT be true" | Test = TRUE | + +Both can cause validation failures at ERROR/CRITICAL level. + +### DD-3: Finding Differentiation + +**Decision:** No special field needed to distinguish report from expect findings. + +Consumers can use `instanceof IReportConstraint` vs `instanceof IExpectConstraint` on the finding's constraint object if differentiation is needed. This follows the existing pattern for other constraint types. + +--- + +## 8. Related Documents + +- [Implementation Plan](./implementation-plan.md) +- [PR #589 - Metapath-based targeting for binding configurations](https://github.com/metaschema-framework/metaschema-java/pull/589) +- [Issue #592 - Add constraint processing support for TargetedReportConstraint](https://github.com/metaschema-framework/metaschema-java/issues/592) diff --git a/PRDs/20251228-targeted-report-constraint/implementation-plan.md b/PRDs/20251228-targeted-report-constraint/implementation-plan.md new file mode 100644 index 000000000..04e923745 --- /dev/null +++ b/PRDs/20251228-targeted-report-constraint/implementation-plan.md @@ -0,0 +1,242 @@ +# Implementation Plan: TargetedReportConstraint Support + +This document details the implementation for adding constraint processing support for `TargetedReportConstraint`. + +--- + +## Prerequisites + +- Build the project to ensure all generated sources exist: `mvn install -DskipTests` +- Understand the difference between report and expect semantics (see PRD) + +--- + +## Test-Driven Development Requirement + +**All functional code changes must follow TDD:** + +1. Write or update tests first to capture expected behavior +2. Verify tests fail with existing implementation +3. Make the code changes +4. Verify tests pass after changes + +--- + +## Phase 1: Complete TargetedReportConstraint Support + +### PR 1: Add IReportConstraint Interface and Implementation + +| Attribute | Value | +|-----------|-------| +| **Files Changed** | ~15 | +| **Risk Level** | Medium | +| **Dependencies** | None | +| **Target Branch** | develop | +| **Status** | Pending | +| **Pull Request** | TBD | + +#### Files to Create + +| File | Purpose | +|------|---------| +| `core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/IReportConstraint.java` | Report constraint interface with `getTest()` method | +| `core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/impl/DefaultReportConstraint.java` | Default implementation with builder | +| `core/src/test/java/gov/nist/secauto/metaschema/core/model/constraint/ReportConstraintTest.java` | Unit tests for report constraint | + +#### Files to Modify + +| File | Changes | +|------|---------| +| `core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/IConstraintVisitor.java` | Add `visitReportConstraint()` method | +| `core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/IValueConstrained.java` | Add `getReportConstraints()` and `addConstraint(IReportConstraint)` methods | +| `core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/IFeatureValueConstrained.java` | Add delegation for report constraints | +| `core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/ValueConstraintSet.java` | Add storage and retrieval for report constraints | +| `core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/AbstractTargetedConstraints.java` | Add `getReportConstraints()` forwarding in `applyTo()` | +| `core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/DefaultConstraintValidator.java` | Add `validateReport()` method and integrate into validation flow | +| `databind/src/main/java/gov/nist/secauto/metaschema/databind/model/metaschema/impl/ConstraintBindingSupport.java` | Add `TargetedReportConstraint` handling to `parse()` methods and `newReport()` factory | +| `databind/src/test/java/gov/nist/secauto/metaschema/databind/model/metaschema/BindingConstraintLoaderTest.java` | Add test for loading TargetedReportConstraint | + +#### Implementation Approach + +##### Step 1: Create Core Interface (TDD) + +1. **Write test first** - Create `ReportConstraintTest.java` with tests for: + - Builder creates valid constraint + - Test expression is retrievable + - Constraint properties (id, level, message) are accessible + - Visitor pattern works correctly + +2. **Watch tests fail** - Verify compilation errors due to missing classes + +3. **Create `IReportConstraint.java`** modeled after `IExpectConstraint`: + ```java + public interface IReportConstraint extends IConfigurableMessageConstraint { + @NonNull + IMetapathExpression getTest(); + + @NonNull + static Builder builder() { + return new Builder(); + } + + final class Builder extends AbstractConfigurableMessageConstraintBuilder { + // Builder implementation + } + } + ``` + +4. **Create `DefaultReportConstraint.java`** in `impl/` package + +5. **Run tests** - Verify they pass + +##### Step 2: Update Constraint Interfaces (TDD) + +1. **Write tests** for constraint storage/retrieval in `ValueConstraintSet` + +2. **Update `IValueConstrained.java`**: + - Add `List getReportConstraints()` + - Add `void addConstraint(@NonNull IReportConstraint constraint)` + +3. **Update `IFeatureValueConstrained.java`**: + - Add delegation methods for report constraints + +4. **Update `ValueConstraintSet.java`**: + - Add `List reportConstraints` field + - Implement getter and add methods + +5. **Update `AbstractTargetedConstraints.java`**: + - Add `getReportConstraints().forEach(definition::addConstraint)` in `applyTo()` + +##### Step 3: Update Visitor Pattern + +1. **Update `IConstraintVisitor.java`**: + ```java + R visitReportConstraint(@NonNull IReportConstraint constraint, T state); + ``` + +2. **Update `IReportConstraint.java`** to implement `accept()`: + ```java + @Override + default R accept(IConstraintVisitor visitor, T state) { + return visitor.visitReportConstraint(this, state); + } + ``` + +##### Step 4: Update Constraint Loader (TDD) + +1. **Write test** in `BindingConstraintLoaderTest.java` for loading `TargetedReportConstraint` + +2. **Update `ConstraintBindingSupport.java`**: + - Add import for `TargetedReportConstraint` + - Add case for `TargetedReportConstraint` in `parse(IValueConstrained, ...)` method + - Add case for `TargetedReportConstraint` in `parse(IValueTargetedConstraintsBase, ...)` method + - Add case for `TargetedReportConstraint` in `parse(IModelConstrained, ...)` method + - Add factory method: + ```java + @NonNull + private static IReportConstraint newReport( + @NonNull TargetedReportConstraint obj, + @NonNull ISource source) { + // Build constraint from binding object + } + ``` + +##### Step 5: Update Validation Pipeline (TDD) + +1. **Write test** for report constraint validation in `DefaultConstraintValidatorTest.java` + +2. **Update `DefaultConstraintValidator.java`**: + - Add `validateReport()` method similar to `validateExpect()` but: + - Generate finding when test is TRUE (opposite of expect) + - Use constraint's configured level (default: INFORMATIONAL) + - Kind mapping: ERROR/CRITICAL → FAIL, all others → INFORMATIONAL + - Call `validateReport()` in `validateFlag()`, `validateField()`, `validateAssembly()` + +##### Step 6: Final Verification + +1. Run full test suite: `mvn test` +2. Run full build with checks: `mvn clean install -PCI -Prelease` +3. Verify all tests pass and no new warnings + +#### Acceptance Criteria + +- [ ] `IReportConstraint` interface created extending `IConfigurableMessageConstraint` +- [ ] `IReportConstraint.getTest()` returns the test Metapath expression +- [ ] `DefaultReportConstraint` implementation with working builder +- [ ] `IConstraintVisitor.visitReportConstraint()` method added +- [ ] `IValueConstrained` has `getReportConstraints()` and `addConstraint(IReportConstraint)` +- [ ] `ValueConstraintSet` stores and retrieves report constraints +- [ ] `AbstractTargetedConstraints.applyTo()` forwards report constraints +- [ ] `ConstraintBindingSupport.parse()` handles `TargetedReportConstraint` in all overloads +- [ ] `ConstraintBindingSupport.newReport()` factory method creates valid constraints +- [ ] `DefaultConstraintValidator.validateReport()` generates findings when test is TRUE +- [ ] Report constraints default to INFORMATIONAL level +- [ ] Report constraints at ERROR/CRITICAL cause validation failures (Kind.FAIL) +- [ ] Unit tests for `IReportConstraint` builder and behavior +- [ ] Unit tests for constraint loading +- [ ] Unit tests for constraint validation +- [ ] All new code has complete Javadoc +- [ ] All tests pass: `mvn test` +- [ ] Build succeeds: `mvn clean install -PCI -Prelease` + +--- + +## Key Design Decisions + +### Report vs Expect Semantics + +| Aspect | Expect Constraint | Report Constraint | +|--------|-------------------|-------------------| +| Meaning | "This MUST be true" | "This MUST NOT be true" | +| Generates finding when | Test = FALSE | Test = TRUE | +| Default level | ERROR | INFORMATIONAL | +| Kind at ERROR/CRITICAL | FAIL | FAIL | +| Kind at WARNING and below | Based on level | INFORMATIONAL | + +Both expect and report can cause validation failures when configured at ERROR or CRITICAL level. + +### Kind Mapping for Report Constraints + +| Configured Level | Kind | +|------------------|------| +| INFORMATIONAL (default) | INFORMATIONAL | +| DEBUG | INFORMATIONAL | +| WARNING | INFORMATIONAL | +| ERROR | FAIL | +| CRITICAL | FAIL | + +### Interface Hierarchy + +```text +IConstraint +└── IConfigurableMessageConstraint + ├── IExpectConstraint (existing) + └── IReportConstraint (new - follows same pattern) +``` + +--- + +## PR Summary Table + +| PR | Description | Files | Risk | Dependencies | Status | +|----|-------------|-------|------|--------------|--------| +| 1 | Add IReportConstraint interface and full implementation | ~15 | Medium | None | Pending | + +**Total Estimated PRs**: 1 +**Total Estimated Files**: ~15 + +--- + +## Verification Commands + +```bash +# Run all tests +mvn test + +# Run specific test classes +mvn -pl core test -Dtest=ReportConstraintTest +mvn -pl databind test -Dtest=BindingConstraintLoaderTest + +# Full CI build +mvn clean install -PCI -Prelease +``` From 137be63701d510924977e372f03b7243b481f968 Mon Sep 17 00:00:00 2001 From: David Waltermire Date: Sun, 28 Dec 2025 17:35:04 -0500 Subject: [PATCH 2/9] docs: add skill documentation updates to PRD Add FR-6 requiring updates to Claude Code skills: - metaschema-constraints-authoring.md - document report constraint - metaschema-java-library.md - document IReportConstraint interface Update implementation plan with Step 6 for skill documentation. --- .../PRD.md | 7 ++++++ .../implementation-plan.md | 22 ++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/PRDs/20251228-targeted-report-constraint/PRD.md b/PRDs/20251228-targeted-report-constraint/PRD.md index 65a646f35..a506a45bb 100644 --- a/PRDs/20251228-targeted-report-constraint/PRD.md +++ b/PRDs/20251228-targeted-report-constraint/PRD.md @@ -132,6 +132,11 @@ Update `IConstraintVisitor` to: - Include `visitReportConstraint()` method - Update all visitor implementations +#### FR-6: Skill Documentation Updates +Update Claude Code skills to document the new constraint type: +- `.claude/skills/metaschema-constraints-authoring.md` - Add `report` constraint type with syntax and examples +- `.claude/skills/metaschema-java-library.md` - Add `IReportConstraint` interface if constraint interfaces are documented + ### 3.2 Non-Functional Requirements #### NFR-1: API Consistency @@ -180,6 +185,8 @@ All development follows TDD: - [ ] Report constraints default to INFORMATIONAL level - [ ] Report constraints at ERROR/CRITICAL level cause validation failures (Kind.FAIL) - [ ] Visitor pattern updated with `visitReportConstraint()` +- [ ] `metaschema-constraints-authoring.md` skill updated with `report` constraint +- [ ] `metaschema-java-library.md` skill updated if applicable - [ ] All new code has Javadoc - [ ] All unit tests pass - [ ] Build succeeds with `mvn clean install -PCI -Prelease` diff --git a/PRDs/20251228-targeted-report-constraint/implementation-plan.md b/PRDs/20251228-targeted-report-constraint/implementation-plan.md index 04e923745..3390a2b77 100644 --- a/PRDs/20251228-targeted-report-constraint/implementation-plan.md +++ b/PRDs/20251228-targeted-report-constraint/implementation-plan.md @@ -55,6 +55,8 @@ This document details the implementation for adding constraint processing suppor | `core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/DefaultConstraintValidator.java` | Add `validateReport()` method and integrate into validation flow | | `databind/src/main/java/gov/nist/secauto/metaschema/databind/model/metaschema/impl/ConstraintBindingSupport.java` | Add `TargetedReportConstraint` handling to `parse()` methods and `newReport()` factory | | `databind/src/test/java/gov/nist/secauto/metaschema/databind/model/metaschema/BindingConstraintLoaderTest.java` | Add test for loading TargetedReportConstraint | +| `.claude/skills/metaschema-constraints-authoring.md` | Add `report` constraint type documentation | +| `.claude/skills/metaschema-java-library.md` | Add `IReportConstraint` interface (if applicable) | #### Implementation Approach @@ -152,7 +154,23 @@ This document details the implementation for adding constraint processing suppor - Kind mapping: ERROR/CRITICAL → FAIL, all others → INFORMATIONAL - Call `validateReport()` in `validateFlag()`, `validateField()`, `validateAssembly()` -##### Step 6: Final Verification +##### Step 6: Update Skill Documentation + +Update Claude Code skills to document the new constraint type: + +1. **Update `.claude/skills/metaschema-constraints-authoring.md`**: + - Add `report` to Constraint Types Overview table + - Add new section for `report` constraint with: + - Purpose: Report/fail when Metapath condition is TRUE (opposite of expect) + - Attributes: `test`, `target`, `message`, `level` (default: INFORMATIONAL) + - YAML and XML syntax examples + - Semantic distinction from `expect` + +2. **Update `.claude/skills/metaschema-java-library.md`** (if constraint interfaces are documented): + - Add `IReportConstraint` interface + - Document relationship to `IExpectConstraint` + +##### Step 7: Final Verification 1. Run full test suite: `mvn test` 2. Run full build with checks: `mvn clean install -PCI -Prelease` @@ -172,6 +190,8 @@ This document details the implementation for adding constraint processing suppor - [ ] `DefaultConstraintValidator.validateReport()` generates findings when test is TRUE - [ ] Report constraints default to INFORMATIONAL level - [ ] Report constraints at ERROR/CRITICAL cause validation failures (Kind.FAIL) +- [ ] `metaschema-constraints-authoring.md` skill updated with `report` constraint +- [ ] `metaschema-java-library.md` skill updated if applicable - [ ] Unit tests for `IReportConstraint` builder and behavior - [ ] Unit tests for constraint loading - [ ] Unit tests for constraint validation From abfe492418d6165c25da568dad43ee33ab573a09 Mon Sep 17 00:00:00 2001 From: David Waltermire Date: Sun, 28 Dec 2025 18:14:31 -0500 Subject: [PATCH 3/9] docs: address PR review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Clarify WARNING → INFORMATIONAL rationale in design decisions - Note that no IConstraintVisitor implementations exist (lower risk) - Enumerate specific validation entry points (validateFlag, validateField, validateAssembly) - Code snippets already have @NonNull annotations --- PRDs/20251228-targeted-report-constraint/PRD.md | 4 ++-- .../implementation-plan.md | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/PRDs/20251228-targeted-report-constraint/PRD.md b/PRDs/20251228-targeted-report-constraint/PRD.md index a506a45bb..ada6952e6 100644 --- a/PRDs/20251228-targeted-report-constraint/PRD.md +++ b/PRDs/20251228-targeted-report-constraint/PRD.md @@ -199,7 +199,7 @@ All development follows TDD: |------|--------|------------|------------| | Report semantics differ from expect | Medium | Low | Clear documentation, separate validation method | | Breaking existing constraint handling | High | Low | Comprehensive test coverage, incremental changes | -| Missing visitor implementations | Medium | Medium | Search for all IConstraintVisitor implementations | +| Missing visitor implementations | Low | Low | No current implementations exist; only interface needs update | --- @@ -217,7 +217,7 @@ All development follows TDD: | ERROR | FAIL | | CRITICAL | FAIL | -**Rationale:** This allows authors to use report constraints for both informational messages and hard errors when detecting problematic patterns. +**Rationale:** Report constraints are fundamentally about "reporting" when a condition is detected. At WARNING level and below, findings remain informational since no validation failure should occur. Only ERROR/CRITICAL elevate to FAIL, indicating the detected condition is serious enough to fail validation. This differs from expect constraints where WARNING produces Kind.PASS (a successful but cautionary result). ### DD-2: Semantic Distinction from Expect diff --git a/PRDs/20251228-targeted-report-constraint/implementation-plan.md b/PRDs/20251228-targeted-report-constraint/implementation-plan.md index 3390a2b77..f7fd2a1e7 100644 --- a/PRDs/20251228-targeted-report-constraint/implementation-plan.md +++ b/PRDs/20251228-targeted-report-constraint/implementation-plan.md @@ -124,6 +124,8 @@ This document details the implementation for adding constraint processing suppor } ``` + **Note:** There are currently no implementations of `IConstraintVisitor` in the codebase. The interface is prepared for future visitor pattern usage. Only the interface itself needs the new method. + ##### Step 4: Update Constraint Loader (TDD) 1. **Write test** in `BindingConstraintLoaderTest.java` for loading `TargetedReportConstraint` @@ -152,7 +154,10 @@ This document details the implementation for adding constraint processing suppor - Generate finding when test is TRUE (opposite of expect) - Use constraint's configured level (default: INFORMATIONAL) - Kind mapping: ERROR/CRITICAL → FAIL, all others → INFORMATIONAL - - Call `validateReport()` in `validateFlag()`, `validateField()`, `validateAssembly()` + - Add `validateReport()` call to all three validation entry points: + - `validateFlag()` (line ~190) - add after other constraint validations + - `validateField()` (line ~217) - add after other constraint validations + - `validateAssembly()` (line ~243) - add after other constraint validations ##### Step 6: Update Skill Documentation From 4354b16278300238c394cd4833d23a78167ea549 Mon Sep 17 00:00:00 2001 From: David Waltermire Date: Sun, 28 Dec 2025 18:25:03 -0500 Subject: [PATCH 4/9] docs: fix KIND mapping inconsistency in FR-4 --- PRDs/20251228-targeted-report-constraint/PRD.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PRDs/20251228-targeted-report-constraint/PRD.md b/PRDs/20251228-targeted-report-constraint/PRD.md index ada6952e6..9e510e8a2 100644 --- a/PRDs/20251228-targeted-report-constraint/PRD.md +++ b/PRDs/20251228-targeted-report-constraint/PRD.md @@ -125,7 +125,7 @@ Update the validation system to: - Call report constraint validation at appropriate points - Generate findings when report test expressions evaluate to TRUE - Default level is INFORMATIONAL (unlike expect's default of ERROR) -- Map Kind based on severity: ERROR/CRITICAL → FAIL, WARNING → PASS, others → INFORMATIONAL +- Map Kind based on severity: ERROR/CRITICAL → FAIL, all other levels → INFORMATIONAL #### FR-5: Visitor Pattern Support Update `IConstraintVisitor` to: From 2eaa8d253915a4094c6c10532235d51372bc2b8e Mon Sep 17 00:00:00 2001 From: David Waltermire Date: Sun, 28 Dec 2025 19:02:35 -0500 Subject: [PATCH 5/9] feat(core): add IReportConstraint interface and implementation Add report constraint support as the inverse of expect constraints: - IReportConstraint interface extending IConfigurableMessageConstraint - DefaultReportConstraint implementation with builder - Default level is INFORMATIONAL (not ERROR like expect) - Add REPORT type to IConstraint.Type enum - Add visitReportConstraint() to IConstraintVisitor Report constraints generate findings when test is TRUE, semantic opposite of expect constraints which generate findings when FALSE. Implements part of #592 --- .../core/model/constraint/IConstraint.java | 4 +- .../model/constraint/IConstraintVisitor.java | 12 + .../model/constraint/IReportConstraint.java | 146 +++++++++++ .../impl/DefaultReportConstraint.java | 92 +++++++ .../constraint/ReportConstraintTest.java | 229 ++++++++++++++++++ 5 files changed, 482 insertions(+), 1 deletion(-) create mode 100644 core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/IReportConstraint.java create mode 100644 core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/impl/DefaultReportConstraint.java create mode 100644 core/src/test/java/gov/nist/secauto/metaschema/core/model/constraint/ReportConstraintTest.java diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/IConstraint.java b/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/IConstraint.java index 0f370ce26..2367ac1ae 100644 --- a/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/IConstraint.java +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/IConstraint.java @@ -41,7 +41,9 @@ enum Type { /** Constraint verifying index key references. */ INDEX_HAS_KEY("index-has-key"), /** Constraint validating pattern matching. */ - MATCHES("matches"); + MATCHES("matches"), + /** Constraint reporting a condition. */ + REPORT("report"); @NonNull private final String name; diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/IConstraintVisitor.java b/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/IConstraintVisitor.java index 9f42e371f..52dc113b9 100644 --- a/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/IConstraintVisitor.java +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/IConstraintVisitor.java @@ -101,4 +101,16 @@ public interface IConstraintVisitor { * @return the visitation result */ R visitUniqueConstraint(@NonNull IUniqueConstraint constraint, T state); + + /** + * Implementation of this method support visitation of an + * {@link IReportConstraint}. + * + * @param constraint + * the constraint to visit + * @param state + * a state object passed to the visitor + * @return the visitation result + */ + R visitReportConstraint(@NonNull IReportConstraint constraint, T state); } diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/IReportConstraint.java b/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/IReportConstraint.java new file mode 100644 index 000000000..c5a661142 --- /dev/null +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/IReportConstraint.java @@ -0,0 +1,146 @@ +/* + * SPDX-FileCopyrightText: none + * SPDX-License-Identifier: CC0-1.0 + */ + +package gov.nist.secauto.metaschema.core.model.constraint; + +import gov.nist.secauto.metaschema.core.metapath.IMetapathExpression; +import gov.nist.secauto.metaschema.core.model.constraint.impl.DefaultReportConstraint; +import gov.nist.secauto.metaschema.core.util.ObjectUtils; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Represents a rule reporting a condition when a Metaschema assembly, field, or + * flag data instance matches a Metapath-based test. + *

+ * Unlike {@link IExpectConstraint} which generates a finding when the test is + * FALSE, a report constraint generates a finding when the test is TRUE. This is + * useful for: + *

    + *
  • Reporting deprecated usage patterns
  • + *
  • Flagging known issues or limitations
  • + *
  • Providing informational messages about content
  • + *
+ *

+ * A custom message can be used to indicate what a matching condition signifies. + * The default severity level is {@link Level#INFORMATIONAL}. + * + * @since 2.0.0 + */ +public interface IReportConstraint extends IConfigurableMessageConstraint { + /** + * The default severity level for report constraints. + */ + @NonNull + Level DEFAULT_LEVEL = Level.INFORMATIONAL; + + @Override + default Type getType() { + return Type.REPORT; + } + + /** + * Get the test to use to identify reportable conditions in selected nodes. + *

+ * A finding is generated when this test evaluates to {@code true}. + * + * @return the test metapath expression to use + */ + @NonNull + IMetapathExpression getTest(); + + @Override + default R accept(IConstraintVisitor visitor, T state) { + return visitor.visitReportConstraint(this, state); + } + + /** + * Create a new constraint builder. + * + * @return the builder + */ + @NonNull + static Builder builder() { + return new Builder(); + } + + /** + * Provides a builder pattern for constructing a new {@link IReportConstraint}. + */ + final class Builder + extends AbstractConfigurableMessageConstraintBuilder { + private IMetapathExpression test; + private boolean levelSet; + + private Builder() { + // disable construction + } + + /** + * Use the provided test to identify reportable conditions in selected nodes. + *

+ * A finding is generated when this test evaluates to {@code true}. + * + * @param test + * the test metapath expression to use + * @return this builder + */ + @NonNull + public Builder test(@NonNull IMetapathExpression test) { + this.test = test; + return this; + } + + @Override + protected Builder getThis() { + return this; + } + + /** + * {@inheritDoc} + *

+ * For report constraints, the default level is {@link Level#INFORMATIONAL} if + * no level is explicitly set. + */ + @Override + @NonNull + public Builder level(@NonNull Level level) { + this.levelSet = true; + return super.level(level); + } + + @Override + @NonNull + protected Level getLevel() { + return levelSet ? super.getLevel() : DEFAULT_LEVEL; + } + + @Override + protected void validate() { + super.validate(); + + ObjectUtils.requireNonNull(getTest()); + } + + private IMetapathExpression getTest() { + return test; + } + + @Override + protected IReportConstraint newInstance() { + return new DefaultReportConstraint( + getId(), + getFormalName(), + getDescription(), + ObjectUtils.notNull(getSource()), + getLevel(), + getTarget(), + getProperties(), + ObjectUtils.requireNonNull(getTest()), + getMessage(), + getRemarks()); + } + } +} diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/impl/DefaultReportConstraint.java b/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/impl/DefaultReportConstraint.java new file mode 100644 index 000000000..610ceb435 --- /dev/null +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/impl/DefaultReportConstraint.java @@ -0,0 +1,92 @@ +/* + * SPDX-FileCopyrightText: none + * SPDX-License-Identifier: CC0-1.0 + */ + +package gov.nist.secauto.metaschema.core.model.constraint.impl; + +import gov.nist.secauto.metaschema.core.datatype.markup.MarkupLine; +import gov.nist.secauto.metaschema.core.datatype.markup.MarkupMultiline; +import gov.nist.secauto.metaschema.core.metapath.IMetapathExpression; +import gov.nist.secauto.metaschema.core.metapath.item.atomic.IBooleanItem; +import gov.nist.secauto.metaschema.core.model.IAttributable; +import gov.nist.secauto.metaschema.core.model.ISource; +import gov.nist.secauto.metaschema.core.model.constraint.IReportConstraint; + +import java.util.Map; +import java.util.Set; + +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +/** + * Represents a report constraint. + *

+ * A report constraint generates a finding when the associated test evaluates to + * {@link IBooleanItem#TRUE} against the target. This is the opposite behavior + * of an expect constraint, which generates a finding when the test evaluates to + * {@code FALSE}. + *

+ * Report constraints are useful for: + *

    + *
  • Flagging deprecated patterns or values
  • + *
  • Reporting known issues or limitations
  • + *
  • Providing informational messages about content characteristics
  • + *
+ * + * @since 2.0.0 + */ +public final class DefaultReportConstraint + extends AbstractConfigurableMessageConstraint + implements IReportConstraint { + @NonNull + private final IMetapathExpression test; + + /** + * Construct a new report constraint. + * + * @param id + * the optional identifier for the constraint + * @param formalName + * the constraint's formal name or {@code null} if not provided + * @param description + * the constraint's semantic description or {@code null} if not + * provided + * @param source + * information about the constraint source + * @param level + * the significance of a violation of this constraint + * @param target + * the Metapath expression identifying the nodes the constraint targets + * @param properties + * a collection of associated properties + * @param test + * a Metapath expression that is evaluated against the target node to + * determine if a condition should be reported; a finding is generated + * when this evaluates to {@code true} + * @param message + * an optional message to emit when the constraint condition is matched + * @param remarks + * optional remarks describing the intent of the constraint + */ + @SuppressWarnings("PMD.ExcessiveParameterList") + public DefaultReportConstraint( + @Nullable String id, + @Nullable String formalName, + @Nullable MarkupLine description, + @NonNull ISource source, + @NonNull Level level, + @NonNull IMetapathExpression target, + @NonNull Map> properties, + @NonNull IMetapathExpression test, + @Nullable String message, + @Nullable MarkupMultiline remarks) { + super(id, formalName, description, source, level, target, properties, message, remarks); + this.test = test; + } + + @Override + public IMetapathExpression getTest() { + return test; + } +} diff --git a/core/src/test/java/gov/nist/secauto/metaschema/core/model/constraint/ReportConstraintTest.java b/core/src/test/java/gov/nist/secauto/metaschema/core/model/constraint/ReportConstraintTest.java new file mode 100644 index 000000000..076982c39 --- /dev/null +++ b/core/src/test/java/gov/nist/secauto/metaschema/core/model/constraint/ReportConstraintTest.java @@ -0,0 +1,229 @@ +/* + * SPDX-FileCopyrightText: none + * SPDX-License-Identifier: CC0-1.0 + */ + +package gov.nist.secauto.metaschema.core.model.constraint; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import gov.nist.secauto.metaschema.core.metapath.IMetapathExpression; +import gov.nist.secauto.metaschema.core.model.ISource; +import gov.nist.secauto.metaschema.core.model.constraint.IConstraint.Level; +import gov.nist.secauto.metaschema.core.model.constraint.IConstraint.Type; + +import org.junit.jupiter.api.Test; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Unit tests for the {@link IReportConstraint} interface and its builder. + *

+ * These tests verify: + *

    + *
  • Builder creates valid constraint
  • + *
  • Test expression is retrievable via getTest()
  • + *
  • Constraint properties (id, level, message) are accessible
  • + *
  • Visitor pattern works correctly (visitReportConstraint)
  • + *
  • Default level is INFORMATIONAL
  • + *
+ */ +class ReportConstraintTest { + @NonNull + private static final String TEST_SOURCE = "https://example.com/test"; + + /** + * Test that the builder creates a valid constraint with test expression. + */ + @Test + void testBuilderCreatesValidConstraint() { + ISource source = ISource.externalSource(TEST_SOURCE); + IMetapathExpression test = IMetapathExpression.compile("string-length(.) > 100"); + + IReportConstraint constraint = IReportConstraint.builder() + .source(source) + .test(test) + .build(); + + assertNotNull(constraint, "Constraint should not be null"); + } + + /** + * Test that getTest() returns the test Metapath expression. + */ + @Test + void testGetTestReturnsExpression() { + ISource source = ISource.externalSource(TEST_SOURCE); + IMetapathExpression test = IMetapathExpression.compile("contains(., 'deprecated')"); + + IReportConstraint constraint = IReportConstraint.builder() + .source(source) + .test(test) + .build(); + + assertSame(test, constraint.getTest(), "getTest() should return the same expression"); + } + + /** + * Test that constraint properties (id, level, message) are accessible. + */ + @Test + void testConstraintPropertiesAreAccessible() { + ISource source = ISource.externalSource(TEST_SOURCE); + IMetapathExpression test = IMetapathExpression.compile(". = 'deprecated'"); + String constraintId = "report-001"; + String constraintMessage = "This value is deprecated"; + Level constraintLevel = Level.WARNING; + + IReportConstraint constraint = IReportConstraint.builder() + .source(source) + .test(test) + .identifier(constraintId) + .message(constraintMessage) + .level(constraintLevel) + .build(); + + assertEquals(constraintId, constraint.getId(), "getId() should return the constraint id"); + assertEquals(constraintMessage, constraint.getMessage(), "getMessage() should return the message"); + assertEquals(constraintLevel, constraint.getLevel(), "getLevel() should return the configured level"); + } + + /** + * Test that the visitor pattern works correctly with visitReportConstraint. + */ + @Test + void testVisitorPatternWorksCorrectly() { + ISource source = ISource.externalSource(TEST_SOURCE); + IMetapathExpression test = IMetapathExpression.compile("true()"); + + IReportConstraint constraint = IReportConstraint.builder() + .source(source) + .test(test) + .build(); + + // Create a test visitor that tracks if visitReportConstraint was called + TestConstraintVisitor visitor = new TestConstraintVisitor(); + + Boolean result = constraint.accept(visitor, null); + + assertEquals(Boolean.TRUE, result, "Visitor should return true"); + assertEquals(1, visitor.getVisitReportCount(), + "visitReportConstraint should be called exactly once"); + } + + /** + * Test that the default level is INFORMATIONAL. + */ + @Test + void testDefaultLevelIsInformational() { + ISource source = ISource.externalSource(TEST_SOURCE); + IMetapathExpression test = IMetapathExpression.compile("true()"); + + IReportConstraint constraint = IReportConstraint.builder() + .source(source) + .test(test) + .build(); + + assertEquals(Level.INFORMATIONAL, constraint.getLevel(), + "Default level should be INFORMATIONAL"); + } + + /** + * Test that the constraint type is REPORT. + */ + @Test + void testConstraintTypeIsReport() { + ISource source = ISource.externalSource(TEST_SOURCE); + IMetapathExpression test = IMetapathExpression.compile("true()"); + + IReportConstraint constraint = IReportConstraint.builder() + .source(source) + .test(test) + .build(); + + assertEquals(Type.REPORT, constraint.getType(), "getType() should return REPORT"); + } + + /** + * Test that building without a test expression throws an exception. + */ + @Test + void testBuilderWithoutTestThrowsException() { + ISource source = ISource.externalSource(TEST_SOURCE); + + IReportConstraint.Builder builder = IReportConstraint.builder() + .source(source); + + assertThrows(NullPointerException.class, builder::build, + "Building without test should throw NullPointerException"); + } + + /** + * Test that building without a source throws an exception. + */ + @Test + void testBuilderWithoutSourceThrowsException() { + IMetapathExpression test = IMetapathExpression.compile("true()"); + + IReportConstraint.Builder builder = IReportConstraint.builder() + .test(test); + + assertThrows(NullPointerException.class, builder::build, + "Building without source should throw NullPointerException"); + } + + /** + * A test visitor implementation to verify the visitor pattern. + */ + private static final class TestConstraintVisitor implements IConstraintVisitor { + private int visitReportCount; + + int getVisitReportCount() { + return visitReportCount; + } + + @Override + public Boolean visitAllowedValues(@NonNull IAllowedValuesConstraint constraint, Void state) { + return Boolean.FALSE; + } + + @Override + public Boolean visitCardinalityConstraint(@NonNull ICardinalityConstraint constraint, Void state) { + return Boolean.FALSE; + } + + @Override + public Boolean visitExpectConstraint(@NonNull IExpectConstraint constraint, Void state) { + return Boolean.FALSE; + } + + @Override + public Boolean visitMatchesConstraint(@NonNull IMatchesConstraint constraint, Void state) { + return Boolean.FALSE; + } + + @Override + public Boolean visitIndexConstraint(@NonNull IIndexConstraint constraint, Void state) { + return Boolean.FALSE; + } + + @Override + public Boolean visitIndexHasKeyConstraint(@NonNull IIndexHasKeyConstraint constraint, Void state) { + return Boolean.FALSE; + } + + @Override + public Boolean visitUniqueConstraint(@NonNull IUniqueConstraint constraint, Void state) { + return Boolean.FALSE; + } + + @Override + public Boolean visitReportConstraint(@NonNull IReportConstraint constraint, Void state) { + visitReportCount++; + return Boolean.TRUE; + } + } +} From 1a31a300e087ea70ec9fb6aea0ff518c27380b16 Mon Sep 17 00:00:00 2001 From: David Waltermire Date: Sun, 28 Dec 2025 19:21:32 -0500 Subject: [PATCH 6/9] feat(core): add report constraint storage to constraint interfaces Update constraint interface hierarchy to support IReportConstraint: - IValueConstrained: add getReportConstraints() and addConstraint() - IFeatureValueConstrained: add delegation methods - ValueConstraintSet: add thread-safe storage and retrieval - AbstractTargetedConstraints: forward report constraints in applyTo() Implements part of #592 --- .../AbstractTargetedConstraints.java | 1 + .../constraint/IFeatureValueConstrained.java | 10 ++++++++ .../model/constraint/IValueConstrained.java | 16 ++++++++++++ .../model/constraint/ValueConstraintSet.java | 25 +++++++++++++++++++ 4 files changed, 52 insertions(+) diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/AbstractTargetedConstraints.java b/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/AbstractTargetedConstraints.java index 8725a2067..f0326cdd6 100644 --- a/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/AbstractTargetedConstraints.java +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/AbstractTargetedConstraints.java @@ -85,6 +85,7 @@ protected void applyTo(@NonNull IValueConstrained definition) { getMatchesConstraints().forEach(definition::addConstraint); getIndexHasKeyConstraints().forEach(definition::addConstraint); getExpectConstraints().forEach(definition::addConstraint); + getReportConstraints().forEach(definition::addConstraint); } @Override diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/IFeatureValueConstrained.java b/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/IFeatureValueConstrained.java index a8e802e1c..742e5398b 100644 --- a/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/IFeatureValueConstrained.java +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/IFeatureValueConstrained.java @@ -61,6 +61,11 @@ default List getExpectConstraints() { return getConstraintSupport().getExpectConstraints(); } + @Override + default List getReportConstraints() { + return getConstraintSupport().getReportConstraints(); + } + @Override default void addConstraint(IAllowedValuesConstraint constraint) { getConstraintSupport().addConstraint(constraint); @@ -80,4 +85,9 @@ default void addConstraint(IIndexHasKeyConstraint constraint) { default void addConstraint(@NonNull IExpectConstraint constraint) { getConstraintSupport().addConstraint(constraint); } + + @Override + default void addConstraint(@NonNull IReportConstraint constraint) { + getConstraintSupport().addConstraint(constraint); + } } diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/IValueConstrained.java b/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/IValueConstrained.java index b912003b1..1c8d7e48d 100644 --- a/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/IValueConstrained.java +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/IValueConstrained.java @@ -74,6 +74,14 @@ public interface IValueConstrained { @NonNull List getExpectConstraints(); + /** + * Get the collection of report constraints, if any. + * + * @return the constraints or an empty list + */ + @NonNull + List getReportConstraints(); + /** * Add a new let expression. * @@ -115,4 +123,12 @@ public interface IValueConstrained { * the constraint to add */ void addConstraint(@NonNull IExpectConstraint constraint); + + /** + * Add a new constraint. + * + * @param constraint + * the constraint to add + */ + void addConstraint(@NonNull IReportConstraint constraint); } diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/ValueConstraintSet.java b/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/ValueConstraintSet.java index b62b8ce23..b1d936b97 100644 --- a/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/ValueConstraintSet.java +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/ValueConstraintSet.java @@ -42,6 +42,8 @@ public class ValueConstraintSet implements IValueConstrained { private final List indexHasKeyConstraints = new LinkedList<>(); @NonNull private final List expectConstraints = new LinkedList<>(); + @NonNull + private final List reportConstraints = new LinkedList<>(); /** * The lock used to manage adjustments to the contents of this constraint set. */ @@ -155,6 +157,17 @@ public List getExpectConstraints() { } } + @Override + public List getReportConstraints() { + Lock readLock = instanceLock.readLock(); + readLock.lock(); + try { + return CollectionUtil.unmodifiableList(reportConstraints); + } finally { + readLock.unlock(); + } + } + @Override public final void addConstraint(@NonNull IAllowedValuesConstraint constraint) { Lock writeLock = instanceLock.writeLock(); @@ -202,4 +215,16 @@ public final void addConstraint(@NonNull IExpectConstraint constraint) { writeLock.unlock(); } } + + @Override + public final void addConstraint(@NonNull IReportConstraint constraint) { + Lock writeLock = instanceLock.writeLock(); + writeLock.lock(); + try { + constraints.add(constraint); + reportConstraints.add(constraint); + } finally { + writeLock.unlock(); + } + } } From 9b6216ad94c3e0f6b2d581f2b3562caadf2542d6 Mon Sep 17 00:00:00 2001 From: David Waltermire Date: Mon, 29 Dec 2025 00:57:22 -0500 Subject: [PATCH 7/9] feat(databind): add code generation and validation support for report constraints Complete the report constraint implementation with: - Add @Report annotation and update @ValueConstraints to include report array - Update AnnotationGenerator to emit @Report annotations in generated code - Add ConstraintFactory.newReportConstraint() for parsing @Report annotations - Update ConstraintSupport to parse report constraints from annotations - Update DefaultConstraintValidator to evaluate report constraints - Add IConstraintValidationHandler.handleReportResult() callback - Update validation handlers (Logging, FindingCollecting, Abstract) - Add comprehensive tests for report constraint lifecycle - Update skill documentation with report constraint usage The report constraint is the inverse of expect: it fires when the test expression evaluates to TRUE (condition detected) rather than FALSE (assertion failed). Typical use cases include deprecation warnings, informational notices, and audit findings. Closes #592 --- .../metaschema-constraints-authoring.md | 55 ++- .../core/model/MetaschemaModelConstants.java | 8 + .../AbstractConstraintValidationHandler.java | 34 ++ .../DefaultConstraintValidator.java | 83 ++++ ...CollectingConstraintValidationHandler.java | 15 + .../IConstraintValidationHandler.java | 27 + .../LoggingConstraintValidationHandler.java | 22 + .../constraint/ReportConstraintTest.java | 423 ++++++++++++++++ .../codegen/impl/AnnotationGenerator.java | 32 +- .../databind/model/annotations/Report.java | 114 +++++ .../model/annotations/ValueConstraints.java | 13 + .../model/impl/ConstraintFactory.java | 33 ++ .../model/impl/ConstraintSupport.java | 3 + .../binding/TargetedExpectConstraint.java | 4 +- .../TargetedHasCardinalityConstraint.java | 4 +- .../binding/TargetedIndexConstraint.java | 4 +- .../TargetedIndexHasKeyConstraint.java | 4 +- .../binding/TargetedIsUniqueConstraint.java | 4 +- .../binding/TargetedMatchesConstraint.java | 4 +- .../binding/TargetedReportConstraint.java | 4 +- .../impl/ConstraintBindingSupport.java | 31 ++ .../metaschema-model-bindings.xml | 7 + .../codegen/impl/AnnotationGeneratorTest.java | 2 + .../BindingConstraintLoaderTest.java | 463 ++++++++++++++++++ .../meta-constraints-report.yaml | 16 + 25 files changed, 1400 insertions(+), 9 deletions(-) create mode 100644 databind/src/main/java/gov/nist/secauto/metaschema/databind/model/annotations/Report.java create mode 100644 databind/src/test/resources/content/constraints/meta-constraints/meta-constraints-report.yaml diff --git a/.claude/skills/metaschema-constraints-authoring.md b/.claude/skills/metaschema-constraints-authoring.md index 4387ac187..6e74380f6 100644 --- a/.claude/skills/metaschema-constraints-authoring.md +++ b/.claude/skills/metaschema-constraints-authoring.md @@ -12,7 +12,8 @@ This skill guides the creation of Metaschema constraints for validating document | Constraint | Allowed On | Purpose | |------------|------------|---------| | `allowed-values` | flag, field, assembly | Restrict values to enumerated set | -| `expect` | flag, field, assembly | Assert Metapath condition is true | +| `expect` | flag, field, assembly | Assert Metapath condition is true (fires when FALSE) | +| `report` | flag, field, assembly | Report when Metapath condition is true (fires when TRUE) | | `matches` | flag, field, assembly | Validate against regex and/or datatype | | `has-cardinality` | assembly only | Enforce occurrence counts | | `index` | assembly only | Create named index with composite keys | @@ -148,6 +149,58 @@ constraint: ``` +### report + +Reports a finding when a Metapath expression evaluates to `true`. This is the inverse of `expect`: `expect` fires when the test is FALSE (assertion failed), while `report` fires when the test is TRUE (condition detected). + +**Use cases:** +- Deprecation warnings (element is deprecated) +- Informational notices (condition detected that user should know about) +- Style suggestions (valid but not preferred) +- Audit findings (specific pattern detected) + +**Attributes:** +- `test`: Required - Metapath boolean expression +- `target`: Required for fields/assemblies +- `message`: Optional - custom message with Metapath templates +- `level`: Typically `INFORMATIONAL` or `WARNING` (not `ERROR`) + +**YAML Example:** +```yaml +constraint: + rules: + - object-type: report + id: deprecated-element + level: WARNING + target: "." + test: "exists(@deprecated)" + message: "This element is deprecated. Consider using the replacement." + - object-type: report + id: large-collection + level: INFORMATIONAL + target: "./items" + test: "count(./item) > 100" + message: "Collection has { count(./item) } items, which may impact performance." +``` + +**XML Example:** +```xml + + + This element is deprecated. Consider using the replacement. + + + Collection has { count(./item) } items, which may impact performance. + + +``` + +**expect vs report:** +| Constraint | Fires When | Typical Level | Use Case | +|------------|------------|---------------|----------| +| `expect` | test is FALSE | ERROR | Validation failures | +| `report` | test is TRUE | WARNING/INFORMATIONAL | Detected conditions | + ### matches Validates values against datatype and/or regex patterns. diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/model/MetaschemaModelConstants.java b/core/src/main/java/gov/nist/secauto/metaschema/core/model/MetaschemaModelConstants.java index ffddb288a..fa325e4b7 100644 --- a/core/src/main/java/gov/nist/secauto/metaschema/core/model/MetaschemaModelConstants.java +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/model/MetaschemaModelConstants.java @@ -12,6 +12,7 @@ import gov.nist.secauto.metaschema.core.model.constraint.IIndexConstraint; import gov.nist.secauto.metaschema.core.model.constraint.IIndexHasKeyConstraint; import gov.nist.secauto.metaschema.core.model.constraint.IMatchesConstraint; +import gov.nist.secauto.metaschema.core.model.constraint.IReportConstraint; import gov.nist.secauto.metaschema.core.model.constraint.IUniqueConstraint; import gov.nist.secauto.metaschema.core.qname.IEnhancedQName; @@ -116,6 +117,13 @@ public final class MetaschemaModelConstants { public static final IEnhancedQName EXPECT_CONSTRAINT_QNAME = IEnhancedQName.of(MetaschemaConstants.METASCHEMA_NAMESPACE, "expect"); + /** + * The name of an {@link IReportConstraint} constraint in the Metaschema model. + */ + @NonNull + public static final IEnhancedQName REPORT_CONSTRAINT_QNAME + = IEnhancedQName.of(MetaschemaConstants.METASCHEMA_NAMESPACE, "report"); + /** * The name of an {@link IIndexConstraint} constraint in the Metaschema model. */ diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/AbstractConstraintValidationHandler.java b/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/AbstractConstraintValidationHandler.java index 9b4b1b521..198854f92 100644 --- a/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/AbstractConstraintValidationHandler.java +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/AbstractConstraintValidationHandler.java @@ -310,6 +310,40 @@ protected String newExpectViolationMessage( : constraint.generateMessage(target, dynamicContext); } + /** + * Construct a new message for the provided report {@code constraint} applied to + * the {@code node}. + *

+ * Report constraints generate findings when their test expression evaluates to + * {@code true}, which is the opposite of expect constraints. + * + * @param constraint + * the constraint the requested message pertains to + * @param node + * the item the constraint targeted + * @param target + * the target matching the constraint + * @param dynamicContext + * the Metapath dynamic execution context to use for Metapath + * evaluation + * @return the new message + * @throws ConstraintValidationException + * if the custom message contains a Metapath expression that is + * invalid or if the expression failed to evaluate + */ + @NonNull + protected String newReportViolationMessage( + @NonNull IReportConstraint constraint, + @NonNull INodeItem node, + @NonNull INodeItem target, + @NonNull DynamicContext dynamicContext) throws ConstraintValidationException { + return constraint.getMessage() == null + ? ObjectUtils.notNull(String.format("Report constraint '%s' matched the data at path '%s'", + constraint.getTest().getPath(), + toPath(target))) + : constraint.generateMessage(target, dynamicContext); + } + /** * Construct a new violation message for the provided {@code constraint} applied * to the {@code node}. diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/DefaultConstraintValidator.java b/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/DefaultConstraintValidator.java index 42d858faa..df46fcd10 100644 --- a/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/DefaultConstraintValidator.java +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/DefaultConstraintValidator.java @@ -188,6 +188,7 @@ protected void validateFlag( try { validateExpect(definition.getExpectConstraints(), item, dynamicContext); + validateReport(definition.getReportConstraints(), item, dynamicContext); validateAllowedValues(definition.getAllowedValuesConstraints(), item, dynamicContext); validateIndexHasKey(definition.getIndexHasKeyConstraints(), item, dynamicContext); validateMatches(definition.getMatchesConstraints(), item, dynamicContext); @@ -215,6 +216,7 @@ protected void validateField( try { validateExpect(definition.getExpectConstraints(), item, dynamicContext); + validateReport(definition.getReportConstraints(), item, dynamicContext); validateAllowedValues(definition.getAllowedValuesConstraints(), item, dynamicContext); validateIndexHasKey(definition.getIndexHasKeyConstraints(), item, dynamicContext); validateMatches(definition.getMatchesConstraints(), item, dynamicContext); @@ -241,6 +243,7 @@ protected void validateAssembly( try { validateExpect(definition.getExpectConstraints(), item, dynamicContext); + validateReport(definition.getReportConstraints(), item, dynamicContext); validateAllowedValues(definition.getAllowedValuesConstraints(), item, dynamicContext); validateIndexHasKey(definition.getIndexHasKeyConstraints(), item, dynamicContext); validateMatches(definition.getMatchesConstraints(), item, dynamicContext); @@ -770,6 +773,86 @@ private void validateExpect( } } + /** + * Evaluates the provided collection of report {@code constraints} in the + * context of the {@code item}. + *

+ * Report constraints generate findings when their test expression evaluates to + * {@code true}, which is the opposite of expect constraints. + * + * @param constraints + * the constraints to execute + * @param item + * the focus of Metapath evaluation + * @param dynamicContext + * the Metapath dynamic execution context to use for Metapath + * evaluation + * @throws ConstraintValidationException + * if an unexpected error occurred while validating a constraint + */ + private void validateReport( + @NonNull List constraints, + @NonNull IDefinitionNodeItem item, + @NonNull DynamicContext dynamicContext) throws ConstraintValidationException { + for (IReportConstraint constraint : constraints) { + assert constraint != null; + + try { + ISequence> targets = constraint.matchTargets(item, dynamicContext); + validateReport(constraint, item, targets, dynamicContext); + } catch (MetapathException ex) { + handleError(constraint, item, ex, dynamicContext); + } + } + } + + /** + * Evaluates the provided report {@code constraint} against each of the + * {@code targets}. + *

+ * Report constraints generate findings when their test expression evaluates to + * {@code true}, which is the opposite of expect constraints. + * + * @param constraint + * the constraint to execute + * @param node + * the original focus of Metapath evaluation for identifying the + * targets + * @param targets + * the focus of Metapath evaluation for evaluating any constraint + * Metapath clauses + * @param dynamicContext + * the Metapath dynamic execution context to use for Metapath + * evaluation + * @throws ConstraintValidationException + * if an unexpected error occurred while validating a constraint + */ + private void validateReport( + @NonNull IReportConstraint constraint, + @NonNull INodeItem node, + @NonNull ISequence targets, + @NonNull DynamicContext dynamicContext) throws ConstraintValidationException { + IMetapathExpression test = constraint.getTest(); + IConstraintValidationHandler handler = getConstraintValidationHandler(); + for (INodeItem item : targets) { + assert item != null; + + if (item.hasValue()) { + try { + ISequence result = test.evaluate(item, dynamicContext); + // Report constraints fire when test is TRUE (opposite of expect) + if (FnBoolean.fnBoolean(result).toBoolean()) { + handler.handleReportViolation(constraint, node, item, dynamicContext); + } else { + handlePass(constraint, node, item, dynamicContext); + } + } catch (MetapathException ex) { + handleError(constraint, item, ex, dynamicContext); + } + } + } + } + /** * Evaluates the provided collection of {@code constraints} in the context of * the {@code item}. diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/FindingCollectingConstraintValidationHandler.java b/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/FindingCollectingConstraintValidationHandler.java index 9ed7a5717..60eda4ec9 100644 --- a/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/FindingCollectingConstraintValidationHandler.java +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/FindingCollectingConstraintValidationHandler.java @@ -11,6 +11,7 @@ import gov.nist.secauto.metaschema.core.metapath.item.ISequence; import gov.nist.secauto.metaschema.core.metapath.item.node.INodeItem; import gov.nist.secauto.metaschema.core.model.constraint.IConstraint.Level; +import gov.nist.secauto.metaschema.core.model.constraint.IReportConstraint; import gov.nist.secauto.metaschema.core.model.validation.IValidationFinding.Kind; import gov.nist.secauto.metaschema.core.model.validation.IValidationResult; import gov.nist.secauto.metaschema.core.util.CollectionUtil; @@ -221,6 +222,20 @@ public void handleExpectViolation( .build()); } + @Override + public void handleReportViolation( + @NonNull IReportConstraint constraint, + @NonNull INodeItem node, + @NonNull INodeItem target, + @NonNull DynamicContext dynamicContext) throws ConstraintValidationException { + addFinding(ConstraintValidationFinding.builder(constraint, node) + .severity(constraint.getLevel()) + .kind(toKind(constraint.getLevel())) + .target(target) + .message(newReportViolationMessage(constraint, node, target, dynamicContext)) + .build()); + } + @Override public void handleAllowedValuesViolation( @NonNull List failedConstraints, diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/IConstraintValidationHandler.java b/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/IConstraintValidationHandler.java index d7b749826..65641c5f4 100644 --- a/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/IConstraintValidationHandler.java +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/IConstraintValidationHandler.java @@ -307,6 +307,33 @@ void handleExpectViolation( @NonNull INodeItem target, @NonNull DynamicContext dynamicContext) throws ConstraintValidationException; + /** + * Handle a report test finding. + *

+ * This happens when the report test expression evaluates to true. Unlike expect + * constraints which generate violations when false, report constraints generate + * findings when true. + * + * @param constraint + * the constraint that was evaluated + * @param node + * the node used as the evaluation focus to determine constraint + * targets + * @param target + * the target of evaluation + * @param dynamicContext + * the Metapath dynamic execution context to use for Metapath + * evaluation + * @throws ConstraintValidationException + * if the constraint has a custom message that contains a Metapath + * expression that is invalid or if the expression failed to evaluate + */ + void handleReportViolation( + @NonNull IReportConstraint constraint, + @NonNull INodeItem node, + @NonNull INodeItem target, + @NonNull DynamicContext dynamicContext) throws ConstraintValidationException; + /** * Handle an allowed values constraint violation. * diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/LoggingConstraintValidationHandler.java b/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/LoggingConstraintValidationHandler.java index ec8f9d7d6..4443c18cc 100644 --- a/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/LoggingConstraintValidationHandler.java +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/LoggingConstraintValidationHandler.java @@ -11,6 +11,7 @@ import gov.nist.secauto.metaschema.core.metapath.item.ISequence; import gov.nist.secauto.metaschema.core.metapath.item.node.INodeItem; import gov.nist.secauto.metaschema.core.model.constraint.IConstraint.Level; +import gov.nist.secauto.metaschema.core.model.constraint.IReportConstraint; import gov.nist.secauto.metaschema.core.util.ObjectUtils; import org.apache.logging.log4j.LogBuilder; @@ -296,6 +297,27 @@ public void handleExpectViolation( } } + @Override + public void handleReportViolation( + @NonNull IReportConstraint constraint, + @NonNull INodeItem node, + @NonNull INodeItem target, + @NonNull DynamicContext dynamicContext) throws ConstraintValidationException { + Level level = constraint.getLevel(); + if (isLogged(level)) { + logMessage( + level, + constraint.getId(), + target, + newReportViolationMessage( + constraint, + node, + target, + dynamicContext), + NO_EXCEPTION); + } + } + @Override public void handleAllowedValuesViolation( List failedConstraints, diff --git a/core/src/test/java/gov/nist/secauto/metaschema/core/model/constraint/ReportConstraintTest.java b/core/src/test/java/gov/nist/secauto/metaschema/core/model/constraint/ReportConstraintTest.java index 076982c39..ca6b8fa5e 100644 --- a/core/src/test/java/gov/nist/secauto/metaschema/core/model/constraint/ReportConstraintTest.java +++ b/core/src/test/java/gov/nist/secauto/metaschema/core/model/constraint/ReportConstraintTest.java @@ -5,18 +5,41 @@ package gov.nist.secauto.metaschema.core.model.constraint; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasProperty; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import gov.nist.secauto.metaschema.core.metapath.DynamicContext; import gov.nist.secauto.metaschema.core.metapath.IMetapathExpression; +import gov.nist.secauto.metaschema.core.metapath.StaticContext; +import gov.nist.secauto.metaschema.core.metapath.format.IPathFormatter; +import gov.nist.secauto.metaschema.core.metapath.item.atomic.IStringItem; +import gov.nist.secauto.metaschema.core.metapath.item.node.IFlagNodeItem; +import gov.nist.secauto.metaschema.core.model.IFlagDefinition; import gov.nist.secauto.metaschema.core.model.ISource; import gov.nist.secauto.metaschema.core.model.constraint.IConstraint.Level; import gov.nist.secauto.metaschema.core.model.constraint.IConstraint.Type; +import gov.nist.secauto.metaschema.core.qname.IEnhancedQName; +import gov.nist.secauto.metaschema.core.testsupport.mocking.MockNodeItemFactory; +import gov.nist.secauto.metaschema.core.util.CollectionUtil; +import gov.nist.secauto.metaschema.core.util.ObjectUtils; import org.junit.jupiter.api.Test; +import java.net.URI; + import edu.umd.cs.findbugs.annotations.NonNull; /** @@ -29,11 +52,21 @@ *

  • Constraint properties (id, level, message) are accessible
  • *
  • Visitor pattern works correctly (visitReportConstraint)
  • *
  • Default level is INFORMATIONAL
  • + *
  • Validation generates findings when test is TRUE (opposite of expect)
  • + *
  • Validation does not generate findings when test is FALSE
  • * */ +@SuppressWarnings("PMD.TooManyStaticImports") class ReportConstraintTest { @NonNull private static final String TEST_SOURCE = "https://example.com/test"; + @NonNull + private static final String NS = ObjectUtils.notNull(URI.create("http://example.com/ns").toASCIIString()); + + @NonNull + private static IEnhancedQName qname(@NonNull String name) { + return IEnhancedQName.of(NS, name); + } /** * Test that the builder creates a valid constraint with test expression. @@ -175,6 +208,396 @@ void testBuilderWithoutSourceThrowsException() { "Building without source should throw NullPointerException"); } + // ========================================================================= + // Validation Pipeline Tests + // Report constraints generate findings when test is TRUE (opposite of expect) + // ========================================================================= + + /** + * Test that report constraint generates a finding when test evaluates to TRUE. + *

    + * This is the opposite of expect constraints, which generate findings when test + * evaluates to FALSE. + * + * @throws ConstraintValidationException + * if an error occurred during validation + */ + @SuppressWarnings("null") + @Test + void testReportConstraintGeneratesFindingWhenTestIsTrue() throws ConstraintValidationException { + MockNodeItemFactory itemFactory = new MockNodeItemFactory(); + + // Create a flag with value "deprecated-value" + IFlagNodeItem flag = itemFactory.flag(qname("value"), IStringItem.valueOf("deprecated-value")); + + IFlagDefinition flagDefinition = mock(IFlagDefinition.class); + + ISource source = mock(ISource.class); + + // Create report constraint with test that evaluates to TRUE + // This should generate a finding because report fires on TRUE + IReportConstraint reportConstraint = IReportConstraint.builder() + .source(source) + .test(IMetapathExpression.compile("contains(., 'deprecated')")) + .message("This value is deprecated") + .build(); + + doReturn(flagDefinition).when(flag).getDefinition(); + doReturn("flag/path").when(flag).toPath(any(IPathFormatter.class)); + + doReturn(CollectionUtil.emptyMap()).when(flagDefinition).getLetExpressions(); + doReturn(CollectionUtil.emptyList()).when(flagDefinition).getAllowedValuesConstraints(); + doReturn(CollectionUtil.emptyList()).when(flagDefinition).getExpectConstraints(); + doReturn(CollectionUtil.singletonList(reportConstraint)).when(flagDefinition).getReportConstraints(); + doReturn(CollectionUtil.emptyList()).when(flagDefinition).getMatchesConstraints(); + doReturn(CollectionUtil.emptyList()).when(flagDefinition).getIndexHasKeyConstraints(); + + StaticContext staticContext = StaticContext.instance(); + doReturn(staticContext).when(source).getStaticContext(); + + FindingCollectingConstraintValidationHandler handler = new FindingCollectingConstraintValidationHandler(); + DefaultConstraintValidator validator = new DefaultConstraintValidator(handler); + DynamicContext dynamicContext = new DynamicContext(staticContext); + validator.validate(flag, dynamicContext); + validator.finalizeValidation(dynamicContext); + + assertAll( + () -> assertTrue(handler.isPassing(), + "Validation should pass because default level is INFORMATIONAL"), + () -> assertThat("should have 1 finding", handler.getFindings(), hasSize(1)), + () -> assertThat("finding should be for the flag node", handler.getFindings(), + hasItem(hasProperty("node", is(flag)))), + () -> assertThat("finding should have the custom message", handler.getFindings(), + hasItem(hasProperty("message", is("This value is deprecated"))))); + } + + /** + * Test that report constraint does NOT generate a finding when test evaluates + * to FALSE. + * + * @throws ConstraintValidationException + * if an error occurred during validation + */ + @SuppressWarnings("null") + @Test + void testReportConstraintNoFindingWhenTestIsFalse() throws ConstraintValidationException { + MockNodeItemFactory itemFactory = new MockNodeItemFactory(); + + // Create a flag with value "normal-value" + IFlagNodeItem flag = itemFactory.flag(qname("value"), IStringItem.valueOf("normal-value")); + + IFlagDefinition flagDefinition = mock(IFlagDefinition.class); + + ISource source = mock(ISource.class); + + // Create report constraint with test that evaluates to FALSE + // This should NOT generate a finding because report only fires on TRUE + IReportConstraint reportConstraint = IReportConstraint.builder() + .source(source) + .test(IMetapathExpression.compile("contains(., 'deprecated')")) + .message("This value is deprecated") + .build(); + + doReturn(flagDefinition).when(flag).getDefinition(); + doReturn("flag/path").when(flag).toPath(any(IPathFormatter.class)); + + doReturn(CollectionUtil.emptyMap()).when(flagDefinition).getLetExpressions(); + doReturn(CollectionUtil.emptyList()).when(flagDefinition).getAllowedValuesConstraints(); + doReturn(CollectionUtil.emptyList()).when(flagDefinition).getExpectConstraints(); + doReturn(CollectionUtil.singletonList(reportConstraint)).when(flagDefinition).getReportConstraints(); + doReturn(CollectionUtil.emptyList()).when(flagDefinition).getMatchesConstraints(); + doReturn(CollectionUtil.emptyList()).when(flagDefinition).getIndexHasKeyConstraints(); + + StaticContext staticContext = StaticContext.instance(); + doReturn(staticContext).when(source).getStaticContext(); + + FindingCollectingConstraintValidationHandler handler = new FindingCollectingConstraintValidationHandler(); + DefaultConstraintValidator validator = new DefaultConstraintValidator(handler); + DynamicContext dynamicContext = new DynamicContext(staticContext); + validator.validate(flag, dynamicContext); + validator.finalizeValidation(dynamicContext); + + assertAll( + () -> assertTrue(handler.isPassing(), "Validation should pass"), + () -> assertThat("should have no findings", handler.getFindings(), hasSize(0))); + } + + /** + * Test that report constraint with ERROR level causes validation failure. + * + * @throws ConstraintValidationException + * if an error occurred during validation + */ + @SuppressWarnings("null") + @Test + void testReportConstraintWithErrorLevel() throws ConstraintValidationException { + MockNodeItemFactory itemFactory = new MockNodeItemFactory(); + + IFlagNodeItem flag = itemFactory.flag(qname("value"), IStringItem.valueOf("forbidden-value")); + + IFlagDefinition flagDefinition = mock(IFlagDefinition.class); + + ISource source = mock(ISource.class); + + // Create report constraint with ERROR level + IReportConstraint reportConstraint = IReportConstraint.builder() + .source(source) + .level(Level.ERROR) + .test(IMetapathExpression.compile("contains(., 'forbidden')")) + .message("Forbidden value detected") + .build(); + + doReturn(flagDefinition).when(flag).getDefinition(); + doReturn("flag/path").when(flag).toPath(any(IPathFormatter.class)); + + doReturn(CollectionUtil.emptyMap()).when(flagDefinition).getLetExpressions(); + doReturn(CollectionUtil.emptyList()).when(flagDefinition).getAllowedValuesConstraints(); + doReturn(CollectionUtil.emptyList()).when(flagDefinition).getExpectConstraints(); + doReturn(CollectionUtil.singletonList(reportConstraint)).when(flagDefinition).getReportConstraints(); + doReturn(CollectionUtil.emptyList()).when(flagDefinition).getMatchesConstraints(); + doReturn(CollectionUtil.emptyList()).when(flagDefinition).getIndexHasKeyConstraints(); + + StaticContext staticContext = StaticContext.instance(); + doReturn(staticContext).when(source).getStaticContext(); + + FindingCollectingConstraintValidationHandler handler = new FindingCollectingConstraintValidationHandler(); + DefaultConstraintValidator validator = new DefaultConstraintValidator(handler); + DynamicContext dynamicContext = new DynamicContext(staticContext); + validator.validate(flag, dynamicContext); + validator.finalizeValidation(dynamicContext); + + assertAll( + () -> assertFalse(handler.isPassing(), + "Validation should fail because report with ERROR level fired"), + () -> assertThat("should have 1 finding", handler.getFindings(), hasSize(1)), + () -> assertThat("finding should have ERROR severity", handler.getFindings(), + hasItem(hasProperty("severity", is(Level.ERROR))))); + } + + /** + * Test that report constraint with WARNING level does not cause validation + * failure but still records the finding. + * + * @throws ConstraintValidationException + * if an error occurred during validation + */ + @SuppressWarnings("null") + @Test + void testReportConstraintWithWarningLevel() throws ConstraintValidationException { + MockNodeItemFactory itemFactory = new MockNodeItemFactory(); + + IFlagNodeItem flag = itemFactory.flag(qname("value"), IStringItem.valueOf("warning-value")); + + IFlagDefinition flagDefinition = mock(IFlagDefinition.class); + + ISource source = mock(ISource.class); + + // Create report constraint with WARNING level + IReportConstraint reportConstraint = IReportConstraint.builder() + .source(source) + .level(Level.WARNING) + .test(IMetapathExpression.compile("contains(., 'warning')")) + .message("Warning: check this value") + .build(); + + doReturn(flagDefinition).when(flag).getDefinition(); + doReturn("flag/path").when(flag).toPath(any(IPathFormatter.class)); + + doReturn(CollectionUtil.emptyMap()).when(flagDefinition).getLetExpressions(); + doReturn(CollectionUtil.emptyList()).when(flagDefinition).getAllowedValuesConstraints(); + doReturn(CollectionUtil.emptyList()).when(flagDefinition).getExpectConstraints(); + doReturn(CollectionUtil.singletonList(reportConstraint)).when(flagDefinition).getReportConstraints(); + doReturn(CollectionUtil.emptyList()).when(flagDefinition).getMatchesConstraints(); + doReturn(CollectionUtil.emptyList()).when(flagDefinition).getIndexHasKeyConstraints(); + + StaticContext staticContext = StaticContext.instance(); + doReturn(staticContext).when(source).getStaticContext(); + + FindingCollectingConstraintValidationHandler handler = new FindingCollectingConstraintValidationHandler(); + DefaultConstraintValidator validator = new DefaultConstraintValidator(handler); + DynamicContext dynamicContext = new DynamicContext(staticContext); + validator.validate(flag, dynamicContext); + validator.finalizeValidation(dynamicContext); + + assertAll( + () -> assertTrue(handler.isPassing(), + "Validation should pass because WARNING level does not fail validation"), + () -> assertThat("should have 1 finding", handler.getFindings(), hasSize(1)), + () -> assertThat("finding should have WARNING severity", handler.getFindings(), + hasItem(hasProperty("severity", is(Level.WARNING))))); + } + + /** + * Test that report constraint with CRITICAL level causes validation failure. + * + * @throws ConstraintValidationException + * if an error occurred during validation + */ + @SuppressWarnings("null") + @Test + void testReportConstraintWithCriticalLevel() throws ConstraintValidationException { + MockNodeItemFactory itemFactory = new MockNodeItemFactory(); + + IFlagNodeItem flag = itemFactory.flag(qname("value"), IStringItem.valueOf("critical-issue")); + + IFlagDefinition flagDefinition = mock(IFlagDefinition.class); + + ISource source = mock(ISource.class); + + // Create report constraint with CRITICAL level + IReportConstraint reportConstraint = IReportConstraint.builder() + .source(source) + .level(Level.CRITICAL) + .test(IMetapathExpression.compile("contains(., 'critical')")) + .message("Critical issue detected!") + .build(); + + doReturn(flagDefinition).when(flag).getDefinition(); + doReturn("flag/path").when(flag).toPath(any(IPathFormatter.class)); + + doReturn(CollectionUtil.emptyMap()).when(flagDefinition).getLetExpressions(); + doReturn(CollectionUtil.emptyList()).when(flagDefinition).getAllowedValuesConstraints(); + doReturn(CollectionUtil.emptyList()).when(flagDefinition).getExpectConstraints(); + doReturn(CollectionUtil.singletonList(reportConstraint)).when(flagDefinition).getReportConstraints(); + doReturn(CollectionUtil.emptyList()).when(flagDefinition).getMatchesConstraints(); + doReturn(CollectionUtil.emptyList()).when(flagDefinition).getIndexHasKeyConstraints(); + + StaticContext staticContext = StaticContext.instance(); + doReturn(staticContext).when(source).getStaticContext(); + + FindingCollectingConstraintValidationHandler handler = new FindingCollectingConstraintValidationHandler(); + DefaultConstraintValidator validator = new DefaultConstraintValidator(handler); + DynamicContext dynamicContext = new DynamicContext(staticContext); + validator.validate(flag, dynamicContext); + validator.finalizeValidation(dynamicContext); + + assertAll( + () -> assertFalse(handler.isPassing(), + "Validation should fail because CRITICAL level report fired"), + () -> assertThat("should have 1 finding", handler.getFindings(), hasSize(1)), + () -> assertThat("finding should have CRITICAL severity", handler.getFindings(), + hasItem(hasProperty("severity", is(Level.CRITICAL))))); + } + + /** + * Test that report constraint uses the custom message in finding. + * + * @throws ConstraintValidationException + * if an error occurred during validation + */ + @SuppressWarnings("null") + @Test + void testReportConstraintCustomMessage() throws ConstraintValidationException { + MockNodeItemFactory itemFactory = new MockNodeItemFactory(); + + IFlagNodeItem flag = itemFactory.flag(qname("value"), IStringItem.valueOf("test")); + + IFlagDefinition flagDefinition = mock(IFlagDefinition.class); + + ISource source = mock(ISource.class); + + String customMessage = "This is a custom report message"; + + // Create report constraint with custom message + IReportConstraint reportConstraint = IReportConstraint.builder() + .source(source) + .test(IMetapathExpression.compile("true()")) + .message(customMessage) + .build(); + + doReturn(flagDefinition).when(flag).getDefinition(); + doReturn("flag/path").when(flag).toPath(any(IPathFormatter.class)); + + doReturn(CollectionUtil.emptyMap()).when(flagDefinition).getLetExpressions(); + doReturn(CollectionUtil.emptyList()).when(flagDefinition).getAllowedValuesConstraints(); + doReturn(CollectionUtil.emptyList()).when(flagDefinition).getExpectConstraints(); + doReturn(CollectionUtil.singletonList(reportConstraint)).when(flagDefinition).getReportConstraints(); + doReturn(CollectionUtil.emptyList()).when(flagDefinition).getMatchesConstraints(); + doReturn(CollectionUtil.emptyList()).when(flagDefinition).getIndexHasKeyConstraints(); + + StaticContext staticContext = StaticContext.instance(); + doReturn(staticContext).when(source).getStaticContext(); + + FindingCollectingConstraintValidationHandler handler = new FindingCollectingConstraintValidationHandler(); + DefaultConstraintValidator validator = new DefaultConstraintValidator(handler); + DynamicContext dynamicContext = new DynamicContext(staticContext); + validator.validate(flag, dynamicContext); + validator.finalizeValidation(dynamicContext); + + assertAll( + () -> assertThat("should have 1 finding", handler.getFindings(), hasSize(1)), + () -> assertThat("finding should have the custom message", handler.getFindings(), + hasItem(hasProperty("message", is(customMessage))))); + } + + /** + * Test report vs expect semantics - verify they are opposites. + *

    + * Report fires when test is TRUE. Expect fails when test is FALSE. Same + * expression should produce opposite outcomes. + * + * @throws ConstraintValidationException + * if an error occurred during validation + */ + @SuppressWarnings("null") + @Test + void testReportAndExpectAreOpposites() throws ConstraintValidationException { + MockNodeItemFactory itemFactory = new MockNodeItemFactory(); + + // Value that makes "contains(., 'deprecated')" return TRUE + IFlagNodeItem flag = itemFactory.flag(qname("value"), IStringItem.valueOf("deprecated-value")); + + IFlagDefinition flagDefinition = mock(IFlagDefinition.class); + + ISource source = mock(ISource.class); + + // Report with test that evaluates to TRUE - should fire + IReportConstraint reportConstraint = IReportConstraint.builder() + .source(source) + .level(Level.ERROR) + .test(IMetapathExpression.compile("contains(., 'deprecated')")) + .message("Report: deprecated detected") + .build(); + + // Expect with same test that evaluates to TRUE - should NOT fire + IExpectConstraint expectConstraint = IExpectConstraint.builder() + .source(source) + .level(Level.ERROR) + .test(IMetapathExpression.compile("contains(., 'deprecated')")) + .message("Expect: deprecated NOT detected") + .build(); + + doReturn(flagDefinition).when(flag).getDefinition(); + doReturn("flag/path").when(flag).toPath(any(IPathFormatter.class)); + + doReturn(CollectionUtil.emptyMap()).when(flagDefinition).getLetExpressions(); + doReturn(CollectionUtil.emptyList()).when(flagDefinition).getAllowedValuesConstraints(); + doReturn(CollectionUtil.singletonList(expectConstraint)).when(flagDefinition).getExpectConstraints(); + doReturn(CollectionUtil.singletonList(reportConstraint)).when(flagDefinition).getReportConstraints(); + doReturn(CollectionUtil.emptyList()).when(flagDefinition).getMatchesConstraints(); + doReturn(CollectionUtil.emptyList()).when(flagDefinition).getIndexHasKeyConstraints(); + + StaticContext staticContext = StaticContext.instance(); + doReturn(staticContext).when(source).getStaticContext(); + + FindingCollectingConstraintValidationHandler handler = new FindingCollectingConstraintValidationHandler(); + DefaultConstraintValidator validator = new DefaultConstraintValidator(handler); + DynamicContext dynamicContext = new DynamicContext(staticContext); + validator.validate(flag, dynamicContext); + validator.finalizeValidation(dynamicContext); + + // Report fires when TRUE (has finding), Expect passes when TRUE (no finding) + // So only Report should generate a finding + assertAll( + () -> assertFalse(handler.isPassing(), + "Validation should fail because Report with ERROR level fired"), + () -> assertThat("should have exactly 1 finding (from Report, not Expect)", + handler.getFindings(), hasSize(1)), + () -> assertThat("finding message should be from Report constraint", + handler.getFindings(), + hasItem(hasProperty("message", is("Report: deprecated detected"))))); + } + /** * A test visitor implementation to verify the visitor pattern. */ diff --git a/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/impl/AnnotationGenerator.java b/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/impl/AnnotationGenerator.java index 631ae0b8c..361a8cfeb 100644 --- a/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/impl/AnnotationGenerator.java +++ b/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/impl/AnnotationGenerator.java @@ -27,6 +27,7 @@ import gov.nist.secauto.metaschema.core.model.constraint.IConstraint; import gov.nist.secauto.metaschema.core.model.constraint.IExpectConstraint; import gov.nist.secauto.metaschema.core.model.constraint.IIndexConstraint; +import gov.nist.secauto.metaschema.core.model.constraint.IReportConstraint; import gov.nist.secauto.metaschema.core.model.constraint.IIndexHasKeyConstraint; import gov.nist.secauto.metaschema.core.model.constraint.IKeyField; import gov.nist.secauto.metaschema.core.model.constraint.ILet; @@ -45,6 +46,7 @@ import gov.nist.secauto.metaschema.databind.model.annotations.KeyField; import gov.nist.secauto.metaschema.databind.model.annotations.Let; import gov.nist.secauto.metaschema.databind.model.annotations.Matches; +import gov.nist.secauto.metaschema.databind.model.annotations.Report; import gov.nist.secauto.metaschema.databind.model.annotations.ValueConstraints; import org.apache.logging.log4j.LogBuilder; @@ -144,6 +146,7 @@ public static void buildValueConstraints( applyIndexHasKeyConstraints(annotation, definition.getIndexHasKeyConstraints()); applyMatchesConstraints(annotation, definition.getMatchesConstraints()); applyExpectConstraints(annotation, definition.getExpectConstraints()); + applyReportConstraints(annotation, definition.getReportConstraints()); builder.addMember("valueConstraints", "$L", annotation.build()); } @@ -166,9 +169,10 @@ public static void buildValueConstraints( List indexHasKey = definition.getIndexHasKeyConstraints(); List matches = definition.getMatchesConstraints(); List expects = definition.getExpectConstraints(); + List reports = definition.getReportConstraints(); if (!lets.isEmpty() || !allowedValues.isEmpty() || !indexHasKey.isEmpty() || !matches.isEmpty() - || !expects.isEmpty()) { + || !expects.isEmpty() || !reports.isEmpty()) { AnnotationSpec.Builder annotation = AnnotationSpec.builder(ValueConstraints.class); assert annotation != null; @@ -177,6 +181,7 @@ public static void buildValueConstraints( applyIndexHasKeyConstraints(annotation, indexHasKey); applyMatchesConstraints(annotation, matches); applyExpectConstraints(annotation, expects); + applyReportConstraints(annotation, reports); builder.addMember("valueConstraints", "$L", annotation.build()); } @@ -370,6 +375,31 @@ private static void applyExpectConstraints( } } + private static void applyReportConstraints( + @NonNull AnnotationSpec.Builder annotation, + @NonNull List constraints) { + for (IReportConstraint constraint : constraints) { + assert constraint != null; + + AnnotationSpec.Builder constraintAnnotation = AnnotationSpec.builder(Report.class); + + buildConstraint(Report.class, constraintAnnotation, constraint); + + constraintAnnotation.addMember("test", "$S", constraint.getTest().getPath()); + + if (constraint.getMessage() != null) { + constraintAnnotation.addMember("message", "$S", constraint.getMessage()); + } + + MarkupMultiline remarks = constraint.getRemarks(); + if (remarks != null) { + constraintAnnotation.addMember("remarks", "$S", remarks.toMarkdown()); + } + + annotation.addMember("report", "$L", constraintAnnotation.build()); + } + } + private static void applyIndexConstraints( @NonNull AnnotationSpec.Builder annotation, @NonNull List constraints) { diff --git a/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/annotations/Report.java b/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/annotations/Report.java new file mode 100644 index 000000000..98bf9d07c --- /dev/null +++ b/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/annotations/Report.java @@ -0,0 +1,114 @@ +/* + * SPDX-FileCopyrightText: none + * SPDX-License-Identifier: CC0-1.0 + */ + +package gov.nist.secauto.metaschema.databind.model.annotations; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import gov.nist.secauto.metaschema.core.model.constraint.IConstraint; +import gov.nist.secauto.metaschema.core.model.constraint.IConstraint.Level; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * This annotation defines a report condition in the context of the containing + * annotation. + *

    + * Report constraints generate findings when their test expression evaluates to + * {@code true}, which is the opposite of expect constraints. + */ +@Documented +@Retention(RUNTIME) +@Target(ANNOTATION_TYPE) +public @interface Report { + /** + * An optional identifier for the constraint, which must be unique to only this + * constraint. + * + * @return the identifier if provided or an empty string otherwise + */ + @SuppressWarnings("PMD.ShortMethodName") + @NonNull + String id() default ""; + + /** + * An optional formal name for the constraint. + * + * @return the formal name if provided or an empty string otherwise + */ + @NonNull + String formalName() default ""; + + /** + * An optional description of the constraint. + * + * @return the description if provided or an empty string otherwise + */ + @NonNull + String description() default ""; + + /** + * The significance of a violation of this constraint. + * + * @return the level + */ + @NonNull + Level level() default IConstraint.Level.ERROR; + + /** + * An optional metapath that points to the target flag or field value that the + * constraint applies to. If omitted the target will be ".", which means the + * target is the value of the {@link BoundFlag}, {@link BoundField} or + * {@link BoundFieldValue} annotation the constraint appears on. In the prior + * case, this annotation may only appear on a {@link BoundField} if the field + * has no flags, which results in a {@link BoundField} annotation on a field + * instance with a scalar, data type value. + * + * @return the target metapath + */ + @NonNull + String target() default "."; + + /** + * An optional set of properties associated with this constraint. + * + * @return the properties or an empty array with no properties + */ + Property[] properties() default {}; + + /** + * A metapath that is expected to evaluate to {@code true} when a finding should + * be reported. + *

    + * This is the opposite of expect constraints - report constraints fire when the + * test is true. + * + * @return a metapath expression + */ + @NonNull + String test(); + + /** + * The message to emit when the constraint is violated. + * + * @return the message or an empty string otherwise + */ + @NonNull + String message() default ""; + + /** + * Any remarks about the constraint, encoded as an escaped Markdown string. + * + * @return an encoded markdown string or an empty string if no remarks are + * provided + */ + @NonNull + String remarks() default ""; +} diff --git a/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/annotations/ValueConstraints.java b/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/annotations/ValueConstraints.java index 5eff46ca5..94b497aef 100644 --- a/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/annotations/ValueConstraints.java +++ b/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/annotations/ValueConstraints.java @@ -65,4 +65,17 @@ */ @NonNull Expect[] expect() default {}; + + /** + * Get the report constraints for the type or field this annotation is applied + * to. + *

    + * Report constraints generate findings when their test expression evaluates to + * {@code true}, which is the opposite of expect constraints. + * + * @return the report constraints or an empty array if no report constraints are + * defined + */ + @NonNull + Report[] report() default {}; } diff --git a/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/impl/ConstraintFactory.java b/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/impl/ConstraintFactory.java index acba81cd0..b312ea0b8 100644 --- a/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/impl/ConstraintFactory.java +++ b/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/impl/ConstraintFactory.java @@ -21,6 +21,7 @@ import gov.nist.secauto.metaschema.core.model.constraint.IExpectConstraint; import gov.nist.secauto.metaschema.core.model.constraint.IIndexConstraint; import gov.nist.secauto.metaschema.core.model.constraint.IIndexHasKeyConstraint; +import gov.nist.secauto.metaschema.core.model.constraint.IReportConstraint; import gov.nist.secauto.metaschema.core.model.constraint.IKeyField; import gov.nist.secauto.metaschema.core.model.constraint.ILet; import gov.nist.secauto.metaschema.core.model.constraint.IMatchesConstraint; @@ -37,6 +38,7 @@ import gov.nist.secauto.metaschema.databind.model.annotations.Let; import gov.nist.secauto.metaschema.databind.model.annotations.Matches; import gov.nist.secauto.metaschema.databind.model.annotations.ModelUtil; +import gov.nist.secauto.metaschema.databind.model.annotations.Report; import gov.nist.secauto.metaschema.databind.model.annotations.NullJavaTypeAdapter; import gov.nist.secauto.metaschema.databind.model.annotations.Property; @@ -305,6 +307,37 @@ static IExpectConstraint newExpectConstraint(@NonNull Expect constraint, @NonNul return builder.build(); } + /** + * Create a new report constraint from the provided annotation. + *

    + * Report constraints generate findings when their test expression evaluates to + * {@code true}, which is the opposite of expect constraints. + * + * @param constraint + * the annotation containing the constraint configuration + * @param source + * the source of the constraint + * @return a new report constraint + */ + @NonNull + static IReportConstraint newReportConstraint(@NonNull Report constraint, @NonNull ISource source) { + IReportConstraint.Builder builder = IReportConstraint.builder(); + applyId(builder, constraint.id()); + applyFormalName(builder, constraint.formalName()); + applyDescription(builder, constraint.description()); + builder + .source(source) + .level(constraint.level()); + applyTarget(builder, metapath(constraint.target(), source)); + applyProperties(builder, constraint.properties()); + applyMessage(builder, constraint.message()); + applyRemarks(builder, constraint.remarks()); + + builder.test(metapath(constraint.test(), source)); + + return builder.build(); + } + @Nullable static Integer toCardinality(int value) { return value < 0 ? null : value; diff --git a/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/impl/ConstraintSupport.java b/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/impl/ConstraintSupport.java index 836bdba99..ce5fabf8d 100644 --- a/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/impl/ConstraintSupport.java +++ b/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/impl/ConstraintSupport.java @@ -53,6 +53,9 @@ public static void parse( // NOPMD - intentional Arrays.stream(valueAnnotation.expect()) .map(annotation -> ConstraintFactory.newExpectConstraint(annotation, source)) .forEachOrdered(set::addConstraint); + Arrays.stream(valueAnnotation.report()) + .map(annotation -> ConstraintFactory.newReportConstraint(annotation, source)) + .forEachOrdered(set::addConstraint); } } diff --git a/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/metaschema/binding/TargetedExpectConstraint.java b/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/metaschema/binding/TargetedExpectConstraint.java index 1f7bbb546..ed06c4fa1 100644 --- a/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/metaschema/binding/TargetedExpectConstraint.java +++ b/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/metaschema/binding/TargetedExpectConstraint.java @@ -27,6 +27,7 @@ import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaAssembly; import gov.nist.secauto.metaschema.databind.model.annotations.ValueConstraints; import gov.nist.secauto.metaschema.databind.model.metaschema.IConfigurableMessageConstraintBase; +import gov.nist.secauto.metaschema.databind.model.metaschema.ITargetedConstraintBase; import java.util.LinkedList; import java.util.List; import org.apache.commons.lang3.builder.ReflectionToStringBuilder; @@ -36,7 +37,8 @@ formalName = "Expect Condition Constraint", name = "targeted-expect-constraint", moduleClass = MetaschemaModelModule.class) -public class TargetedExpectConstraint implements IBoundObject, IConfigurableMessageConstraintBase { +public class TargetedExpectConstraint + implements IBoundObject, ITargetedConstraintBase, IConfigurableMessageConstraintBase { private final IMetaschemaData __metaschemaData; @BoundFlag( diff --git a/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/metaschema/binding/TargetedHasCardinalityConstraint.java b/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/metaschema/binding/TargetedHasCardinalityConstraint.java index 808f711de..d2d25be1f 100644 --- a/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/metaschema/binding/TargetedHasCardinalityConstraint.java +++ b/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/metaschema/binding/TargetedHasCardinalityConstraint.java @@ -29,6 +29,7 @@ import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaAssembly; import gov.nist.secauto.metaschema.databind.model.annotations.ValueConstraints; import gov.nist.secauto.metaschema.databind.model.metaschema.IConfigurableMessageConstraintBase; +import gov.nist.secauto.metaschema.databind.model.metaschema.ITargetedConstraintBase; import java.math.BigInteger; import java.util.LinkedList; import java.util.List; @@ -39,7 +40,8 @@ formalName = "Targeted Cardinality Constraint", name = "targeted-has-cardinality-constraint", moduleClass = MetaschemaModelModule.class) -public class TargetedHasCardinalityConstraint implements IBoundObject, IConfigurableMessageConstraintBase { +public class TargetedHasCardinalityConstraint + implements IBoundObject, ITargetedConstraintBase, IConfigurableMessageConstraintBase { private final IMetaschemaData __metaschemaData; @BoundFlag( diff --git a/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/metaschema/binding/TargetedIndexConstraint.java b/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/metaschema/binding/TargetedIndexConstraint.java index aee1b72d8..7cbb92dff 100644 --- a/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/metaschema/binding/TargetedIndexConstraint.java +++ b/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/metaschema/binding/TargetedIndexConstraint.java @@ -27,6 +27,7 @@ import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaAssembly; import gov.nist.secauto.metaschema.databind.model.annotations.ValueConstraints; import gov.nist.secauto.metaschema.databind.model.metaschema.IConfigurableMessageConstraintBase; +import gov.nist.secauto.metaschema.databind.model.metaschema.ITargetedConstraintBase; import java.util.LinkedList; import java.util.List; import org.apache.commons.lang3.builder.ReflectionToStringBuilder; @@ -36,7 +37,8 @@ formalName = "Targeted Index Constraint", name = "targeted-index-constraint", moduleClass = MetaschemaModelModule.class) -public class TargetedIndexConstraint implements IBoundObject, IConfigurableMessageConstraintBase { +public class TargetedIndexConstraint + implements IBoundObject, ITargetedConstraintBase, IConfigurableMessageConstraintBase { private final IMetaschemaData __metaschemaData; @BoundFlag( diff --git a/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/metaschema/binding/TargetedIndexHasKeyConstraint.java b/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/metaschema/binding/TargetedIndexHasKeyConstraint.java index 186fe574b..eff4c5745 100644 --- a/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/metaschema/binding/TargetedIndexHasKeyConstraint.java +++ b/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/metaschema/binding/TargetedIndexHasKeyConstraint.java @@ -27,6 +27,7 @@ import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaAssembly; import gov.nist.secauto.metaschema.databind.model.annotations.ValueConstraints; import gov.nist.secauto.metaschema.databind.model.metaschema.IConfigurableMessageConstraintBase; +import gov.nist.secauto.metaschema.databind.model.metaschema.ITargetedConstraintBase; import java.util.LinkedList; import java.util.List; import org.apache.commons.lang3.builder.ReflectionToStringBuilder; @@ -36,7 +37,8 @@ formalName = "Targeted Index Has Key Constraint", name = "targeted-index-has-key-constraint", moduleClass = MetaschemaModelModule.class) -public class TargetedIndexHasKeyConstraint implements IBoundObject, IConfigurableMessageConstraintBase { +public class TargetedIndexHasKeyConstraint + implements IBoundObject, ITargetedConstraintBase, IConfigurableMessageConstraintBase { private final IMetaschemaData __metaschemaData; @BoundFlag( diff --git a/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/metaschema/binding/TargetedIsUniqueConstraint.java b/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/metaschema/binding/TargetedIsUniqueConstraint.java index 03b9b7ea3..be5ef77ad 100644 --- a/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/metaschema/binding/TargetedIsUniqueConstraint.java +++ b/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/metaschema/binding/TargetedIsUniqueConstraint.java @@ -27,6 +27,7 @@ import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaAssembly; import gov.nist.secauto.metaschema.databind.model.annotations.ValueConstraints; import gov.nist.secauto.metaschema.databind.model.metaschema.IConfigurableMessageConstraintBase; +import gov.nist.secauto.metaschema.databind.model.metaschema.ITargetedConstraintBase; import java.util.LinkedList; import java.util.List; import org.apache.commons.lang3.builder.ReflectionToStringBuilder; @@ -36,7 +37,8 @@ formalName = "Targeted Unique Constraint", name = "targeted-is-unique-constraint", moduleClass = MetaschemaModelModule.class) -public class TargetedIsUniqueConstraint implements IBoundObject, IConfigurableMessageConstraintBase { +public class TargetedIsUniqueConstraint + implements IBoundObject, ITargetedConstraintBase, IConfigurableMessageConstraintBase { private final IMetaschemaData __metaschemaData; @BoundFlag( diff --git a/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/metaschema/binding/TargetedMatchesConstraint.java b/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/metaschema/binding/TargetedMatchesConstraint.java index 47ae8e31c..a0c88b321 100644 --- a/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/metaschema/binding/TargetedMatchesConstraint.java +++ b/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/metaschema/binding/TargetedMatchesConstraint.java @@ -27,6 +27,7 @@ import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaAssembly; import gov.nist.secauto.metaschema.databind.model.annotations.ValueConstraints; import gov.nist.secauto.metaschema.databind.model.metaschema.IConfigurableMessageConstraintBase; +import gov.nist.secauto.metaschema.databind.model.metaschema.ITargetedConstraintBase; import java.util.LinkedList; import java.util.List; import org.apache.commons.lang3.builder.ReflectionToStringBuilder; @@ -36,7 +37,8 @@ formalName = "Value Matches Constraint", name = "targeted-matches-constraint", moduleClass = MetaschemaModelModule.class) -public class TargetedMatchesConstraint implements IBoundObject, IConfigurableMessageConstraintBase { +public class TargetedMatchesConstraint + implements IBoundObject, ITargetedConstraintBase, IConfigurableMessageConstraintBase { private final IMetaschemaData __metaschemaData; @BoundFlag( diff --git a/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/metaschema/binding/TargetedReportConstraint.java b/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/metaschema/binding/TargetedReportConstraint.java index b3550b07d..7f3e06aab 100644 --- a/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/metaschema/binding/TargetedReportConstraint.java +++ b/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/metaschema/binding/TargetedReportConstraint.java @@ -27,6 +27,7 @@ import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaAssembly; import gov.nist.secauto.metaschema.databind.model.annotations.ValueConstraints; import gov.nist.secauto.metaschema.databind.model.metaschema.IConfigurableMessageConstraintBase; +import gov.nist.secauto.metaschema.databind.model.metaschema.ITargetedConstraintBase; import java.util.LinkedList; import java.util.List; import org.apache.commons.lang3.builder.ReflectionToStringBuilder; @@ -36,7 +37,8 @@ formalName = "Report Condition Constraint", name = "targeted-report-constraint", moduleClass = MetaschemaModelModule.class) -public class TargetedReportConstraint implements IBoundObject, IConfigurableMessageConstraintBase { +public class TargetedReportConstraint + implements IBoundObject, ITargetedConstraintBase, IConfigurableMessageConstraintBase { private final IMetaschemaData __metaschemaData; @BoundFlag( diff --git a/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/metaschema/impl/ConstraintBindingSupport.java b/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/metaschema/impl/ConstraintBindingSupport.java index 766dfb648..ba6339418 100644 --- a/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/metaschema/impl/ConstraintBindingSupport.java +++ b/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/metaschema/impl/ConstraintBindingSupport.java @@ -24,6 +24,7 @@ import gov.nist.secauto.metaschema.core.model.constraint.ILet; import gov.nist.secauto.metaschema.core.model.constraint.IMatchesConstraint; import gov.nist.secauto.metaschema.core.model.constraint.IModelConstrained; +import gov.nist.secauto.metaschema.core.model.constraint.IReportConstraint; import gov.nist.secauto.metaschema.core.model.constraint.IUniqueConstraint; import gov.nist.secauto.metaschema.core.model.constraint.IValueConstrained; import gov.nist.secauto.metaschema.core.util.ObjectUtils; @@ -47,6 +48,7 @@ import gov.nist.secauto.metaschema.databind.model.metaschema.binding.TargetedIndexHasKeyConstraint; import gov.nist.secauto.metaschema.databind.model.metaschema.binding.TargetedIsUniqueConstraint; import gov.nist.secauto.metaschema.databind.model.metaschema.binding.TargetedMatchesConstraint; +import gov.nist.secauto.metaschema.databind.model.metaschema.binding.TargetedReportConstraint; import java.math.BigInteger; import java.util.List; @@ -128,6 +130,9 @@ public static void parse( } else if (ruleObj instanceof TargetedMatchesConstraint) { IMatchesConstraint constraint = newMatches((TargetedMatchesConstraint) ruleObj, source); constraintSet.addConstraint(constraint); + } else if (ruleObj instanceof TargetedReportConstraint) { + IReportConstraint constraint = newReport((TargetedReportConstraint) ruleObj, source); + constraintSet.addConstraint(constraint); } } } @@ -171,6 +176,9 @@ public static void parse( } else if (ruleObj instanceof TargetedIsUniqueConstraint) { IUniqueConstraint constraint = newUnique((TargetedIsUniqueConstraint) ruleObj, source); constraintSet.addConstraint(constraint); + } else if (ruleObj instanceof TargetedReportConstraint) { + IReportConstraint constraint = newReport((TargetedReportConstraint) ruleObj, source); + constraintSet.addConstraint(constraint); } } } @@ -268,6 +276,29 @@ private static IExpectConstraint newExpect( return builder.build(); } + /** + * Create a new report constraint from a parsed binding object. + *

    + * Report constraints generate findings when their test expression evaluates to + * {@code true}, which is the opposite of expect constraints. + * + * @param obj + * the parsed constraint binding object + * @param source + * the source of the constraint + * @return the new report constraint + */ + @NonNull + private static IReportConstraint newReport( + @NonNull TargetedReportConstraint obj, + @NonNull ISource source) { + IReportConstraint.Builder builder = IReportConstraint.builder() + .test(metapath(ObjectUtils.requireNonNull(obj.getTest()), source)); + applyConfigurableCommonValues(obj, obj.getTarget(), source, builder); + + return builder.build(); + } + @NonNull private static > T handleKeyConstraints( @NonNull List keys, diff --git a/databind/src/main/metaschema-bindings/metaschema-model-bindings.xml b/databind/src/main/metaschema-bindings/metaschema-model-bindings.xml index a6ffece9a..90e09d4b6 100644 --- a/databind/src/main/metaschema-bindings/metaschema-model-bindings.xml +++ b/databind/src/main/metaschema-bindings/metaschema-model-bindings.xml @@ -79,36 +79,43 @@ + gov.nist.secauto.metaschema.databind.model.metaschema.ITargetedConstraintBase gov.nist.secauto.metaschema.databind.model.metaschema.IConfigurableMessageConstraintBase + gov.nist.secauto.metaschema.databind.model.metaschema.ITargetedConstraintBase gov.nist.secauto.metaschema.databind.model.metaschema.IConfigurableMessageConstraintBase + gov.nist.secauto.metaschema.databind.model.metaschema.ITargetedConstraintBase gov.nist.secauto.metaschema.databind.model.metaschema.IConfigurableMessageConstraintBase + gov.nist.secauto.metaschema.databind.model.metaschema.ITargetedConstraintBase gov.nist.secauto.metaschema.databind.model.metaschema.IConfigurableMessageConstraintBase + gov.nist.secauto.metaschema.databind.model.metaschema.ITargetedConstraintBase gov.nist.secauto.metaschema.databind.model.metaschema.IConfigurableMessageConstraintBase + gov.nist.secauto.metaschema.databind.model.metaschema.ITargetedConstraintBase gov.nist.secauto.metaschema.databind.model.metaschema.IConfigurableMessageConstraintBase + gov.nist.secauto.metaschema.databind.model.metaschema.ITargetedConstraintBase gov.nist.secauto.metaschema.databind.model.metaschema.IConfigurableMessageConstraintBase diff --git a/databind/src/test/java/gov/nist/secauto/metaschema/databind/codegen/impl/AnnotationGeneratorTest.java b/databind/src/test/java/gov/nist/secauto/metaschema/databind/codegen/impl/AnnotationGeneratorTest.java index 6ad0290f0..c8385b867 100644 --- a/databind/src/test/java/gov/nist/secauto/metaschema/databind/codegen/impl/AnnotationGeneratorTest.java +++ b/databind/src/test/java/gov/nist/secauto/metaschema/databind/codegen/impl/AnnotationGeneratorTest.java @@ -63,6 +63,8 @@ void letAssignmentTest() { will(returnValue(List.of())); allowing(flag).getExpectConstraints(); will(returnValue(List.of())); + allowing(flag).getReportConstraints(); + will(returnValue(List.of())); } }); diff --git a/databind/src/test/java/gov/nist/secauto/metaschema/databind/model/metaschema/BindingConstraintLoaderTest.java b/databind/src/test/java/gov/nist/secauto/metaschema/databind/model/metaschema/BindingConstraintLoaderTest.java index d862390f4..a73cb4cc9 100644 --- a/databind/src/test/java/gov/nist/secauto/metaschema/databind/model/metaschema/BindingConstraintLoaderTest.java +++ b/databind/src/test/java/gov/nist/secauto/metaschema/databind/model/metaschema/BindingConstraintLoaderTest.java @@ -7,19 +7,33 @@ import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; +import gov.nist.secauto.metaschema.core.metapath.StaticContext; import gov.nist.secauto.metaschema.core.model.IAssemblyDefinition; import gov.nist.secauto.metaschema.core.model.IConstraintLoader; +import gov.nist.secauto.metaschema.core.model.ISource; import gov.nist.secauto.metaschema.core.model.MetaschemaException; +import gov.nist.secauto.metaschema.core.model.constraint.AssemblyConstraintSet; +import gov.nist.secauto.metaschema.core.model.constraint.IConstraint; import gov.nist.secauto.metaschema.core.model.constraint.IConstraintSet; +import gov.nist.secauto.metaschema.core.model.constraint.IModelConstrained; +import gov.nist.secauto.metaschema.core.model.constraint.IReportConstraint; import gov.nist.secauto.metaschema.core.qname.IEnhancedQName; import gov.nist.secauto.metaschema.databind.IBindingContext; +import gov.nist.secauto.metaschema.databind.io.IBoundLoader; import gov.nist.secauto.metaschema.databind.model.IBoundModule; +import gov.nist.secauto.metaschema.databind.model.metaschema.binding.AssemblyConstraints; +import gov.nist.secauto.metaschema.databind.model.metaschema.binding.MetaschemaMetaConstraints; +import gov.nist.secauto.metaschema.databind.model.metaschema.binding.TargetedReportConstraint; +import gov.nist.secauto.metaschema.databind.model.metaschema.impl.ConstraintBindingSupport; import org.junit.jupiter.api.Test; import java.io.IOException; +import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -75,4 +89,453 @@ void testValid() throws MetaschemaException, IOException { () -> assertEquals(1, level3 == null ? 0 : level3.getLetExpressions().size(), "level 3 let"), () -> assertEquals(1, level3 == null ? 0 : level3.getExpectConstraints().size(), "level 3 expect")); } + + @Test + void testReportConstraintLoading() throws MetaschemaException, IOException { + IBindingContext bindingContext = IBindingContext.newInstance(); + IConstraintLoader loader = new BindingConstraintLoader(bindingContext); + + List constraints = loader.load( + Paths.get("src/test/resources/content/constraints/meta-constraints/meta-constraints-report.yaml")); + assertEquals(1, constraints.size(), "should load exactly one constraint set"); + + // Get the constraint set and verify it has the context with report constraint + IConstraintSet constraintSet = constraints.get(0); + assertNotNull(constraintSet, "constraint set should not be null"); + + // Now test that it applies to level1 + Path compileDir = Paths.get("target/generated-test-modules/meta-constraints-report/"); + Files.createDirectories(compileDir); + + bindingContext = IBindingContext.builder() + .compilePath(compileDir) + .constraintSet(constraints) + .build(); + + IBindingMetaschemaModule metaschema = bindingContext.loadMetaschema( + Paths.get("src/test/resources/content/constraints/meta-constraints/metaschema.xml")); + IBoundModule module = bindingContext.registerModule(metaschema); + + final IAssemblyDefinition level1 + = module.getAssemblyDefinitionByName(IEnhancedQName.of(NS, "level1").getIndexPosition()); + + assertNotNull(level1, "level1 assembly should exist"); + + // Debug: print all constraints for level1 + List allConstraints = level1.getConstraints(); + StringBuilder debug = new StringBuilder(); + debug.append("All constraints on level1: ").append(allConstraints.size()).append("\n"); + for (IConstraint c : allConstraints) { + debug.append(" - ").append(c.getClass().getSimpleName()) + .append(": ").append(c.getId()) + .append(" (type: ").append(c.getType()).append(")\n"); + } + + // Verify report constraints were loaded + List reportConstraints = level1.getReportConstraints(); + debug.append("Report constraints on level1: ").append(reportConstraints.size()); + + // Use assertEquals with debug message to ensure we see the output + assertEquals(1, reportConstraints.size(), + "should have exactly one report constraint. Debug info:\n" + debug); + assertAll( + () -> assertNotNull(reportConstraints, "report constraints list should not be null"), + () -> assertFalse(reportConstraints.isEmpty(), "should have at least one report constraint"), + () -> assertEquals(1, reportConstraints.size(), "should have exactly one report constraint"), + () -> assertEquals("level1-report", reportConstraints.get(0).getId(), "constraint should have correct id")); + } + + /** + * Diagnostic test to trace through each step of the constraint loading process. + * This helps identify where report constraints might be lost. + */ + @Test + void testTraceReportConstraintLoading() throws IOException { + StringBuilder trace = new StringBuilder(); + trace.append("=== Tracing Report Constraint Loading ===\n\n"); + + // Step 1: Parse YAML directly to binding object + trace.append("STEP 1: Parse YAML to binding object\n"); + IBindingContext bindingContext = IBindingContext.newInstance(); + IBoundLoader loader = bindingContext.newBoundLoader(); + URI resourceUri = Paths.get("src/test/resources/content/constraints/meta-constraints/meta-constraints-report.yaml") + .toUri(); + + Object constraintsDocument = loader.load(resourceUri); + trace.append(" Document type: ").append(constraintsDocument.getClass().getName()).append("\n"); + assertEquals(MetaschemaMetaConstraints.class, constraintsDocument.getClass(), + "Should parse to MetaschemaMetaConstraints"); + + MetaschemaMetaConstraints metaConstraints = (MetaschemaMetaConstraints) constraintsDocument; + + // Step 2: Check contexts + trace.append("\nSTEP 2: Check parsed contexts\n"); + List contexts + = metaConstraints.getContexts(); + trace.append(" Number of contexts: ").append(contexts == null ? "null" : contexts.size()).append("\n"); + assertNotNull(contexts, "Contexts should not be null"); + assertFalse(contexts.isEmpty(), "Should have at least one context"); + + // Step 3: Check first context's constraints + trace.append("\nSTEP 3: Check first context's constraints\n"); + gov.nist.secauto.metaschema.databind.model.metaschema.binding.MetapathContext firstContext = contexts.get(0); + trace.append(" First context metapaths: "); + if (firstContext.getMetapaths() != null) { + for (var mp : firstContext.getMetapaths()) { + trace.append(mp.getTarget()).append(" "); + } + } + trace.append("\n"); + + AssemblyConstraints assemblyConstraints = firstContext.getConstraints(); + trace.append(" Constraints object: ").append(assemblyConstraints == null ? "null" : "present").append("\n"); + assertNotNull(assemblyConstraints, "Constraints should not be null"); + + // Step 4: Check rules in constraints + trace.append("\nSTEP 4: Check rules in AssemblyConstraints\n"); + List rules = assemblyConstraints.getRules(); + trace.append(" Number of rules: ").append(rules == null ? "null" : rules.size()).append("\n"); + assertNotNull(rules, "Rules should not be null"); + + if (rules != null && !rules.isEmpty()) { + for (int i = 0; i < rules.size(); i++) { + Object rule = rules.get(i); + trace.append(" Rule ").append(i).append(": "); + if (rule == null) { + trace.append("null\n"); + } else { + trace.append(rule.getClass().getName()).append("\n"); + if (rule instanceof TargetedReportConstraint) { + TargetedReportConstraint report = (TargetedReportConstraint) rule; + trace.append(" ID: ").append(report.getId()).append("\n"); + trace.append(" Test: ").append(report.getTest()).append("\n"); + trace.append(" Target: ").append(report.getTarget()).append("\n"); + } else if (rule instanceof IConstraintBase) { + IConstraintBase constraint = (IConstraintBase) rule; + trace.append(" ID: ").append(constraint.getId()).append("\n"); + } + } + } + } else { + trace.append(" WARNING: No rules found!\n"); + } + + // Step 5: Check if TargetedReportConstraint is in the rules + trace.append("\nSTEP 5: Look for TargetedReportConstraint\n"); + boolean foundReport = false; + for (Object rule : rules) { + if (rule instanceof TargetedReportConstraint) { + foundReport = true; + trace.append(" FOUND TargetedReportConstraint!\n"); + break; + } + } + if (!foundReport) { + trace.append(" NOT FOUND - TargetedReportConstraint missing from rules\n"); + } + + // Output trace for debugging + System.out.println(trace); + + // The assertion that matters + assertEquals(1, rules.size(), "Should have exactly one rule. Trace:\n" + trace); + assertEquals(TargetedReportConstraint.class, rules.get(0).getClass(), + "Rule should be TargetedReportConstraint. Trace:\n" + trace); + } + + /** + * Test that ConstraintBindingSupport.parse() correctly converts + * TargetedReportConstraint to IReportConstraint and adds it to the + * AssemblyConstraintSet. + */ + @Test + void testConstraintBindingSupportParsesReportConstraint() throws IOException { + StringBuilder trace = new StringBuilder(); + trace.append("=== Testing ConstraintBindingSupport.parse() ===\n\n"); + + // Step 1: Parse YAML directly to binding object + IBindingContext bindingContext = IBindingContext.newInstance(); + IBoundLoader loader = bindingContext.newBoundLoader(); + URI resourceUri = Paths.get("src/test/resources/content/constraints/meta-constraints/meta-constraints-report.yaml") + .toUri(); + + MetaschemaMetaConstraints metaConstraints = (MetaschemaMetaConstraints) loader.load(resourceUri); + var contexts = metaConstraints.getContexts(); + assertFalse(contexts.isEmpty(), "Should have at least one context"); + + var firstContext = contexts.get(0); + AssemblyConstraints assemblyConstraints = firstContext.getConstraints(); + assertNotNull(assemblyConstraints, "Should have assembly constraints"); + + trace.append("YAML parsed successfully. Rules count: ").append(assemblyConstraints.getRules().size()).append("\n"); + + // Step 2: Create AssemblyConstraintSet and parse constraints into it + StaticContext staticContext = StaticContext.builder() + .baseUri(resourceUri) + .useWildcardWhenNamespaceNotDefaulted(true) + .build(); + ISource source = ISource.externalSource(staticContext, false); + + AssemblyConstraintSet constraintSet = new AssemblyConstraintSet(source); + + trace.append("Before ConstraintBindingSupport.parse():\n"); + trace.append(" expect constraints: ").append(constraintSet.getExpectConstraints().size()).append("\n"); + trace.append(" report constraints: ").append(constraintSet.getReportConstraints().size()).append("\n"); + trace.append(" all constraints: ").append(constraintSet.getConstraints().size()).append("\n"); + + // Parse using ConstraintBindingSupport + ConstraintBindingSupport.parse(constraintSet, assemblyConstraints, source); + + trace.append("\nAfter ConstraintBindingSupport.parse():\n"); + trace.append(" expect constraints: ").append(constraintSet.getExpectConstraints().size()).append("\n"); + trace.append(" report constraints: ").append(constraintSet.getReportConstraints().size()).append("\n"); + trace.append(" all constraints: ").append(constraintSet.getConstraints().size()).append("\n"); + + // List all constraints with their types + for (IConstraint c : constraintSet.getConstraints()) { + trace.append(" - ").append(c.getClass().getSimpleName()).append(": id=").append(c.getId()).append("\n"); + } + + // Report constraints specifically + trace.append("\nReport constraints details:\n"); + for (IReportConstraint rc : constraintSet.getReportConstraints()) { + trace.append(" - id=").append(rc.getId()) + .append(", test=").append(rc.getTest().getPath()) + .append(", target=").append(rc.getTarget().getPath()).append("\n"); + } + + System.out.println(trace); + + // Assertions + assertEquals(1, constraintSet.getReportConstraints().size(), + "Should have exactly one report constraint. Trace:\n" + trace); + assertEquals("level1-report", constraintSet.getReportConstraints().get(0).getId(), + "Report constraint should have correct id"); + } + + /** + * Test the full BindingConstraintLoader.load() path to verify report + * constraints are in the returned IConstraintSet. + */ + @Test + void testBindingConstraintLoaderReturnsReportConstraints() throws MetaschemaException, IOException { + StringBuilder trace = new StringBuilder(); + trace.append("=== Testing BindingConstraintLoader.load() ===\n\n"); + + IBindingContext bindingContext = IBindingContext.newInstance(); + IConstraintLoader loader = new BindingConstraintLoader(bindingContext); + + List constraintSets = loader.load( + Paths.get("src/test/resources/content/constraints/meta-constraints/meta-constraints-report.yaml")); + + trace.append("Constraint sets loaded: ").append(constraintSets.size()).append("\n\n"); + assertEquals(1, constraintSets.size(), "Should have exactly one constraint set"); + + IConstraintSet constraintSet = constraintSets.get(0); + trace.append("Constraint set type: ").append(constraintSet.getClass().getName()).append("\n"); + + // For MetaConstraintSet, we need to check the contexts + if (constraintSet instanceof gov.nist.secauto.metaschema.core.model.constraint.MetaConstraintSet) { + trace.append("MetaConstraintSet detected - need to check contexts\n"); + // Can't directly access contexts, but we can test the behavior + } + + // Now test applying constraints to see if they work + Path compileDir = Paths.get("target/generated-test-modules/meta-constraints-report-trace/"); + Files.createDirectories(compileDir); + + bindingContext = IBindingContext.builder() + .compilePath(compileDir) + .constraintSet(constraintSets) + .build(); + + IBindingMetaschemaModule metaschema = bindingContext.loadMetaschema( + Paths.get("src/test/resources/content/constraints/meta-constraints/metaschema.xml")); + + trace.append("\nBefore registerModule:\n"); + IAssemblyDefinition level1Before + = metaschema.getAssemblyDefinitionByName(IEnhancedQName.of(NS, "level1").getIndexPosition()); + if (level1Before != null) { + trace.append(" level1 report constraints: ").append(level1Before.getReportConstraints().size()).append("\n"); + trace.append(" level1 expect constraints: ").append(level1Before.getExpectConstraints().size()).append("\n"); + } + + IBoundModule module = bindingContext.registerModule(metaschema); + + trace.append("\nAfter registerModule:\n"); + IAssemblyDefinition level1 = module.getAssemblyDefinitionByName(IEnhancedQName.of(NS, "level1").getIndexPosition()); + assertNotNull(level1, "level1 should exist"); + + trace.append(" level1 all constraints: ").append(level1.getConstraints().size()).append("\n"); + trace.append(" level1 expect constraints: ").append(level1.getExpectConstraints().size()).append("\n"); + trace.append(" level1 report constraints: ").append(level1.getReportConstraints().size()).append("\n"); + + // List all constraints + for (IConstraint c : level1.getConstraints()) { + trace.append(" - ").append(c.getClass().getSimpleName()) + .append(": id=").append(c.getId()) + .append(", type=").append(c.getType()).append("\n"); + } + + System.out.println(trace); + + assertEquals(1, level1.getReportConstraints().size(), + "Should have exactly one report constraint on level1. Trace:\n" + trace); + } + + /** + * Test that expect constraints are preserved through the registerModule + * process. This verifies that constraints defined in the metaschema XML are + * properly embedded in generated code and available on the bound module. + */ + @Test + void testExpectConstraintPreservedThroughRegisterModule() throws MetaschemaException, IOException { + IBindingContext bindingContext = IBindingContext.newInstance(); + IConstraintLoader loader = new BindingConstraintLoader(bindingContext); + + List constraints = loader.load( + Paths.get("src/test/resources/content/constraints/meta-constraints/meta-constraints-valid.yaml")); + + Path compileDir = Paths.get("target/generated-test-modules/meta-constraints-trace-expect/"); + Files.createDirectories(compileDir); + + bindingContext = IBindingContext.builder() + .compilePath(compileDir) + .constraintSet(constraints) + .build(); + + IBindingMetaschemaModule metaschema = bindingContext.loadMetaschema( + Paths.get("src/test/resources/content/constraints/meta-constraints/metaschema.xml")); + + // Verify expect constraint exists before registerModule + IAssemblyDefinition level1Before + = metaschema.getAssemblyDefinitionByName(IEnhancedQName.of(NS, "level1").getIndexPosition()); + assertNotNull(level1Before, "level1 definition should exist before registerModule"); + assertEquals(1, level1Before.getExpectConstraints().size(), + "level1 should have 1 expect constraint before registerModule"); + + // Register the module + IBoundModule module = bindingContext.registerModule(metaschema); + + // Verify expect constraint exists after registerModule + IAssemblyDefinition level1After + = module.getAssemblyDefinitionByName(IEnhancedQName.of(NS, "level1").getIndexPosition()); + assertNotNull(level1After, "level1 definition should exist after registerModule"); + assertEquals(1, level1After.getExpectConstraints().size(), + "level1 should have 1 expect constraint after registerModule"); + } + + /** + * Parallel comparison test: Trace both expect and report constraints through + * the exact same loading path to identify where they diverge. + */ + @Test + void testCompareExpectVsReportYamlParsing() throws IOException { + StringBuilder comparison = new StringBuilder(); + comparison.append("=== Comparing Expect vs Report YAML Parsing ===\n\n"); + + IBindingContext bindingContext = IBindingContext.newInstance(); + IBoundLoader loader = bindingContext.newBoundLoader(); + + // Load both YAML files + URI expectUri = Paths.get("src/test/resources/content/constraints/meta-constraints/meta-constraints-valid.yaml") + .toUri(); + URI reportUri = Paths.get("src/test/resources/content/constraints/meta-constraints/meta-constraints-report.yaml") + .toUri(); + + Object expectDoc = loader.load(expectUri); + Object reportDoc = loader.load(reportUri); + + comparison.append("Document types:\n"); + comparison.append(" Expect: ").append(expectDoc.getClass().getName()).append("\n"); + comparison.append(" Report: ").append(reportDoc.getClass().getName()).append("\n\n"); + + assertEquals(expectDoc.getClass(), reportDoc.getClass(), + "Both should parse to same document type"); + + MetaschemaMetaConstraints expectConstraints = (MetaschemaMetaConstraints) expectDoc; + MetaschemaMetaConstraints reportConstraints = (MetaschemaMetaConstraints) reportDoc; + + // Compare contexts + comparison.append("Contexts:\n"); + comparison.append(" Expect contexts: ").append( + expectConstraints.getContexts() == null ? "null" : expectConstraints.getContexts().size()).append("\n"); + comparison.append(" Report contexts: ").append( + reportConstraints.getContexts() == null ? "null" : reportConstraints.getContexts().size()).append("\n\n"); + + assertNotNull(expectConstraints.getContexts(), "Expect contexts should not be null"); + assertNotNull(reportConstraints.getContexts(), "Report contexts should not be null"); + assertFalse(expectConstraints.getContexts().isEmpty(), "Expect contexts should not be empty"); + assertFalse(reportConstraints.getContexts().isEmpty(), "Report contexts should not be empty"); + + // Compare first context's constraints + var expectFirstContext = expectConstraints.getContexts().get(0); + var reportFirstContext = reportConstraints.getContexts().get(0); + + comparison.append("First context metapaths:\n"); + comparison.append(" Expect: "); + for (var mp : expectFirstContext.getMetapaths()) { + comparison.append(mp.getTarget()).append(" "); + } + comparison.append("\n Report: "); + for (var mp : reportFirstContext.getMetapaths()) { + comparison.append(mp.getTarget()).append(" "); + } + comparison.append("\n\n"); + + // Compare rules + AssemblyConstraints expectAssemblyConstraints = expectFirstContext.getConstraints(); + AssemblyConstraints reportAssemblyConstraints = reportFirstContext.getConstraints(); + + comparison.append("AssemblyConstraints:\n"); + comparison.append(" Expect constraints: ").append( + expectAssemblyConstraints == null ? "null" : "present").append("\n"); + comparison.append(" Report constraints: ").append( + reportAssemblyConstraints == null ? "null" : "present").append("\n\n"); + + assertNotNull(expectAssemblyConstraints, "Expect AssemblyConstraints should not be null"); + assertNotNull(reportAssemblyConstraints, "Report AssemblyConstraints should not be null"); + + // Compare rules lists + List expectRules = expectAssemblyConstraints.getRules(); + List reportRules = reportAssemblyConstraints.getRules(); + + comparison.append("Rules:\n"); + comparison.append(" Expect rules count: ").append(expectRules == null ? "null" : expectRules.size()).append("\n"); + comparison.append(" Report rules count: ").append(reportRules == null ? "null" : reportRules.size()).append("\n"); + + if (expectRules != null && !expectRules.isEmpty()) { + comparison.append(" Expect rules types:\n"); + for (int i = 0; i < expectRules.size(); i++) { + Object rule = expectRules.get(i); + comparison.append(" [").append(i).append("] ").append( + rule == null ? "null" : rule.getClass().getSimpleName()).append("\n"); + } + } + + if (reportRules != null && !reportRules.isEmpty()) { + comparison.append(" Report rules types:\n"); + for (int i = 0; i < reportRules.size(); i++) { + Object rule = reportRules.get(i); + comparison.append(" [").append(i).append("] ").append( + rule == null ? "null" : rule.getClass().getSimpleName()).append("\n"); + } + } else { + comparison.append(" Report rules: EMPTY OR NULL - THIS IS THE PROBLEM!\n"); + } + + // Output comparison for debugging + System.out.println(comparison); + + // Key assertions + assertNotNull(expectRules, "Expect rules should not be null"); + assertNotNull(reportRules, "Report rules should not be null. Comparison:\n" + comparison); + assertFalse(expectRules.isEmpty(), "Expect rules should not be empty"); + assertFalse(reportRules.isEmpty(), + "Report rules should not be empty - YAML parsing failed! Comparison:\n" + comparison); + assertEquals(1, reportRules.size(), + "Should have exactly one report rule. Comparison:\n" + comparison); + assertEquals(TargetedReportConstraint.class, reportRules.get(0).getClass(), + "Rule should be TargetedReportConstraint. Comparison:\n" + comparison); + } } diff --git a/databind/src/test/resources/content/constraints/meta-constraints/meta-constraints-report.yaml b/databind/src/test/resources/content/constraints/meta-constraints/meta-constraints-report.yaml new file mode 100644 index 000000000..3c16abaa9 --- /dev/null +++ b/databind/src/test/resources/content/constraints/meta-constraints/meta-constraints-report.yaml @@ -0,0 +1,16 @@ +# yaml-language-server: $schema=../../../../../../../../../../../metaschema-cli/target/generated-resources/metaschema/schema/json/metaschema-model_schema.json +metaschema-meta-constraints: + contexts: + - metapaths: + - target: "/level1" + constraints: + lets: + - var: level + expression: 'level1' + rules: + - object-type: report + id: level1-report + target: . + test: exists(@deprecated) + message: This element is deprecated. + level: INFORMATIONAL From b7c891e57c4cc61586f27d79f4751e700c7d7b94 Mon Sep 17 00:00:00 2001 From: David Waltermire Date: Mon, 29 Dec 2025 01:15:46 -0500 Subject: [PATCH 8/9] docs: update PRD with completion status and move to completed --- CLAUDE.md | 2 +- .../implementation-plan.md | 52 +++++++++---------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d181684b3..160bf7785 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -294,12 +294,12 @@ PRDs are stored in `PRDs/-/` with: | PRD | Description | Status | |-----|-------------|--------| | `PRDs/20251206-build-cleanup/` | Build warnings and deprecation removal | In Progress | -| `PRDs/20251228-targeted-report-constraint/` | Add constraint processing support for TargetedReportConstraint (issue #592) | In Progress | ### Completed PRDs | PRD | Description | Completed | |-----|-------------|-----------| +| `PRDs/20251228-targeted-report-constraint/` | Add constraint processing support for TargetedReportConstraint (issue #592) | 2025-12-29 | | `PRDs/20251228-validation-errors/` | Validation error message improvements (#595, #596, #205) | 2025-12-28 | | `PRDs/20251224-codegen-quality/` | Code generator Javadoc and quality improvements | 2025-12-28 | | `PRDs/20251217-cli-processor-refactor/` | CLI processor refactoring (issue #252) | 2025-12-21 | diff --git a/PRDs/20251228-targeted-report-constraint/implementation-plan.md b/PRDs/20251228-targeted-report-constraint/implementation-plan.md index f7fd2a1e7..998dad11b 100644 --- a/PRDs/20251228-targeted-report-constraint/implementation-plan.md +++ b/PRDs/20251228-targeted-report-constraint/implementation-plan.md @@ -28,12 +28,12 @@ This document details the implementation for adding constraint processing suppor | Attribute | Value | |-----------|-------| -| **Files Changed** | ~15 | +| **Files Changed** | 25 | | **Risk Level** | Medium | | **Dependencies** | None | | **Target Branch** | develop | -| **Status** | Pending | -| **Pull Request** | TBD | +| **Status** | Complete | +| **Pull Request** | [#598](https://github.com/metaschema-framework/metaschema-java/pull/598) | #### Files to Create @@ -183,26 +183,26 @@ Update Claude Code skills to document the new constraint type: #### Acceptance Criteria -- [ ] `IReportConstraint` interface created extending `IConfigurableMessageConstraint` -- [ ] `IReportConstraint.getTest()` returns the test Metapath expression -- [ ] `DefaultReportConstraint` implementation with working builder -- [ ] `IConstraintVisitor.visitReportConstraint()` method added -- [ ] `IValueConstrained` has `getReportConstraints()` and `addConstraint(IReportConstraint)` -- [ ] `ValueConstraintSet` stores and retrieves report constraints -- [ ] `AbstractTargetedConstraints.applyTo()` forwards report constraints -- [ ] `ConstraintBindingSupport.parse()` handles `TargetedReportConstraint` in all overloads -- [ ] `ConstraintBindingSupport.newReport()` factory method creates valid constraints -- [ ] `DefaultConstraintValidator.validateReport()` generates findings when test is TRUE -- [ ] Report constraints default to INFORMATIONAL level -- [ ] Report constraints at ERROR/CRITICAL cause validation failures (Kind.FAIL) -- [ ] `metaschema-constraints-authoring.md` skill updated with `report` constraint -- [ ] `metaschema-java-library.md` skill updated if applicable -- [ ] Unit tests for `IReportConstraint` builder and behavior -- [ ] Unit tests for constraint loading -- [ ] Unit tests for constraint validation -- [ ] All new code has complete Javadoc -- [ ] All tests pass: `mvn test` -- [ ] Build succeeds: `mvn clean install -PCI -Prelease` +- [x] `IReportConstraint` interface created extending `IConfigurableMessageConstraint` +- [x] `IReportConstraint.getTest()` returns the test Metapath expression +- [x] `DefaultReportConstraint` implementation with working builder +- [x] `IConstraintVisitor.visitReportConstraint()` method added +- [x] `IValueConstrained` has `getReportConstraints()` and `addConstraint(IReportConstraint)` +- [x] `ValueConstraintSet` stores and retrieves report constraints +- [x] `AbstractTargetedConstraints.applyTo()` forwards report constraints +- [x] `ConstraintBindingSupport.parse()` handles `TargetedReportConstraint` in all overloads +- [x] `ConstraintBindingSupport.newReport()` factory method creates valid constraints +- [x] `DefaultConstraintValidator.validateReport()` generates findings when test is TRUE +- [x] Report constraints default to INFORMATIONAL level +- [x] Report constraints at ERROR/CRITICAL cause validation failures (Kind.FAIL) +- [x] `metaschema-constraints-authoring.md` skill updated with `report` constraint +- [x] `metaschema-java-library.md` skill updated if applicable +- [x] Unit tests for `IReportConstraint` builder and behavior +- [x] Unit tests for constraint loading +- [x] Unit tests for constraint validation +- [x] All new code has complete Javadoc +- [x] All tests pass: `mvn test` +- [x] Build succeeds: `mvn clean install -PCI -Prelease` --- @@ -245,10 +245,10 @@ IConstraint | PR | Description | Files | Risk | Dependencies | Status | |----|-------------|-------|------|--------------|--------| -| 1 | Add IReportConstraint interface and full implementation | ~15 | Medium | None | Pending | +| [#598](https://github.com/metaschema-framework/metaschema-java/pull/598) | Add IReportConstraint interface and full implementation | 25 | Medium | None | Complete | -**Total Estimated PRs**: 1 -**Total Estimated Files**: ~15 +**Total PRs**: 1 +**Total Files Changed**: 25 --- From 4bcec452422ac29011109ac798af7aca272f6804 Mon Sep 17 00:00:00 2001 From: David Waltermire Date: Mon, 29 Dec 2025 01:25:10 -0500 Subject: [PATCH 9/9] fix: set @Report annotation default level to INFORMATIONAL Per PRD design decision, report constraints should default to INFORMATIONAL level, which differs from expect constraints that default to ERROR. This aligns with IReportConstraint interface documentation and unit tests. --- .../metaschema/databind/model/annotations/Report.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/annotations/Report.java b/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/annotations/Report.java index 98bf9d07c..66db5144a 100644 --- a/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/annotations/Report.java +++ b/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/annotations/Report.java @@ -56,11 +56,14 @@ /** * The significance of a violation of this constraint. + *

    + * The default level for report constraints is {@link Level#INFORMATIONAL}, + * which differs from expect constraints that default to {@link Level#ERROR}. * * @return the level */ @NonNull - Level level() default IConstraint.Level.ERROR; + Level level() default IConstraint.Level.INFORMATIONAL; /** * An optional metapath that points to the target flag or field value that the