You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sourcelume.apache.org by jg...@apache.org on 2026/09/05 18:36:53 UTC

(sourcelume-spec) 01/02: Initial content for context, schema, and specification. Initial tooling for validation.

This is an automated email from the ASF dual-hosted git repository.

jgoodyear pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/sourcelume-spec.git

commit 24a31fd13bb7bbae0215ff12eb5fd4560ee5ffb3
Author: jgoodyear <jg...@apache.org>
AuthorDate: Sat Sep 5 16:05:54 2026 -0230

    Initial content for context, schema, and specification. Initial tooling for validation.
    
    Apache-ai: Yes
    Generated-by: PyCharm AI Assistant (Claude 4.5 Sonnet)
    Reviewed-by: jgoodyear
---
 .gitignore                          |   3 +
 SETUP.md                            |  23 +++++
 VERSIONING.md                       |  17 ++++
 context/0.0.1/sourcelume.jsonld     |  16 ++++
 pyproject.toml                      |  17 ++++
 schema/0.0.1/sourcelume.schema.json |  36 ++++++++
 schema/0.0.1/sourcelume.shacl.ttl   |  52 +++++++++++
 spec/0.0.1/index.md                 |  74 +++++++++++++++
 tests/test_validate.py              | 104 +++++++++++++++++++++
 tools/render-docs.py                | 115 ++++++++++++++++++++++++
 tools/validate.py                   | 175 ++++++++++++++++++++++++++++++++++++
 11 files changed, 632 insertions(+)

diff --git a/.gitignore b/.gitignore
index c8e7871..eeac8aa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,6 +13,9 @@ __pycache__/
 # Maven / Java build output
 target/
 
+# Generated docs (see tools/render-docs.py)
+spec/*/generated-field-reference.md
+
 # Editor/OS noise
 .vscode/
 *.swp
\ No newline at end of file
diff --git a/SETUP.md b/SETUP.md
index ed08b0c..b12eb1d 100644
--- a/SETUP.md
+++ b/SETUP.md
@@ -1,3 +1,20 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
 # Apache Sourcelume Spec — Development Setup
 
 This document describes how to set up a local development environment for the
@@ -93,6 +110,12 @@ Example:
 uv run python tools/validate.py
 ```
 
+To test the validation tooling:
+```bash
+uv run python -m unittest discover -s tests
+```
+
+
 The validation tooling is expected to check things such as:
 
 - JSON-LD example syntax
diff --git a/VERSIONING.md b/VERSIONING.md
index 823d3fe..9d3cf12 100644
--- a/VERSIONING.md
+++ b/VERSIONING.md
@@ -1,3 +1,20 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
 # Versioning
  
 This project follows [Semantic Versioning](https://semver.org)
diff --git a/context/0.0.1/sourcelume.jsonld b/context/0.0.1/sourcelume.jsonld
new file mode 100644
index 0000000..35abea4
--- /dev/null
+++ b/context/0.0.1/sourcelume.jsonld
@@ -0,0 +1,16 @@
+{
+  "@context": {
+    "@version": 1.1,
+    "sl": "https://sourcelume.apache.org/ns/0.0.1#",
+    "id": "@id",
+    "type": "@type",
+
+    "name": "sl:name",
+    "description": "sl:description",
+    "version": "sl:version",
+    "createdAt": {
+      "@id": "sl:createdAt",
+      "@type": "http://www.w3.org/2001/XMLSchema#dateTime"
+    }
+  }
+}
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index a7d7892..69f6a23 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,3 +1,20 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
 [project]
 name = "sourcelume-spec"
 version = "0.0.1"
diff --git a/schema/0.0.1/sourcelume.schema.json b/schema/0.0.1/sourcelume.schema.json
new file mode 100644
index 0000000..5a682f7
--- /dev/null
+++ b/schema/0.0.1/sourcelume.schema.json
@@ -0,0 +1,36 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "https://sourcelume.apache.org/schema/0.0.1/sourcelume.schema.json",
+  "title": "Apache Sourcelume Record",
+  "description": "Minimal skeleton schema for an Apache Sourcelume record. Fields and constraints will be filled in as the specification develops.",
+  "type": "object",
+  "properties": {
+    "id": {
+      "type": "string",
+      "description": "Unique identifier (IRI) for this record."
+    },
+    "type": {
+      "type": "string",
+      "description": "The record type."
+    },
+    "name": {
+      "type": "string",
+      "description": "Human-readable name for the record."
+    },
+    "description": {
+      "type": "string",
+      "description": "Free-text description of the record."
+    },
+    "version": {
+      "type": "string",
+      "description": "Version of the record content, e.g. following semver."
+    },
+    "createdAt": {
+      "type": "string",
+      "format": "date-time",
+      "description": "Creation timestamp, ISO 8601."
+    }
+  },
+  "required": ["id", "type"],
+  "additionalProperties": true
+}
\ No newline at end of file
diff --git a/schema/0.0.1/sourcelume.shacl.ttl b/schema/0.0.1/sourcelume.shacl.ttl
new file mode 100644
index 0000000..ac577f0
--- /dev/null
+++ b/schema/0.0.1/sourcelume.shacl.ttl
@@ -0,0 +1,52 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+@prefix sh: <http://www.w3.org/ns/shacl#> .
+@prefix sl: <https://sourcelume.apache.org/ns/0.0.1#> .
+@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
+
+# Minimal skeleton SHACL shape for a Sourcelume record.
+# Constraints will be expanded as the specification develops, and should
+# stay aligned with schema/0.0.1/sourcelume.schema.json.
+
+sl:RecordShape
+    a sh:NodeShape ;
+    sh:targetClass sl:Record ;
+
+    sh:property [
+        sh:path sl:name ;
+        sh:datatype xsd:string ;
+        sh:maxCount 1 ;
+    ] ;
+
+    sh:property [
+        sh:path sl:description ;
+        sh:datatype xsd:string ;
+        sh:maxCount 1 ;
+    ] ;
+
+    sh:property [
+        sh:path sl:version ;
+        sh:datatype xsd:string ;
+        sh:maxCount 1 ;
+    ] ;
+
+    sh:property [
+        sh:path sl:createdAt ;
+        sh:datatype xsd:dateTime ;
+        sh:maxCount 1 ;
+    ] .
\ No newline at end of file
diff --git a/spec/0.0.1/index.md b/spec/0.0.1/index.md
new file mode 100644
index 0000000..a2dd094
--- /dev/null
+++ b/spec/0.0.1/index.md
@@ -0,0 +1,74 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+# Apache Sourcelume Specification — v0.0.1
+
+> **Status:** Draft / skeleton. This version is a starting point for
+> development and has not been released. Content will be filled in as the
+> specification develops. See `VERSIONING.md` for how this version number
+> relates to changes in `context/` and `schema/`.
+
+## 1. Introduction
+
+Sourcelume defines a vocabulary and data model for describing
+[fill in: what Sourcelume records represent, and the problem the spec
+solves].
+
+## 2. Terminology
+
+This section defines terms used throughout the specification.
+
+- **Record** — [definition]
+
+## 3. Data model
+
+This section describes the structure of a Sourcelume record.
+
+Normative field definitions live in the corresponding JSON Schema
+(`schema/0.0.1/sourcelume.schema.json`) and SHACL shapes
+(`schema/0.0.1/sourcelume.shacl.ttl`). This document should describe the
+*meaning* and intended usage of each field; the schema/shapes describe the
+*constraints*.
+
+| Field         | Type     | Required | Description                          |
+|---------------|----------|----------|---------------------------------------|
+| `id`          | IRI      | Yes      | Unique identifier for the record.     |
+| `type`        | string   | Yes      | The record type.                      |
+| `name`        | string   | No       | Human-readable name.                  |
+| `description` | string   | No       | Free-text description.                |
+| `version`     | string   | No       | Version of the record content.        |
+| `createdAt`   | datetime | No       | Creation timestamp (ISO 8601).        |
+
+## 4. JSON-LD context
+
+Sourcelume records are expressed as JSON-LD using the context published at
+`context/0.0.1/sourcelume.jsonld`.
+
+## 5. Examples
+
+See `examples/` for sample records conforming to this version of the
+specification.
+
+## 6. Conformance
+
+[fill in: what it means for a document/tool to conform to this version of
+the spec]
+
+## 7. Changes from previous versions
+
+This is the first version of the specification; there is no prior version
+to compare against.
\ No newline at end of file
diff --git a/tests/test_validate.py b/tests/test_validate.py
new file mode 100644
index 0000000..58fe3db
--- /dev/null
+++ b/tests/test_validate.py
@@ -0,0 +1,104 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+from __future__ import annotations
+
+import importlib.util
+import json
+import tempfile
+import unittest
+from pathlib import Path
+
+
+REPO_ROOT = Path(__file__).resolve().parent.parent
+VALIDATE_PATH = REPO_ROOT / "tools" / "validate.py"
+
+
+def load_validate_module():
+    spec = importlib.util.spec_from_file_location("validate", VALIDATE_PATH)
+    if spec is None or spec.loader is None:
+        raise RuntimeError(f"Could not load {VALIDATE_PATH}")
+
+    module = importlib.util.module_from_spec(spec)
+    spec.loader.exec_module(module)
+    return module
+
+
+validate = load_validate_module()
+
+
+class ValidateToolTests(unittest.TestCase):
+    def test_find_version_dirs_returns_only_semver_dirs(self) -> None:
+        with tempfile.TemporaryDirectory() as tmp:
+            base = Path(tmp)
+
+            (base / "0.0.1").mkdir()
+            (base / "1.2.3").mkdir()
+            (base / "latest").mkdir()
+            (base / "1-draft").mkdir()
+            (base / "2026-old").mkdir()
+            (base / "not-a-version").mkdir()
+
+            version_dirs = validate.find_version_dirs(base)
+
+            self.assertEqual(
+                [path.name for path in version_dirs],
+                ["0.0.1", "1.2.3"],
+            )
+
+    def test_find_version_dirs_returns_empty_for_missing_base(self) -> None:
+        with tempfile.TemporaryDirectory() as tmp:
+            missing = Path(tmp) / "missing"
+
+            self.assertEqual(validate.find_version_dirs(missing), [])
+
+    def test_check_json_file_accepts_valid_json(self) -> None:
+        with tempfile.TemporaryDirectory() as tmp:
+            path = Path(tmp) / "valid.jsonld"
+            path.write_text(json.dumps({"@context": {}, "id": "example"}), encoding="utf-8")
+
+            self.assertEqual(validate.check_json_file(path), [])
+
+    def test_check_json_file_reports_invalid_json(self) -> None:
+        with tempfile.TemporaryDirectory() as tmp:
+            path = Path(tmp) / "invalid.jsonld"
+            path.write_text("{ invalid json", encoding="utf-8")
+
+            errors = validate.check_json_file(path)
+
+            self.assertEqual(len(errors), 1)
+            self.assertIn("invalid JSON", errors[0])
+
+    def test_require_file_accepts_existing_file(self) -> None:
+        with tempfile.TemporaryDirectory() as tmp:
+            path = Path(tmp) / "asset.json"
+            path.write_text("{}", encoding="utf-8")
+
+            self.assertEqual(validate.require_file(path), [])
+
+    def test_require_file_reports_missing_file(self) -> None:
+        with tempfile.TemporaryDirectory() as tmp:
+            path = Path(tmp) / "missing.json"
+
+            errors = validate.require_file(path)
+
+            self.assertEqual(len(errors), 1)
+            self.assertIn("missing required file", errors[0])
+
+
+if __name__ == "__main__":
+    unittest.main()
\ No newline at end of file
diff --git a/tools/render-docs.py b/tools/render-docs.py
new file mode 100644
index 0000000..df4b35e
--- /dev/null
+++ b/tools/render-docs.py
@@ -0,0 +1,115 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""Render human-readable docs from the JSON Schema.
+
+This is a starting skeleton. It currently generates a single Markdown
+field-reference table per published schema version, from
+`schema/<version>/sourcelume.schema.json`. It does not yet render SHACL
+shapes, cross-link to the prose spec, or produce HTML output — see
+SETUP.md for the intended scope.
+
+Usage:
+    uv run python tools/render-docs.py
+
+Output is written to `spec/<version>/generated-field-reference.md` for
+each published schema version. Generated files are not intended to be
+committed until the project decides how generated output should be
+handled (see the "Notes for contributors" section of SETUP.md).
+Note: gitIgnores spec/*/generated-field-reference.md entry exists.
+"""
+
+from __future__ import annotations
+
+import json
+import sys
+from pathlib import Path
+
+REPO_ROOT = Path(__file__).resolve().parent.parent
+SCHEMA_DIR = REPO_ROOT / "schema"
+SPEC_DIR = REPO_ROOT / "spec"
+
+# Find published schema version directories that should have docs generated.
+def find_version_dirs(base: Path) -> list[Path]:
+    """Return published version directories under `base` (e.g. schema/0.0.1)."""
+    if not base.is_dir():
+        return []
+    return sorted(
+        p for p in base.iterdir()
+        if p.is_dir() and not p.is_symlink() and p.name[0].isdigit()
+    )
+
+
+# Build a Markdown field-reference table from a JSON Schema dictionary.
+def render_field_table(schema: dict) -> str:
+    properties = schema.get("properties", {})
+    required = set(schema.get("required", []))
+
+    lines = [
+        f"# {schema.get('title', 'Schema Reference')}",
+        "",
+        "> Generated from the JSON Schema. Do not edit by hand — edit the",
+        "> schema and re-run `tools/render-docs.py` instead.",
+        "",
+        "| Field | Type | Required | Description |",
+        "|-------|------|----------|-------------|",
+    ]
+
+    for field_name, field_def in properties.items():
+        field_type = field_def.get("type", field_def.get("format", "—"))
+        is_required = "Yes" if field_name in required else "No"
+        description = field_def.get("description", "").replace("\n", " ")
+        lines.append(f"| `{field_name}` | {field_type} | {is_required} | {description} |")
+
+    return "\n".join(lines) + "\n"
+
+
+# Render generated documentation for one schema version and return the output path.
+def render_version(version_dir: Path) -> Path | None:
+    schema_files = list(version_dir.glob("*.schema.json"))
+    if not schema_files:
+        return None
+
+    schema = json.loads(schema_files[0].read_text())
+    doc = render_field_table(schema)
+
+    output_dir = SPEC_DIR / version_dir.name
+    output_dir.mkdir(parents=True, exist_ok=True)
+    output_path = output_dir / "generated-field-reference.md"
+    output_path.write_text(doc)
+    return output_path
+
+
+# Generate documentation for all available schema versions and return an exit code.
+def main() -> int:
+    version_dirs = find_version_dirs(SCHEMA_DIR)
+
+    if not version_dirs:
+        print("No schema versions found under schema/.")
+        return 0
+
+    for version_dir in version_dirs:
+        output_path = render_version(version_dir)
+        if output_path:
+            print(f"Wrote {output_path}")
+        else:
+            print(f"Skipped {version_dir}: no *.schema.json file found.")
+
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/tools/validate.py b/tools/validate.py
new file mode 100644
index 0000000..9528b65
--- /dev/null
+++ b/tools/validate.py
@@ -0,0 +1,175 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""Validate Sourcelume specification assets.
+
+This is a starting skeleton. It currently checks that the versioned
+schema, context, and SHACL files for each published version are present
+and syntactically valid (valid JSON / valid Turtle). It does not yet
+validate examples against the schema/SHACL, or check cross-version
+consistency — see SETUP.md for the full intended scope.
+
+Usage:
+    uv run python tools/validate.py
+"""
+
+from __future__ import annotations
+
+import json
+import re
+import sys
+from pathlib import Path
+
+VERSION_DIR_PATTERN = re.compile(r"^\d+\.\d+\.\d+$")
+
+REPO_ROOT = Path(__file__).resolve().parent.parent
+CONTEXT_DIR = REPO_ROOT / "context"
+SCHEMA_DIR = REPO_ROOT / "schema"
+EXAMPLES_DIR = REPO_ROOT / "examples"
+
+# Convert a filesystem path to a repository-relative string when possible.
+def display_path(path: Path) -> str:
+    """Return `path` relative to the repository root when possible."""
+    try:
+        return str(path.relative_to(REPO_ROOT))
+    except ValueError:
+        return str(path)
+
+
+# Check that a required path exists and is a regular file.
+def require_file(path: Path) -> list[str]:
+    """Return an error if `path` is not a regular file."""
+    if not path.is_file():
+        return [f"{display_path(path)}: missing required file"]
+    return []
+
+
+# Find all published version directories whose names match the expected SemVer format.
+def find_version_dirs(base: Path) -> list[Path]:
+    """Return published SemVer-like version directories under `base`."""
+    if not base.is_dir():
+        return []
+    return sorted(
+        p for p in base.iterdir()
+        if p.is_dir() and not p.is_symlink() and VERSION_DIR_PATTERN.fullmatch(p.name)
+    )
+
+
+# Validate that a file can be read and parsed as JSON.
+def check_json_file(path: Path) -> list[str]:
+    errors = []
+    try:
+        json.loads(path.read_text(encoding="utf-8"))
+    except json.JSONDecodeError as exc:
+        errors.append(f"{display_path(path)}: invalid JSON ({exc})")
+    except OSError as exc:
+        errors.append(f"{display_path(path)}: could not read file ({exc})")
+    return errors
+
+
+# Validate that a Turtle file can be read and parsed with rdflib.
+def check_turtle_file(path: Path) -> list[str]:
+    errors = []
+    try:
+        import rdflib
+    except ImportError as exc:
+        errors.append(f"{display_path(path)}: cannot validate Turtle because rdflib is not installed ({exc})")
+        return errors
+
+    try:
+        rdflib.Graph().parse(str(path), format="turtle")
+    except OSError as exc:
+        errors.append(f"{display_path(path)}: could not read file ({exc})")
+    except Exception as exc:  # noqa: BLE001 - report any parse failure
+        errors.append(f"{display_path(path)}: invalid Turtle ({exc})")
+    return errors
+
+
+# Validate required schema, context, and SHACL assets for each published version.
+def validate_schema_and_context() -> list[str]:
+    errors: list[str] = []
+
+    context_version_dirs = find_version_dirs(CONTEXT_DIR)
+    schema_version_dirs = find_version_dirs(SCHEMA_DIR)
+
+    context_versions = {version_dir.name for version_dir in context_version_dirs}
+    schema_versions = {version_dir.name for version_dir in schema_version_dirs}
+
+    for version in sorted(context_versions - schema_versions):
+        errors.append(
+            f"{display_path(SCHEMA_DIR / version)}: missing schema directory for context version {version}"
+        )
+
+    for version in sorted(schema_versions - context_versions):
+        errors.append(
+            f"{display_path(CONTEXT_DIR / version)}: missing context directory for schema version {version}"
+        )
+
+    for version_dir in context_version_dirs:
+        context_file = version_dir / "sourcelume.jsonld"
+
+        errors.extend(require_file(context_file))
+        if context_file.is_file():
+            errors.extend(check_json_file(context_file))
+
+    for version_dir in schema_version_dirs:
+        schema_file = version_dir / "sourcelume.schema.json"
+        shacl_file = version_dir / "sourcelume.shacl.ttl"
+
+        errors.extend(require_file(schema_file))
+        errors.extend(require_file(shacl_file))
+
+        if schema_file.is_file():
+            errors.extend(check_json_file(schema_file))
+        if shacl_file.is_file():
+            errors.extend(check_turtle_file(shacl_file))
+
+    return errors
+
+
+# Validate example JSON-LD files when an examples directory is present.
+def validate_examples() -> list[str]:
+    errors: list[str] = []
+    if not EXAMPLES_DIR.is_dir():
+        return errors
+
+    for example_file in EXAMPLES_DIR.rglob("*.jsonld"):
+        if example_file.is_file() and not example_file.is_symlink():
+            errors.extend(check_json_file(example_file))
+
+    # TODO: validate each example against the corresponding version's
+    # schema (jsonschema) and SHACL shapes (pyshacl), once example files
+    # and a real data model exist.
+
+    return errors
+
+
+# Run all validation checks and return a process exit code.
+def main() -> int:
+    errors = validate_schema_and_context() + validate_examples()
+
+    if errors:
+        print("Validation failed:")
+        for error in errors:
+            print(f"  - {error}")
+        return 1
+
+    print("Validation passed (skeleton checks only; see TODOs in this file).")
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())
\ No newline at end of file


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@sourcelume.apache.org
For additional commands, e-mail: commits-help@sourcelume.apache.org