diff --git a/AGENTS.md b/AGENTS.md
index 13da636..74a8f4c 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -84,6 +84,9 @@
bumping the range.
- Schema.org grammar checks are deliberately permissive, accepting URL/text literals
for all properties.
+- Schema.org grammar generation emits warning-level property-domain checks from
+ `domainIncludes`, with standard subclass closure and multi-type union semantics;
+ grammar and subclass ontology artifacts are refreshed from the same source graph.
- Schema.org grammar issue extraction is subclass-aware for range checks:
warnings like `Schema.org range check: priceSpecification.` are suppressed
when the value node type is a valid schema.org subclass of the expected
diff --git a/README.md b/README.md
index 694816c..3e9a9ab 100644
--- a/README.md
+++ b/README.md
@@ -219,7 +219,7 @@ Behavior notes:
## Validation
-SHACL validation utilities and generated Google Search Gallery shapes are included. When a feature includes both container types (for example `ItemList`, `BreadcrumbList`, `QAPage`, `FAQPage`, `Quiz`, `ProfilePage`, `Product`, `Recipe`, `Course`, `Review`) and their contained types (`ListItem`, `Question`, `Answer`, `Comment`, `Offer`, `AggregateOffer`, `HowToStep`, `Person`, `Organization`, `Rating`, `AggregateRating`, `Review`, `ItemList`), the generator scopes the contained constraints under the container properties to avoid enforcing them on unrelated nodes. For Product snippets, `offers` is scoped as `Offer` or `AggregateOffer`, matching Google requirements. The generator also captures "one of" requirements expressed in prose lists and emits `sh:or` constraints so any listed property satisfies the requirement. For tables with explicit `Option A` / `Option B` branches, the generator emits branch-level alternatives (a branch can require multiple properties), and it ignores enum URL literals when extracting property alternatives. Schema.org grammar checks are intentionally permissive and accept URL/text literals for all properties.
+SHACL validation utilities and generated Google Search Gallery shapes are included. When a feature includes both container types (for example `ItemList`, `BreadcrumbList`, `QAPage`, `FAQPage`, `Quiz`, `ProfilePage`, `Product`, `Recipe`, `Course`, `Review`) and their contained types (`ListItem`, `Question`, `Answer`, `Comment`, `Offer`, `AggregateOffer`, `HowToStep`, `Person`, `Organization`, `Rating`, `AggregateRating`, `Review`, `ItemList`), the generator scopes the contained constraints under the container properties to avoid enforcing them on unrelated nodes. For Product snippets, `offers` is scoped as `Offer` or `AggregateOffer`, matching Google requirements. The generator also captures "one of" requirements expressed in prose lists and emits `sh:or` constraints so any listed property satisfies the requirement. For tables with explicit `Option A` / `Option B` branches, the generator emits branch-level alternatives (a branch can require multiple properties), and it ignores enum URL literals when extracting property alternatives. Schema.org grammar checks warn when a known property is used on an incompatible Schema.org type, including subclass and multi-type handling. Range checks remain intentionally permissive and accept URL/text literals for all properties.
The generator also recognizes explicit fallback wording in required rows (for example, `contentUrl` with supported `url` fallback if `contentUrl` is missing) and emits `sh:or` alternatives instead of hard-requiring only the preferred property.
Recommended-table "choose either ... or ..." alternatives are emitted as warning-level `sh:or` constraints (including scoped/nested shapes; warn only when none of the alternatives is present).
Paragraph-level "one of the following values" lists are treated as value guidance (not property alternatives), and conditional sections phrased as "required when"/"required if" are emitted as warnings instead of unconditional required errors.
diff --git a/docs/validation.md b/docs/validation.md
index 10a2323..6d7e970 100644
--- a/docs/validation.md
+++ b/docs/validation.md
@@ -135,6 +135,12 @@ lists.
`wordlift_sdk.validation.generator` contains the helper scripts used to generate the bundled
SHACL files from the schema.org grammar and Google Search Gallery feature pages.
+Schema.org grammar generation also emits property-domain warnings from
+`domainIncludes`; core validation combines that metadata with the bundled
+standard subclass hierarchy.
+The schema generator writes the grammar and sorted direct-subclass ontology from
+the same official source graph; `--ontology-output-file` can override the default
+bundled-resource refresh path beside `--output-file`.
Google-table parsing supports both property-level "one of" alternatives and explicit
option branches (`Option A` / `Option B`) where each branch can require multiple properties.
Required rows that explicitly document a supported fallback (for example, `url` when
diff --git a/specs/validation.md b/specs/validation.md
index 1db282c..3e8267f 100644
--- a/specs/validation.md
+++ b/specs/validation.md
@@ -48,6 +48,14 @@ Search Gallery quality gates:
Schema.org grammar checks intentionally allow URL and text literals for every
property (in addition to the documented range types).
+Schema.org properties with documented `domainIncludes` values emit warning-level
+domain constraints. A property is accepted when any declared Schema.org type is
+the documented domain or one of its standard subclasses. Multi-typed nodes use
+union semantics, untyped nodes and external predicates are not domain-checked,
+and each incompatible subject/property pair produces one deterministic warning.
+The grammar and normalized direct-subclass ontology must be regenerated from the
+same downloaded Schema.org graph so they remain version-aligned.
+
## JSON-LD validation from URLs
The validation module can render a URL with Playwright, extract all
diff --git a/tests/test_graph_audit.py b/tests/test_graph_audit.py
index b6cb4fc..327e762 100644
--- a/tests/test_graph_audit.py
+++ b/tests/test_graph_audit.py
@@ -2,6 +2,7 @@
from __future__ import annotations
+import json
import textwrap
from pathlib import Path
@@ -34,6 +35,10 @@
# ---------------------------------------------------------------------------
_SCHEMA = "http://schema.org/"
+_DOMAIN_MESSAGE = (
+ "The property isPartOf is not recognized by the schema (e.g. schema.org) "
+ "for an object of type Service."
+)
def _g(*triples) -> Graph:
@@ -700,3 +705,104 @@ def test_build_entity_matrix_columns_sorted(tmp_path: Path) -> None:
cols = list(rows[0].keys())
type_cols = cols[1:] # skip "url"
assert type_cols == sorted(type_cols)
+
+
+def test_graph_auditor_reports_service_is_part_of(tmp_path: Path) -> None:
+ path = tmp_path / "zurich-regression.jsonld"
+ site_a = "https://data.wordlift.io/example/web-sites/site-a"
+ site_b = "https://data.wordlift.io/example/web-sites/site-b"
+ path.write_text(
+ json.dumps(
+ {
+ "@graph": [
+ {
+ "@id": "https://data.wordlift.io/example/services/life-insurance",
+ "@type": "schema:Service",
+ "schema:isPartOf": [{"@id": site_b}, {"@id": site_a}],
+ "schema:name": "Life insurance",
+ "schema:url": "https://example.org/life-insurance",
+ },
+ {
+ "@id": site_a,
+ "@type": "schema:WebSite",
+ "schema:url": "https://example.org/",
+ },
+ ],
+ "@context": {"schema": "http://schema.org/"},
+ }
+ ),
+ encoding="utf-8",
+ )
+
+ report = GraphAuditor().audit(
+ path,
+ AuditOptions(builtin_shapes=["schemaorg-grammar"], max_workers=1),
+ )
+ by_url = {entry.url: entry for entry in report.schema_compliance.by_url}
+ result = by_url["https://example.org/life-insurance"]
+
+ assert result.warning_count == 1
+ assert len(result.warnings) == 1
+ warning = result.warnings[0]
+ assert warning.path == "http://schema.org/isPartOf"
+ assert warning.shape_source == "schemaorg-grammar"
+ assert warning.message == _DOMAIN_MESSAGE
+ assert warning.value == site_a
+
+ error_only = GraphAuditor().audit(
+ path,
+ AuditOptions(
+ builtin_shapes=["schemaorg-grammar"],
+ issue_level="error",
+ max_workers=1,
+ ),
+ )
+ error_by_url = {entry.url: entry for entry in error_only.schema_compliance.by_url}
+ assert error_by_url["https://example.org/life-insurance"].warning_count == 0
+
+
+def test_graph_auditor_domain_results_are_stable_with_workers(tmp_path: Path) -> None:
+ path = tmp_path / "concurrent-domain.jsonld"
+ path.write_text(
+ json.dumps(
+ {
+ "@context": {"@vocab": "http://schema.org/"},
+ "@graph": [
+ {
+ "@id": "https://example.org/services/invalid",
+ "@type": "Service",
+ "isPartOf": {"@id": "https://example.org/site"},
+ "url": "https://example.org/invalid",
+ },
+ {
+ "@id": "https://example.org/articles/valid",
+ "@type": "Article",
+ "isPartOf": {"@id": "https://example.org/site"},
+ "url": "https://example.org/valid",
+ },
+ ],
+ }
+ ),
+ encoding="utf-8",
+ )
+
+ def collect(max_workers: int) -> dict[str, list[str]]:
+ report = GraphAuditor().audit(
+ path,
+ AuditOptions(
+ builtin_shapes=["schemaorg-grammar"],
+ max_workers=max_workers,
+ ),
+ )
+ return {
+ entry.url: [issue.message for issue in entry.warnings]
+ for entry in report.schema_compliance.by_url
+ }
+
+ expected = collect(1)
+ assert expected == {
+ "https://example.org/invalid": [_DOMAIN_MESSAGE],
+ "https://example.org/valid": [],
+ }
+ for _ in range(3):
+ assert collect(2) == expected
diff --git a/tests/test_schemaorg_domain_validation.py b/tests/test_schemaorg_domain_validation.py
new file mode 100644
index 0000000..437cb2f
--- /dev/null
+++ b/tests/test_schemaorg_domain_validation.py
@@ -0,0 +1,101 @@
+from __future__ import annotations
+
+import json
+from pathlib import Path
+
+import pytest
+
+from wordlift_sdk.validation import shacl
+
+
+_MESSAGE = (
+ "The property isPartOf is not recognized by the schema (e.g. schema.org) "
+ "for an object of type Service."
+)
+
+
+def _validate_payload(tmp_path: Path, types: str | list[str]):
+ path = tmp_path / "schemaorg-domain.jsonld"
+ path.write_text(
+ json.dumps(
+ {
+ "@context": {"@vocab": "https://schema.org/"},
+ "@id": "https://example.org/items/1",
+ "@type": types,
+ "isPartOf": {"@id": "https://example.org/site"},
+ }
+ ),
+ encoding="utf-8",
+ )
+ return shacl.validate_file(path.as_posix(), shape_specs=["schemaorg-grammar"])
+
+
+def _domain_issues(result):
+ return [
+ issue
+ for issue in shacl.extract_validation_issues(result)
+ if "is not recognized by the schema" in issue.message
+ ]
+
+
+def test_schemaorg_domain_rejects_is_part_of_on_service(tmp_path: Path) -> None:
+ result = _validate_payload(tmp_path, "Service")
+ issues = _domain_issues(result)
+
+ assert len(issues) == 1
+ assert result.warning_count == 1
+ assert _MESSAGE in result.report_text
+ assert issues[0].level == "warning"
+ assert issues[0].focus_node == "https://example.org/items/1"
+ assert issues[0].result_path == "http://schema.org/isPartOf"
+ assert issues[0].rule_set == "schemaorg-grammar"
+ assert issues[0].message == _MESSAGE
+
+
+@pytest.mark.parametrize(
+ "types",
+ ["CreativeWork", "Article", ["Service", "CreativeWork"]],
+)
+def test_schemaorg_domain_accepts_compatible_type_or_subtype(
+ tmp_path: Path,
+ types: str | list[str],
+) -> None:
+ assert _domain_issues(_validate_payload(tmp_path, types)) == []
+
+
+def test_schemaorg_domain_reports_unrelated_multitype_once(tmp_path: Path) -> None:
+ issues = _domain_issues(_validate_payload(tmp_path, ["Service", "Product"]))
+
+ assert len(issues) == 1
+
+
+def test_schemaorg_domain_ignores_untyped_and_external_predicates(
+ tmp_path: Path,
+) -> None:
+ path = tmp_path / "domain-boundaries.jsonld"
+ path.write_text(
+ json.dumps(
+ {
+ "@context": {"@vocab": "http://schema.org/"},
+ "@graph": [
+ {
+ "@id": "https://example.org/untyped",
+ "isPartOf": "https://example.org/site",
+ },
+ {
+ "@id": "https://example.org/service",
+ "@type": "Service",
+ "https://example.org/isPartOf": "https://example.org/site",
+ "name": "Valid inherited Thing property",
+ },
+ ],
+ }
+ ),
+ encoding="utf-8",
+ )
+
+ result = shacl.validate_file(
+ path.as_posix(),
+ shape_specs=["schemaorg-grammar"],
+ )
+ assert _domain_issues(result) == []
diff --git a/tests/test_validation_generator_more.py b/tests/test_validation_generator_more.py
index 6981b02..c0ebbdd 100644
--- a/tests/test_validation_generator_more.py
+++ b/tests/test_validation_generator_more.py
@@ -1,10 +1,12 @@
from __future__ import annotations
+import json
from pathlib import Path
from rdflib import Graph, RDF, RDFS, URIRef
import wordlift_sdk.validation.generator as generator
+from wordlift_sdk.validation import shacl
from wordlift_sdk.validation.generator import FeatureData
@@ -417,6 +419,7 @@ def test_generate_google_shacls_and_entrypoint(monkeypatch, tmp_path: Path):
def test_generate_schema_shacls_and_schema_main(monkeypatch, tmp_path: Path):
out = tmp_path / "schemaorg-grammar.ttl"
+ ontology_out = tmp_path / "schemaorg-subclass-ontology.nt"
monkeypatch.setattr(
generator.requests,
@@ -427,26 +430,111 @@ def test_generate_schema_shacls_and_schema_main(monkeypatch, tmp_path: Path):
)
monkeypatch.setattr(generator, "tqdm", lambda seq, **kwargs: seq)
- cls = URIRef("http://schema.org/Thing")
- prop = URIRef("http://schema.org/name")
- monkeypatch.setattr(generator, "_collect_classes", lambda graph: [cls])
+ cls = URIRef("http://schema.org/CreativeWork")
+ child_cls = URIRef("http://schema.org/Article")
+ unrelated_cls = URIRef("http://schema.org/Service")
+ prop = URIRef("http://schema.org/isPartOf")
+ monkeypatch.setattr(
+ generator, "_collect_classes", lambda graph: [child_cls, cls, unrelated_cls]
+ )
monkeypatch.setattr(generator, "_collect_properties", lambda graph: [prop])
monkeypatch.setattr(
generator,
"_collect_domain_ranges",
- lambda graph, p: [(cls, [URIRef("http://schema.org/Text")])],
+ lambda graph, p: [(cls, [URIRef("http://schema.org/CreativeWork")])],
)
- rc = generator.generate_schema_shacls(out, overwrite=True)
+ graph = Graph()
+ graph.add((child_cls, RDFS.subClassOf, cls))
+ monkeypatch.setattr(generator, "_load_schema_graph", lambda: graph)
+
+ rc = generator.generate_schema_shacls(
+ out,
+ overwrite=True,
+ ontology_output_file=ontology_out,
+ )
assert rc == 0
content = out.read_text(encoding="utf-8")
- assert "sh:targetClass schema:Thing" in content
- assert "sh:path schema:name" in content
+ assert "sh:targetClass schema:CreativeWork" in content
+ assert "sh:path schema:isPartOf" in content
+ assert ":schema_isPartOfDomainRule" in content
+ assert "a :PropertyDomainRule" in content
+ assert ":path schema:isPartOf" in content
+ assert ":domain schema:CreativeWork" in content
+ assert ontology_out.exists()
+ ontology = Graph().parse(ontology_out, format="nt")
+ assert (child_cls, RDFS.subClassOf, cls) in ontology
+
+ invalid_path = tmp_path / "invalid.jsonld"
+ invalid_path.write_text(
+ json.dumps(
+ {
+ "@context": {"@vocab": "http://schema.org/"},
+ "@type": "Service",
+ "isPartOf": "https://example.org/site",
+ }
+ ),
+ encoding="utf-8",
+ )
+ invalid = shacl.validate_file(invalid_path.as_posix(), shape_specs=[str(out)])
+ issues = shacl.extract_validation_issues(invalid)
+ assert len(issues) == 1
+ assert issues[0].result_path == "http://schema.org/isPartOf"
+ assert issues[0].message.endswith("object of type Service.")
+
+ valid_path = tmp_path / "valid.jsonld"
+ valid_path.write_text(
+ json.dumps(
+ {
+ "@context": {"@vocab": "http://schema.org/"},
+ "@type": "Article",
+ "isPartOf": "https://example.org/site",
+ }
+ ),
+ encoding="utf-8",
+ )
+ valid = shacl.validate_file(valid_path.as_posix(), shape_specs=[str(out)])
+ assert shacl.extract_validation_issues(valid) == []
- rc_main = generator.schema_main(["--output-file", str(out), "--overwrite"])
+ rc_main = generator.schema_main(
+ [
+ "--output-file",
+ str(out),
+ "--ontology-output-file",
+ str(ontology_out),
+ "--overwrite",
+ ]
+ )
assert rc_main == 0
+def test_schema_domain_rule_is_deterministic() -> None:
+ creative_work = URIRef("https://schema.org/CreativeWork")
+ thing = URIRef("https://schema.org/Thing")
+ external = URIRef("https://example.org/ExternalType")
+
+ assert generator._canonical_schema_uri(external) is None
+ assert (
+ generator._render_domain_rule(
+ URIRef("https://schema.org/about"),
+ [external],
+ )
+ == []
+ )
+
+ lines = generator._render_domain_rule(
+ URIRef("https://schema.org/about"),
+ [thing, creative_work],
+ )
+ assert lines == [
+ ":schema_aboutDomainRule",
+ " a :PropertyDomainRule ;",
+ " :path schema:about ;",
+ " :domain schema:CreativeWork,",
+ " schema:Thing .",
+ ]
+
+
def test_write_feature_respects_overwrite(tmp_path: Path):
feature = FeatureData(
url="https://example.org",
diff --git a/tests/tools/run_slice_tests.py b/tests/tools/run_slice_tests.py
index d2ef093..39cacf0 100644
--- a/tests/tools/run_slice_tests.py
+++ b/tests/tools/run_slice_tests.py
@@ -30,6 +30,7 @@
"tests/test_recommended_one_of_validation.py",
"tests/test_merchant_listing_defined_region_validation.py",
"tests/test_product_snippet_validation.py",
+ "tests/test_schemaorg_domain_validation.py",
"tests/test_shacl_generator.py",
"tests/test_validation_generator_more.py",
],
diff --git a/wordlift_sdk/validation/generator.py b/wordlift_sdk/validation/generator.py
index 07b8af8..f5caa21 100644
--- a/wordlift_sdk/validation/generator.py
+++ b/wordlift_sdk/validation/generator.py
@@ -1021,6 +1021,76 @@ def _collect_domain_ranges(
return [(domain, ranges) for domain in domains]
+def _canonical_schema_uri(uri: URIRef) -> URIRef | None:
+ value = str(uri)
+ for namespace in (str(SCHEMA_VOCAB), str(SCHEMA_DATA)):
+ if value.startswith(namespace):
+ return URIRef(f"{SCHEMA_DATA}{value[len(namespace) :]}")
+ return None
+
+
+def _schema_subclass_edges(graph: Graph) -> set[tuple[URIRef, URIRef]]:
+ edges: set[tuple[URIRef, URIRef]] = set()
+ for child, parent in graph.subject_objects(RDFS.subClassOf):
+ if not isinstance(child, URIRef) or not isinstance(parent, URIRef):
+ continue
+ canonical_child = _canonical_schema_uri(child)
+ canonical_parent = _canonical_schema_uri(parent)
+ if canonical_child is not None and canonical_parent is not None:
+ edges.add((canonical_child, canonical_parent))
+ return edges
+
+
+def _render_domain_rule(
+ prop: URIRef,
+ domains: Iterable[URIRef],
+) -> list[str]:
+ short_prop = _short_name(prop)
+ domain_names = sorted(
+ {
+ _short_name(canonical)
+ for domain in domains
+ if (canonical := _canonical_schema_uri(domain)) is not None
+ }
+ )
+ if not domain_names:
+ return []
+
+ lines = [
+ f":schema_{short_prop}DomainRule",
+ " a :PropertyDomainRule ;",
+ f" :path schema:{short_prop} ;",
+ ]
+ for index, name in enumerate(domain_names):
+ suffix = "," if index < len(domain_names) - 1 else " ."
+ predicate = " :domain" if index == 0 else " "
+ lines.append(f"{predicate} schema:{name}{suffix}")
+ return lines
+
+
+def _load_schema_graph() -> Graph:
+ response = requests.get(SCHEMA_JSONLD_URL, timeout=60)
+ response.raise_for_status()
+ graph = Graph()
+ graph.parse(data=response.text, format="json-ld")
+ return graph
+
+
+def _write_subclass_ontology(
+ output_path: Path,
+ subclass_edges: set[tuple[URIRef, URIRef]],
+) -> None:
+ predicate = str(RDFS.subClassOf)
+ lines = [
+ f"<{child}> <{predicate}> <{parent}> ."
+ for child, parent in sorted(
+ subclass_edges, key=lambda edge: tuple(map(str, edge))
+ )
+ ]
+ output_path.parent.mkdir(parents=True, exist_ok=True)
+ output_path.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8")
+
+
def _render_property_shape(prop: URIRef, ranges: list[URIRef]) -> list[str]:
lines: list[str] = []
lines.append(" sh:property [")
@@ -1055,25 +1125,34 @@ def _render_property_shape(prop: URIRef, ranges: list[URIRef]) -> list[str]:
return lines
-def generate_schema_shacls(output_file: Path, overwrite: bool) -> int:
+def generate_schema_shacls(
+ output_file: Path,
+ overwrite: bool,
+ ontology_output_file: Path | None = None,
+) -> int:
output_path = output_file
+ ontology_output_path = ontology_output_file or output_path.with_name(
+ "schemaorg-subclass-ontology.nt"
+ )
if output_path.exists() and not overwrite:
print(f"Output exists: {output_path}")
return 1
+ if ontology_output_path.exists() and not overwrite:
+ print(f"Output exists: {ontology_output_path}")
+ return 1
- response = requests.get(SCHEMA_JSONLD_URL, timeout=60)
- response.raise_for_status()
-
- graph = Graph()
- graph.parse(data=response.text, format="json-ld")
+ graph = _load_schema_graph()
classes = _collect_classes(graph)
props = _collect_properties(graph)
+ subclass_edges = _schema_subclass_edges(graph)
class_props: dict[URIRef, list[PropertyRange]] = {cls: [] for cls in classes}
+ property_domains: dict[URIRef, set[URIRef]] = {}
for prop in tqdm(props, desc="Collecting properties", unit="prop"):
for domain, ranges in _collect_domain_ranges(graph, prop):
+ property_domains.setdefault(prop, set()).add(domain)
if domain not in class_props:
class_props[domain] = []
class_props[domain].append(PropertyRange(prop=prop, ranges=ranges))
@@ -1108,9 +1187,21 @@ def generate_schema_shacls(output_file: Path, overwrite: bool) -> int:
lines.append(".")
lines.append("")
+ for prop in tqdm(props, desc="Writing domain checks", unit="prop"):
+ domain_lines = _render_domain_rule(
+ prop,
+ property_domains.get(prop, set()),
+ )
+ if not domain_lines:
+ continue
+ lines.extend(domain_lines)
+ lines.append("")
+
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8")
+ _write_subclass_ontology(ontology_output_path, subclass_edges)
print(f"Wrote {output_path}")
+ print(f"Wrote {ontology_output_path}")
return 0
@@ -1148,8 +1239,21 @@ def schema_main(argv: list[str] | None = None) -> int:
parser.add_argument(
"--overwrite", action="store_true", help="Overwrite existing file."
)
+ parser.add_argument(
+ "--ontology-output-file",
+ help=(
+ "Output Schema.org subclass ontology file. Defaults to "
+ "schemaorg-subclass-ontology.nt beside --output-file."
+ ),
+ )
args = parser.parse_args(argv)
- return generate_schema_shacls(Path(args.output_file), args.overwrite)
+ return generate_schema_shacls(
+ Path(args.output_file),
+ args.overwrite,
+ ontology_output_file=Path(args.ontology_output_file)
+ if args.ontology_output_file
+ else None,
+ )
def _gs1_short_name(uri: URIRef) -> str:
diff --git a/wordlift_sdk/validation/shacl.py b/wordlift_sdk/validation/shacl.py
index b9b0773..fcf9d39 100644
--- a/wordlift_sdk/validation/shacl.py
+++ b/wordlift_sdk/validation/shacl.py
@@ -14,7 +14,7 @@
import json
from rdflib.collection import Collection
-from rdflib import Graph, URIRef
+from rdflib import BNode, Graph, Literal, Namespace, URIRef
from rdflib.namespace import RDF, SH
from rdflib.term import Identifier
from requests import Response, get
@@ -24,6 +24,12 @@
DEFAULT_OPT_IN_EXCLUDED_SHAPES = {"google-image-license-metadata.ttl"}
_SCHEMAORG_GRAMMAR_SHAPE = "schemaorg-grammar.ttl"
_SCHEMAORG_SUBCLASS_ONTOLOGY_RESOURCE = "schemaorg-subclass-ontology.nt"
+_SCHEMAORG_HTTP = "http://schema.org/"
+_SCHEMAORG_GRAMMAR = Namespace("https://wordlift.io/shacl/schemaorg-grammar/")
+_DOMAIN_RULE_CLASS = _SCHEMAORG_GRAMMAR.PropertyDomainRule
+_DOMAIN_RULE_PATH = _SCHEMAORG_GRAMMAR.path
+_DOMAIN_RULE_DOMAIN = _SCHEMAORG_GRAMMAR.domain
+_DOMAIN_CONSTRAINT_COMPONENT = _SCHEMAORG_GRAMMAR.PropertyDomainConstraintComponent
_VALIDATOR_OPTIONS = {
"inference": "rdfs",
"abort_on_first": False,
@@ -70,6 +76,13 @@ class PreparedValidationResult:
warning_count: int
+@dataclass(frozen=True)
+class _SchemaOrgDomainRule:
+ source_shape: Identifier
+ path: URIRef
+ domains: frozenset[URIRef]
+
+
class _JsonLdScriptExtractor(HTMLParser):
def __init__(self) -> None:
super().__init__(convert_charrefs=True)
@@ -330,7 +343,8 @@ def _schemaorg_subclass_map() -> dict[URIRef, set[URIRef]]:
return subclass_map
-def _schemaorg_superclasses(class_iri: URIRef) -> set[URIRef]:
+@lru_cache(maxsize=None)
+def _schemaorg_superclasses(class_iri: URIRef) -> frozenset[URIRef]:
subclass_map = _schemaorg_subclass_map()
seen: set[URIRef] = set()
stack = [class_iri]
@@ -341,7 +355,106 @@ def _schemaorg_superclasses(class_iri: URIRef) -> set[URIRef]:
continue
seen.add(parent)
stack.append(parent)
- return seen
+ return frozenset(seen)
+
+
+def _schemaorg_domain_rules(shapes_graph: Graph) -> dict[URIRef, _SchemaOrgDomainRule]:
+ rules: dict[URIRef, _SchemaOrgDomainRule] = {}
+ for source_shape in shapes_graph.subjects(RDF.type, _DOMAIN_RULE_CLASS):
+ path = shapes_graph.value(source_shape, _DOMAIN_RULE_PATH)
+ if not isinstance(path, URIRef):
+ continue
+ domains = frozenset(
+ domain
+ for domain in shapes_graph.objects(source_shape, _DOMAIN_RULE_DOMAIN)
+ if isinstance(domain, URIRef)
+ )
+ if domains:
+ rules[path] = _SchemaOrgDomainRule(
+ source_shape=source_shape,
+ path=path,
+ domains=domains,
+ )
+ return rules
+
+
+def _schemaorg_local_name(uri: URIRef) -> str:
+ value = str(uri)
+ return value[len(_SCHEMAORG_HTTP) :] if value.startswith(_SCHEMAORG_HTTP) else value
+
+
+def _rdf_term_sort_key(term: Identifier) -> tuple[str, str, str, str]:
+ return (
+ term.__class__.__name__,
+ str(term),
+ str(getattr(term, "datatype", "") or ""),
+ str(getattr(term, "language", "") or ""),
+ )
+
+
+def _append_schemaorg_domain_results(
+ *,
+ data_graph: Graph,
+ report_graph: Graph,
+ rules: dict[URIRef, _SchemaOrgDomainRule],
+) -> list[str]:
+ messages: list[str] = []
+ report_node = next(report_graph.subjects(RDF.type, SH.ValidationReport), None)
+ for focus_node in sorted(set(data_graph.subjects()), key=str):
+ declared_types = sorted(
+ {
+ type_iri
+ for type_iri in data_graph.objects(focus_node, RDF.type)
+ if isinstance(type_iri, URIRef)
+ and str(type_iri).startswith(_SCHEMAORG_HTTP)
+ },
+ key=str,
+ )
+ if not declared_types:
+ continue
+ for path in sorted(set(data_graph.predicates(focus_node)), key=str):
+ rule = rules.get(path) if isinstance(path, URIRef) else None
+ if rule is None:
+ continue
+ if any(
+ declared_type in rule.domains
+ or bool(
+ rule.domains.intersection(_schemaorg_superclasses(declared_type))
+ )
+ for declared_type in declared_types
+ ):
+ continue
+
+ property_name = _schemaorg_local_name(rule.path)
+ type_name = _schemaorg_local_name(declared_types[0])
+ message = (
+ f"The property {property_name} is not recognized by the schema "
+ f"(e.g. schema.org) for an object of type {type_name}."
+ )
+ result_node = BNode()
+ report_graph.add((result_node, RDF.type, SH.ValidationResult))
+ report_graph.add((result_node, SH.resultSeverity, SH.Warning))
+ report_graph.add((result_node, SH.focusNode, focus_node))
+ report_graph.add((result_node, SH.resultPath, rule.path))
+ report_graph.add((result_node, SH.sourceShape, rule.source_shape))
+ report_graph.add(
+ (
+ result_node,
+ SH.sourceConstraintComponent,
+ _DOMAIN_CONSTRAINT_COMPONENT,
+ )
+ )
+ report_graph.add((result_node, SH.resultMessage, Literal(message)))
+ values = sorted(
+ data_graph.objects(focus_node, path),
+ key=_rdf_term_sort_key,
+ )
+ if values:
+ report_graph.add((result_node, SH.value, values[0]))
+ if report_node is not None:
+ report_graph.add((report_node, SH.result, result_node))
+ messages.append(message)
+ return messages
def _shape_expected_classes(
@@ -498,6 +611,9 @@ def __init__(self, prepared_shapes: PreparedShapes):
from pyshacl import Validator
self._prepared_shapes = prepared_shapes
+ self._schemaorg_domain_rules = _schemaorg_domain_rules(
+ prepared_shapes.shapes_graph
+ )
self._validator = Validator(
Graph(),
shacl_graph=prepared_shapes.shapes_graph,
@@ -529,6 +645,18 @@ def validate_graph(
self._validator._target_graph = None
conforms, report_graph, report_text = self._validator.run()
+ domain_messages = _append_schemaorg_domain_results(
+ data_graph=data_graph,
+ report_graph=report_graph,
+ rules=self._schemaorg_domain_rules,
+ )
+ if domain_messages:
+ report_text = "\n".join(
+ [
+ report_text.rstrip(),
+ *(f"Domain warning: {m}" for m in domain_messages),
+ ]
+ )
warning_count = sum(
1 for _ in report_graph.subjects(SH.resultSeverity, SH.Warning)
)
diff --git a/wordlift_sdk/validation/shacls/schemaorg-grammar.ttl b/wordlift_sdk/validation/shacls/schemaorg-grammar.ttl
index c702759..77cabab 100644
--- a/wordlift_sdk/validation/shacls/schemaorg-grammar.ttl
+++ b/wordlift_sdk/validation/shacls/schemaorg-grammar.ttl
@@ -3,7 +3,7 @@
@prefix schema: .
# Source: https://schema.org/version/latest/schemaorg-current-https.jsonld
-# Generated: 2026-02-26T15:42:24Z
+# Generated: 2026-07-13T13:16:37Z
# Notes: schema.org grammar checks only; all constraints are warnings.
:schema_3DModelShape
@@ -119,8 +119,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:BedType ]
[ sh:class schema:BedDetails ]
+ [ sh:class schema:BedType ]
) ;
sh:message "Schema.org range check: bed." ;
] ;
@@ -175,10 +175,10 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:QuantitativeValue ]
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
+ [ sh:class schema:QuantitativeValue ]
) ;
sh:message "Schema.org range check: numberOfBedrooms." ;
] ;
@@ -215,10 +215,10 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:QuantitativeValue ]
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
+ [ sh:class schema:QuantitativeValue ]
) ;
sh:message "Schema.org range check: numberOfRooms." ;
] ;
@@ -357,9 +357,9 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:VirtualLocation ]
[ sh:class schema:Place ]
[ sh:class schema:PostalAddress ]
+ [ sh:class schema:VirtualLocation ]
) ;
sh:message "Schema.org range check: location." ;
] ;
@@ -381,8 +381,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:Person ]
[ sh:class schema:Organization ]
+ [ sh:class schema:Person ]
) ;
sh:message "Schema.org range check: participant." ;
] ;
@@ -393,8 +393,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:Person ]
[ sh:class schema:Organization ]
+ [ sh:class schema:Person ]
) ;
sh:message "Schema.org range check: provider." ;
] ;
@@ -444,8 +444,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:datatype ]
[ sh:datatype ]
+ [ sh:datatype ]
[ sh:datatype ]
) ;
sh:message "Schema.org range check: availabilityEnds." ;
@@ -457,8 +457,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:datatype ]
[ sh:datatype ]
+ [ sh:datatype ]
[ sh:datatype ]
) ;
sh:message "Schema.org range check: availabilityStarts." ;
@@ -470,8 +470,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:PhysicalActivityCategory ]
[ sh:class schema:CategoryCode ]
+ [ sh:class schema:PhysicalActivityCategory ]
[ sh:class schema:Thing ]
) ;
sh:message "Schema.org range check: category." ;
@@ -572,8 +572,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:Offer ]
[ sh:class schema:Demand ]
+ [ sh:class schema:Offer ]
) ;
sh:message "Schema.org range check: offers." ;
] ;
@@ -896,8 +896,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:CreativeWork ]
[ sh:class schema:Comment ]
+ [ sh:class schema:CreativeWork ]
) ;
sh:message "Schema.org range check: parentItem." ;
] ;
@@ -913,10 +913,10 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:QuantitativeValue ]
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
+ [ sh:class schema:QuantitativeValue ]
) ;
sh:message "Schema.org range check: numberOfRooms." ;
] ;
@@ -965,10 +965,10 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:QuantitativeValue ]
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
+ [ sh:class schema:QuantitativeValue ]
) ;
sh:message "Schema.org range check: numberOfBedrooms." ;
] ;
@@ -1272,10 +1272,10 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
+ [ sh:class schema:Audience ]
[ sh:class schema:ContactPoint ]
- [ sh:class schema:Person ]
[ sh:class schema:Organization ]
- [ sh:class schema:Audience ]
+ [ sh:class schema:Person ]
) ;
sh:message "Schema.org range check: recipient." ;
] ;
@@ -1493,8 +1493,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:Taxon ]
[ sh:class schema:DefinedTerm ]
+ [ sh:class schema:Taxon ]
) ;
sh:message "Schema.org range check: taxonomicRange." ;
] ;
@@ -1825,10 +1825,10 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:QuantitativeValue ]
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
+ [ sh:class schema:QuantitativeValue ]
) ;
sh:message "Schema.org range check: broadcastFrequencyValue." ;
] ;
@@ -2013,8 +2013,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:BusStop ]
[ sh:class schema:BusStation ]
+ [ sh:class schema:BusStop ]
) ;
sh:message "Schema.org range check: arrivalBusStop." ;
] ;
@@ -2045,8 +2045,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:BusStop ]
[ sh:class schema:BusStation ]
+ [ sh:class schema:BusStop ]
) ;
sh:message "Schema.org range check: departureBusStop." ;
] ;
@@ -2100,8 +2100,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:Person ]
[ sh:class schema:Organization ]
+ [ sh:class schema:Person ]
) ;
sh:message "Schema.org range check: seller." ;
] ;
@@ -2340,8 +2340,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:datatype ]
[ sh:datatype ]
+ [ sh:datatype ]
) ;
sh:message "Schema.org range check: datePosted." ;
] ;
@@ -2436,8 +2436,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:datatype ]
[ sh:datatype ]
+ [ sh:datatype ]
) ;
sh:message "Schema.org range check: auditDate." ;
] ;
@@ -2493,8 +2493,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:datatype ]
[ sh:datatype ]
+ [ sh:datatype ]
) ;
sh:message "Schema.org range check: expires." ;
] ;
@@ -2538,8 +2538,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:datatype ]
[ sh:datatype ]
+ [ sh:datatype ]
) ;
sh:message "Schema.org range check: validFrom." ;
] ;
@@ -2693,8 +2693,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:Person ]
[ sh:class schema:Organization ]
+ [ sh:class schema:Person ]
) ;
sh:message "Schema.org range check: claimInterpreter." ;
] ;
@@ -2736,9 +2736,9 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:Property ]
[ sh:class schema:Class ]
[ sh:class schema:Enumeration ]
+ [ sh:class schema:Property ]
) ;
sh:message "Schema.org range check: supersededBy." ;
] ;
@@ -3045,8 +3045,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:CreativeWork ]
[ sh:class schema:Comment ]
+ [ sh:class schema:CreativeWork ]
) ;
sh:message "Schema.org range check: parentItem." ;
] ;
@@ -3133,10 +3133,10 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
+ [ sh:class schema:Audience ]
[ sh:class schema:ContactPoint ]
- [ sh:class schema:Person ]
[ sh:class schema:Organization ]
- [ sh:class schema:Audience ]
+ [ sh:class schema:Person ]
) ;
sh:message "Schema.org range check: recipient." ;
] ;
@@ -3233,9 +3233,9 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:Place ]
[ sh:class schema:AdministrativeArea ]
[ sh:class schema:GeoShape ]
+ [ sh:class schema:Place ]
) ;
sh:message "Schema.org range check: areaServed." ;
] ;
@@ -3348,8 +3348,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:Place ]
[ sh:class schema:FoodEstablishment ]
+ [ sh:class schema:Place ]
) ;
sh:message "Schema.org range check: foodEstablishment." ;
] ;
@@ -3423,8 +3423,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:Course ]
[ sh:class schema:AlignmentObject ]
+ [ sh:class schema:Course ]
) ;
sh:message "Schema.org range check: coursePrerequisites." ;
] ;
@@ -3468,8 +3468,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:StructuredValue ]
[ sh:datatype ]
+ [ sh:class schema:StructuredValue ]
) ;
sh:message "Schema.org range check: numberOfCredits." ;
] ;
@@ -3744,9 +3744,9 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:MusicRecording ]
- [ sh:class schema:Clip ]
[ sh:class schema:AudioObject ]
+ [ sh:class schema:Clip ]
+ [ sh:class schema:MusicRecording ]
) ;
sh:message "Schema.org range check: audio." ;
] ;
@@ -3876,8 +3876,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:Person ]
[ sh:class schema:Organization ]
+ [ sh:class schema:Person ]
) ;
sh:message "Schema.org range check: contributor." ;
] ;
@@ -3888,8 +3888,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:Person ]
[ sh:class schema:Organization ]
+ [ sh:class schema:Person ]
) ;
sh:message "Schema.org range check: copyrightHolder." ;
] ;
@@ -3956,8 +3956,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:Person ]
[ sh:class schema:Organization ]
+ [ sh:class schema:Person ]
) ;
sh:message "Schema.org range check: creator." ;
] ;
@@ -3978,8 +3978,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:datatype ]
[ sh:datatype ]
+ [ sh:datatype ]
) ;
sh:message "Schema.org range check: dateCreated." ;
] ;
@@ -4143,8 +4143,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:datatype ]
[ sh:datatype ]
+ [ sh:datatype ]
) ;
sh:message "Schema.org range check: expires." ;
] ;
@@ -4375,8 +4375,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:Person ]
[ sh:class schema:Organization ]
+ [ sh:class schema:Person ]
) ;
sh:message "Schema.org range check: maintainer." ;
] ;
@@ -4420,8 +4420,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:Offer ]
[ sh:class schema:Demand ]
+ [ sh:class schema:Offer ]
) ;
sh:message "Schema.org range check: offers." ;
] ;
@@ -4454,8 +4454,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:Person ]
[ sh:class schema:Organization ]
+ [ sh:class schema:Person ]
) ;
sh:message "Schema.org range check: producer." ;
] ;
@@ -4466,8 +4466,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:Person ]
[ sh:class schema:Organization ]
+ [ sh:class schema:Person ]
) ;
sh:message "Schema.org range check: provider." ;
] ;
@@ -4489,8 +4489,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:Person ]
[ sh:class schema:Organization ]
+ [ sh:class schema:Person ]
) ;
sh:message "Schema.org range check: publisher." ;
] ;
@@ -4599,8 +4599,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:Person ]
[ sh:class schema:Organization ]
+ [ sh:class schema:Person ]
) ;
sh:message "Schema.org range check: sdPublisher." ;
] ;
@@ -4611,8 +4611,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:QuantitativeValue ]
[ sh:class schema:DefinedTerm ]
+ [ sh:class schema:QuantitativeValue ]
[ sh:class schema:SizeSpecification ]
) ;
sh:message "Schema.org range check: size." ;
@@ -4657,8 +4657,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:Person ]
[ sh:class schema:Organization ]
+ [ sh:class schema:Person ]
) ;
sh:message "Schema.org range check: sponsor." ;
] ;
@@ -4801,8 +4801,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:VideoObject ]
[ sh:class schema:Clip ]
+ [ sh:class schema:VideoObject ]
) ;
sh:message "Schema.org range check: video." ;
] ;
@@ -4874,8 +4874,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:datatype ]
[ sh:datatype ]
+ [ sh:datatype ]
) ;
sh:message "Schema.org range check: endDate." ;
] ;
@@ -4952,8 +4952,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:datatype ]
[ sh:datatype ]
+ [ sh:datatype ]
) ;
sh:message "Schema.org range check: startDate." ;
] ;
@@ -4980,8 +4980,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:datatype ]
[ sh:datatype ]
+ [ sh:datatype ]
) ;
sh:message "Schema.org range check: endDate." ;
] ;
@@ -5002,13 +5002,62 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:datatype ]
[ sh:datatype ]
+ [ sh:datatype ]
) ;
sh:message "Schema.org range check: startDate." ;
] ;
.
+:schema_CredentialShape
+ a sh:NodeShape ;
+ sh:targetClass schema:Credential ;
+ sh:property [
+ sh:path schema:credentialCategory ;
+ sh:severity sh:Warning ;
+ sh:or (
+ [ sh:datatype ]
+ [ sh:datatype ]
+ [ sh:datatype ]
+ [ sh:class schema:DefinedTerm ]
+ ) ;
+ sh:message "Schema.org range check: credentialCategory." ;
+ ] ;
+ sh:property [
+ sh:path schema:recognizedBy ;
+ sh:severity sh:Warning ;
+ sh:or (
+ [ sh:datatype ]
+ [ sh:datatype ]
+ [ sh:datatype ]
+ [ sh:class schema:Organization ]
+ ) ;
+ sh:message "Schema.org range check: recognizedBy." ;
+ ] ;
+ sh:property [
+ sh:path schema:validFor ;
+ sh:severity sh:Warning ;
+ sh:or (
+ [ sh:datatype ]
+ [ sh:datatype ]
+ [ sh:datatype ]
+ [ sh:class schema:Duration ]
+ ) ;
+ sh:message "Schema.org range check: validFor." ;
+ ] ;
+ sh:property [
+ sh:path schema:validIn ;
+ sh:severity sh:Warning ;
+ sh:or (
+ [ sh:datatype ]
+ [ sh:datatype ]
+ [ sh:datatype ]
+ [ sh:class schema:AdministrativeArea ]
+ ) ;
+ sh:message "Schema.org range check: validIn." ;
+ ] ;
+.
+
:schema_DDxElementShape
a sh:NodeShape ;
sh:targetClass schema:DDxElement ;
@@ -5057,8 +5106,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:MeasurementMethodEnum ]
[ sh:class schema:DefinedTerm ]
+ [ sh:class schema:MeasurementMethodEnum ]
) ;
sh:message "Schema.org range check: measurementMethod." ;
] ;
@@ -5069,8 +5118,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:MeasurementMethodEnum ]
[ sh:class schema:DefinedTerm ]
+ [ sh:class schema:MeasurementMethodEnum ]
) ;
sh:message "Schema.org range check: measurementTechnique." ;
] ;
@@ -5086,8 +5135,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:MeasurementMethodEnum ]
[ sh:class schema:DefinedTerm ]
+ [ sh:class schema:MeasurementMethodEnum ]
) ;
sh:message "Schema.org range check: measurementMethod." ;
] ;
@@ -5098,8 +5147,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:MeasurementMethodEnum ]
[ sh:class schema:DefinedTerm ]
+ [ sh:class schema:MeasurementMethodEnum ]
) ;
sh:message "Schema.org range check: measurementTechnique." ;
] ;
@@ -5132,8 +5181,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:datatype ]
[ sh:datatype ]
+ [ sh:datatype ]
) ;
sh:message "Schema.org range check: dateCreated." ;
] ;
@@ -5144,8 +5193,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:datatype ]
[ sh:datatype ]
+ [ sh:datatype ]
) ;
sh:message "Schema.org range check: dateDeleted." ;
] ;
@@ -5249,8 +5298,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:MeasurementMethodEnum ]
[ sh:class schema:DefinedTerm ]
+ [ sh:class schema:MeasurementMethodEnum ]
) ;
sh:message "Schema.org range check: measurementMethod." ;
] ;
@@ -5261,8 +5310,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:MeasurementMethodEnum ]
[ sh:class schema:DefinedTerm ]
+ [ sh:class schema:MeasurementMethodEnum ]
) ;
sh:message "Schema.org range check: measurementTechnique." ;
] ;
@@ -5291,10 +5340,10 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
+ [ sh:class schema:MonetaryAmount ]
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype ]
- [ sh:class schema:MonetaryAmount ]
) ;
sh:message "Schema.org range check: amount." ;
] ;
@@ -5315,8 +5364,8 @@
[ sh:datatype ]
[ sh:datatype ]
[ sh:datatype