diff --git a/artifacts/openlabel-v2/openlabel-v2.owl.ttl b/artifacts/openlabel-v2/openlabel-v2.owl.ttl index 7fbf2b78..6a79767d 100644 --- a/artifacts/openlabel-v2/openlabel-v2.owl.ttl +++ b/artifacts/openlabel-v2/openlabel-v2.owl.ttl @@ -4462,6 +4462,14 @@ openlabel_v2:Odd a owl:Class, owl:onProperty openlabel_v2:illuminationCloudinessValue ; owl:someValuesFrom xsd:string ] openlabel_v2:Odd ) . +[] rdfs:subClassOf [ a owl:Restriction ; + owl:onProperty openlabel_v2:DrivableAreaEdge ; + owl:someValuesFrom xsd:string ] ; + owl:intersectionOf ( [ a owl:Restriction ; + owl:onProperty openlabel_v2:DrivableAreaEdge ; + owl:someValuesFrom [ a rdfs:Datatype ; + owl:oneOf ( "EdgeNone" ) ] ] openlabel_v2:Odd ) . + [] rdfs:subClassOf [ a owl:Restriction ; owl:onProperty openlabel_v2:LongitudinalDownSlope ; owl:someValuesFrom [ a rdfs:Datatype ; @@ -4489,13 +4497,6 @@ openlabel_v2:Odd a owl:Class, owl:onProperty openlabel_v2:particulatesWaterValue ; owl:someValuesFrom xsd:string ] openlabel_v2:Odd ) . -[] rdfs:subClassOf [ a owl:Restriction ; - owl:onProperty openlabel_v2:DrivableAreaEdge ; - owl:someValuesFrom xsd:string ] ; - owl:intersectionOf ( [ a owl:Restriction ; - owl:onProperty openlabel_v2:DrivableAreaEdge ; - owl:someValuesFrom xsd:string ] openlabel_v2:Odd ) . - [] rdfs:subClassOf [ a owl:Restriction ; owl:onProperty openlabel_v2:LongitudinalUpSlope ; owl:someValuesFrom [ a rdfs:Datatype ; diff --git a/artifacts/openlabel-v2/openlabel-v2.shacl.ttl b/artifacts/openlabel-v2/openlabel-v2.shacl.ttl index d28edb9e..d30005ce 100644 --- a/artifacts/openlabel-v2/openlabel-v2.shacl.ttl +++ b/artifacts/openlabel-v2/openlabel-v2.shacl.ttl @@ -1614,32 +1614,32 @@ openlabel_v2:Behaviour a sh:NodeShape ; sh:order 1 ; sh:path openlabel_v2:MotionAccelerate ] ; sh:sparql [ a sh:SPARQLConstraint ; - sh:message "If motionDriveValue is provided, MotionDrive must be true."@en ; + sh:message "If motionAccelerateValue is provided, MotionAccelerate must be true." ; sh:select """SELECT $this WHERE { - OPTIONAL { $this ?flag . } - OPTIONAL { $this ?value . } + OPTIONAL { $this ?flag . } + OPTIONAL { $this ?value . } FILTER ( - ( !BOUND(?flag) || str(?flag) != "true" ) && + ( !BOUND(?flag) || ?flag != true ) && BOUND(?value) ) }""" ], [ a sh:SPARQLConstraint ; - sh:message "If motionDecelerateValue is provided, MotionDecelerate must be true."@en ; + sh:message "If motionDecelerateValue is provided, MotionDecelerate must be true." ; sh:select """SELECT $this WHERE { OPTIONAL { $this ?flag . } OPTIONAL { $this ?value . } FILTER ( - ( !BOUND(?flag) || str(?flag) != "true" ) && + ( !BOUND(?flag) || ?flag != true ) && BOUND(?value) ) }""" ], [ a sh:SPARQLConstraint ; - sh:message "If motionAccelerateValue is provided, MotionAccelerate must be true."@en ; + sh:message "If motionDriveValue is provided, MotionDrive must be true." ; sh:select """SELECT $this WHERE { - OPTIONAL { $this ?flag . } - OPTIONAL { $this ?value . } + OPTIONAL { $this ?flag . } + OPTIONAL { $this ?value . } FILTER ( - ( !BOUND(?flag) || str(?flag) != "true" ) && + ( !BOUND(?flag) || ?flag != true ) && BOUND(?value) ) }""" ] ; @@ -2073,169 +2073,169 @@ openlabel_v2:Odd a sh:NodeShape ; sh:order 15 ; sh:path openlabel_v2:IlluminationArtificial ] ; sh:sparql [ a sh:SPARQLConstraint ; - sh:message "If weatherRainValue is provided, WeatherRain must be true."@en ; + sh:message "If subjectVehicleSpeedValue is provided, SubjectVehicleSpeed must be true." ; sh:select """SELECT $this WHERE { - OPTIONAL { $this ?flag . } - OPTIONAL { $this ?value . } + OPTIONAL { $this ?flag . } + OPTIONAL { $this ?value . } FILTER ( - ( !BOUND(?flag) || str(?flag) != "true" ) && + ( !BOUND(?flag) || ?flag != true ) && BOUND(?value) ) }""" ], [ a sh:SPARQLConstraint ; - sh:message "If longitudinalUpSlopeValue is provided, LongitudinalUpSlope must be true."@en ; + sh:message "If daySunElevationValue is provided, DaySunElevation must be true." ; sh:select """SELECT $this WHERE { - OPTIONAL { $this ?flag . } - OPTIONAL { $this ?value . } + OPTIONAL { $this ?flag . } + OPTIONAL { $this ?value . } FILTER ( - ( !BOUND(?flag) || str(?flag) != "true" ) && + ( !BOUND(?flag) || ?flag != true ) && BOUND(?value) ) }""" ], [ a sh:SPARQLConstraint ; - sh:message "EdgeNone is mutually exclusive with other drivable area edge types. If DrivableAreaEdge includes EdgeNone, no other edge values are allowed."@en ; - sh:select """SELECT $this WHERE { - $this . - $this ?other . - FILTER (?other != ) -}""" ], - [ a sh:SPARQLConstraint ; - sh:message "If horizontalCurvesValue is provided, HorizontalCurves must be true."@en ; + sh:message "If longitudinalUpSlopeValue is provided, LongitudinalUpSlope must be true." ; sh:select """SELECT $this WHERE { - OPTIONAL { $this ?flag . } - OPTIONAL { $this ?value . } + OPTIONAL { $this ?flag . } + OPTIONAL { $this ?value . } FILTER ( - ( !BOUND(?flag) || str(?flag) != "true" ) && + ( !BOUND(?flag) || ?flag != true ) && BOUND(?value) ) }""" ], [ a sh:SPARQLConstraint ; - sh:message "If trafficAgentTypeValue is provided, TrafficAgentType must be true."@en ; + sh:message "If trafficAgentTypeValue is provided, TrafficAgentType must be true." ; sh:select """SELECT $this WHERE { OPTIONAL { $this ?flag . } OPTIONAL { $this ?value . } FILTER ( - ( !BOUND(?flag) || str(?flag) != "true" ) && + ( !BOUND(?flag) || ?flag != true ) && BOUND(?value) ) }""" ], [ a sh:SPARQLConstraint ; - sh:message "If weatherSnowValue is provided, WeatherSnow must be true."@en ; + sh:message "If horizontalCurvesValue is provided, HorizontalCurves must be true." ; sh:select """SELECT $this WHERE { - OPTIONAL { $this ?flag . } - OPTIONAL { $this ?value . } + OPTIONAL { $this ?flag . } + OPTIONAL { $this ?value . } FILTER ( - ( !BOUND(?flag) || str(?flag) != "true" ) && + ( !BOUND(?flag) || ?flag != true ) && BOUND(?value) ) }""" ], [ a sh:SPARQLConstraint ; - sh:message "If daySunElevationValue is provided, DaySunElevation must be true."@en ; + sh:message "If trafficVolumeValue is provided, TrafficVolume must be true." ; sh:select """SELECT $this WHERE { - OPTIONAL { $this ?flag . } - OPTIONAL { $this ?value . } + OPTIONAL { $this ?flag . } + OPTIONAL { $this ?value . } FILTER ( - ( !BOUND(?flag) || str(?flag) != "true" ) && + ( !BOUND(?flag) || ?flag != true ) && BOUND(?value) ) }""" ], [ a sh:SPARQLConstraint ; - sh:message "If laneSpecificationLaneCountValue is provided, LaneSpecificationLaneCount must be true."@en ; + sh:message "EdgeNone is mutually exclusive with other drivable area edge types. If DrivableAreaEdge includes EdgeNone, no other edge values are allowed." ; sh:select """SELECT $this WHERE { - OPTIONAL { $this ?flag . } - OPTIONAL { $this ?value . } + $this . + $this ?other . + FILTER (?other != ) +}""" ], + [ a sh:SPARQLConstraint ; + sh:message "If laneSpecificationDimensionsValue is provided, LaneSpecificationDimensions must be true." ; + sh:select """SELECT $this WHERE { + OPTIONAL { $this ?flag . } + OPTIONAL { $this ?value . } FILTER ( - ( !BOUND(?flag) || str(?flag) != "true" ) && + ( !BOUND(?flag) || ?flag != true ) && BOUND(?value) ) }""" ], [ a sh:SPARQLConstraint ; - sh:message "If subjectVehicleSpeedValue is provided, SubjectVehicleSpeed must be true."@en ; + sh:message "If particulatesWaterValue is provided, ParticulatesWater must be true." ; sh:select """SELECT $this WHERE { - OPTIONAL { $this ?flag . } - OPTIONAL { $this ?value . } + OPTIONAL { $this ?flag . } + OPTIONAL { $this ?value . } FILTER ( - ( !BOUND(?flag) || str(?flag) != "true" ) && + ( !BOUND(?flag) || ?flag != true ) && BOUND(?value) ) }""" ], [ a sh:SPARQLConstraint ; - sh:message "If particulatesWaterValue is provided, ParticulatesWater must be true."@en ; + sh:message "If weatherRainValue is provided, WeatherRain must be true." ; sh:select """SELECT $this WHERE { - OPTIONAL { $this ?flag . } - OPTIONAL { $this ?value . } + OPTIONAL { $this ?flag . } + OPTIONAL { $this ?value . } FILTER ( - ( !BOUND(?flag) || str(?flag) != "true" ) && + ( !BOUND(?flag) || ?flag != true ) && BOUND(?value) ) }""" ], [ a sh:SPARQLConstraint ; - sh:message "If weatherWindValue is provided, WeatherWind must be true."@en ; + sh:message "If weatherSnowValue is provided, WeatherSnow must be true." ; sh:select """SELECT $this WHERE { - OPTIONAL { $this ?flag . } - OPTIONAL { $this ?value . } + OPTIONAL { $this ?flag . } + OPTIONAL { $this ?value . } FILTER ( - ( !BOUND(?flag) || str(?flag) != "true" ) && + ( !BOUND(?flag) || ?flag != true ) && BOUND(?value) ) }""" ], [ a sh:SPARQLConstraint ; - sh:message "If longitudinalDownSlopeValue is provided, LongitudinalDownSlope must be true."@en ; + sh:message "If trafficFlowRateValue is provided, TrafficFlowRate must be true." ; sh:select """SELECT $this WHERE { - OPTIONAL { $this ?flag . } - OPTIONAL { $this ?value . } + OPTIONAL { $this ?flag . } + OPTIONAL { $this ?value . } FILTER ( - ( !BOUND(?flag) || str(?flag) != "true" ) && + ( !BOUND(?flag) || ?flag != true ) && BOUND(?value) ) }""" ], [ a sh:SPARQLConstraint ; - sh:message "If trafficAgentDensityValue is provided, TrafficAgentDensity must be true."@en ; + sh:message "If weatherWindValue is provided, WeatherWind must be true." ; sh:select """SELECT $this WHERE { - OPTIONAL { $this ?flag . } - OPTIONAL { $this ?value . } + OPTIONAL { $this ?flag . } + OPTIONAL { $this ?value . } FILTER ( - ( !BOUND(?flag) || str(?flag) != "true" ) && + ( !BOUND(?flag) || ?flag != true ) && BOUND(?value) ) }""" ], [ a sh:SPARQLConstraint ; - sh:message "If laneSpecificationDimensionsValue is provided, LaneSpecificationDimensions must be true."@en ; + sh:message "If laneSpecificationLaneCountValue is provided, LaneSpecificationLaneCount must be true." ; sh:select """SELECT $this WHERE { - OPTIONAL { $this ?flag . } - OPTIONAL { $this ?value . } + OPTIONAL { $this ?flag . } + OPTIONAL { $this ?value . } FILTER ( - ( !BOUND(?flag) || str(?flag) != "true" ) && + ( !BOUND(?flag) || ?flag != true ) && BOUND(?value) ) }""" ], [ a sh:SPARQLConstraint ; - sh:message "If trafficFlowRateValue is provided, TrafficFlowRate must be true."@en ; + sh:message "If illuminationCloudinessValue is provided, IlluminationCloudiness must be true." ; sh:select """SELECT $this WHERE { - OPTIONAL { $this ?flag . } - OPTIONAL { $this ?value . } + OPTIONAL { $this ?flag . } + OPTIONAL { $this ?value . } FILTER ( - ( !BOUND(?flag) || str(?flag) != "true" ) && + ( !BOUND(?flag) || ?flag != true ) && BOUND(?value) ) }""" ], [ a sh:SPARQLConstraint ; - sh:message "If trafficVolumeValue is provided, TrafficVolume must be true."@en ; + sh:message "If longitudinalDownSlopeValue is provided, LongitudinalDownSlope must be true." ; sh:select """SELECT $this WHERE { - OPTIONAL { $this ?flag . } - OPTIONAL { $this ?value . } + OPTIONAL { $this ?flag . } + OPTIONAL { $this ?value . } FILTER ( - ( !BOUND(?flag) || str(?flag) != "true" ) && + ( !BOUND(?flag) || ?flag != true ) && BOUND(?value) ) }""" ], [ a sh:SPARQLConstraint ; - sh:message "If illuminationCloudinessValue is provided, IlluminationCloudiness must be true."@en ; + sh:message "If trafficAgentDensityValue is provided, TrafficAgentDensity must be true." ; sh:select """SELECT $this WHERE { - OPTIONAL { $this ?flag . } - OPTIONAL { $this ?value . } + OPTIONAL { $this ?flag . } + OPTIONAL { $this ?value . } FILTER ( - ( !BOUND(?flag) || str(?flag) != "true" ) && + ( !BOUND(?flag) || ?flag != true ) && BOUND(?value) ) }""" ] ; diff --git a/src/tools/utils/registry_resolver.py b/src/tools/utils/registry_resolver.py index b97a1e70..b5b8aa5d 100644 --- a/src/tools/utils/registry_resolver.py +++ b/src/tools/utils/registry_resolver.py @@ -799,7 +799,7 @@ def add_temporary_test_entries( # File is outside repo, use absolute path rel_path = file_path - test_id = f"temporary:{domain}:file{i:03d}" + test_id = f"temporary:{domain}:{test_type}:file{i:03d}" self._catalog[test_id] = { "path": str(rel_path), "domain": domain, @@ -838,10 +838,20 @@ def create_temporary_domain(self, paths: List[Path | str]) -> Optional[str]: ).hexdigest()[:8] temp_domain = f"{TEMP_DOMAIN_PREFIX}{path_hash}" - # Add temporary entries to catalog - self.add_temporary_test_entries( - temp_domain, unique_file_paths, test_type="valid" - ) + # Add temporary entries to catalog, classifying each file by the name of + # its parent directory. This lets externally-supplied negative fixtures be + # exercised by `check-failing-tests` in data-paths mode: files under an + # ``invalid`` directory are registered as ``invalid`` test-data (matching + # OMB's own ``tests/data/{domain}/{valid,invalid}/`` convention); every + # other file is registered as ``valid``. + invalid_files = [p for p in unique_file_paths if p.parent.name == "invalid"] + valid_files = [p for p in unique_file_paths if p.parent.name != "invalid"] + if valid_files: + self.add_temporary_test_entries(temp_domain, valid_files, test_type="valid") + if invalid_files: + self.add_temporary_test_entries( + temp_domain, invalid_files, test_type="invalid" + ) print( f"📋 Created temporary domain '{temp_domain}' with {len(unique_file_paths)} file(s)", diff --git a/src/tools/validators/validation_suite.py b/src/tools/validators/validation_suite.py index cae58d4f..935f16ce 100644 --- a/src/tools/validators/validation_suite.py +++ b/src/tools/validators/validation_suite.py @@ -84,9 +84,12 @@ - In Data Path Mode: Discovers schemas from --artifacts directories. --run check-failing-tests - (Catalog/Domain Mode only) Executes "Negative Tests" - data files expected to fail validation. - Verifies that they fail with the specific error code/message defined in .expected files. + Verifies that they fail with the specific error code/message defined in + .expected files. Works in Catalog/Domain Mode and, for externally-supplied + fixtures, in Data Path Mode (files under an `invalid/` directory are treated + as negative tests). Pass --update-expected to (re)record each .expected + snapshot from the live validation report instead of comparing. EXAMPLES: ========= @@ -348,6 +351,7 @@ def check_failing_tests_all( resolver: RegistryResolver = None, inference_mode: str = "rdfs", allow_online: bool = True, + update_expected: bool = False, ) -> int: """ Run failing test cases from tests/data/{domain}/invalid/ directories. @@ -360,6 +364,8 @@ def check_failing_tests_all( resolver: Optional pre-configured RegistryResolver (with temporary entries) inference_mode: Inference mode for SHACL validation (rdfs|owlrl|none|both) allow_online: If True, attempt HTTP resolution for unresolved IRIs + update_expected: If True, (re)record each `.expected` snapshot from the + live validation report instead of comparing against it. Returns: 0 on success, non-zero on failure @@ -407,7 +413,7 @@ def check_failing_tests_all( ".expected" ) - if not expected_output_path.exists(): + if not expected_output_path.exists() and not update_expected: expected_path_display = normalize_path_for_display( expected_output_path, root_dir ) @@ -418,8 +424,6 @@ def check_failing_tests_all( ) return 1 - expected_output = expected_output_path.read_text(encoding="utf-8").strip() - print(f"🔍 Running failing test: {test_path}", flush=True) # Validate single file (fixtures/schemas resolved via catalog) @@ -429,6 +433,18 @@ def check_failing_tests_all( print("\n", flush=True) if result.return_code == 210: + if update_expected: + expected_output_path.write_text(output, encoding="utf-8") + print( + "📝 Recorded expected snapshot: " + f"{normalize_path_for_display(expected_output_path, root_dir)}", + flush=True, + ) + continue + + expected_output = expected_output_path.read_text( + encoding="utf-8" + ).strip() output_norm = normalize_text(output) expected_norm = normalize_text(expected_output) @@ -600,6 +616,15 @@ def main(): help="Validation mode to run (default: all)", ) + mode_group.add_argument( + "--update-expected", + action="store_true", + default=False, + help="check-failing-tests only: (re)record each negative test's .expected " + "snapshot from the live validation report instead of comparing. Use after " + "an intentional schema or OMB/pyshacl change, then review the diff.", + ) + target_group.add_argument( "--domain", type=str, @@ -825,14 +850,17 @@ def main(): _allow_online = args.allow_online _per_resource = args.per_resource - # Artifact coherence and failing tests require standard domain structure - if data_paths and args.run in ["check-artifact-coherence", "check-failing-tests"]: + # Artifact coherence requires standard catalog structure with domain + # artifacts, so it stays unsupported in data-paths mode. check-failing-tests + # DOES work in data-paths mode: negative fixtures supplied under an `invalid/` + # directory are registered as invalid test-data by create_temporary_domain. + if data_paths and args.run == "check-artifact-coherence": print( f"❌ Error: {args.run} is not supported in data-paths mode.", file=sys.stderr, ) print( - " These checks require catalog structure with domain artifacts.", + " This check requires catalog structure with domain artifacts.", file=sys.stderr, ) sys.exit(1) @@ -873,6 +901,7 @@ def main(): catalog_resolver, _inference_mode, allow_online=_allow_online, + update_expected=args.update_expected, ), ) ],