Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ described in `VERSIONING.md`.

## [Unreleased]

### Changed

- Namespace moved to the stable `https://sourcelume.apache.org/ns#`. The context URL stays
versioned (`/context/0.0.1/`).
- The JSON Schema accepts only the canonical context IRI as `@context`. Inline context objects
and other IRIs are rejected.
- `tools/validate.py` resolves the canonical context from the bundled copy instead of fetching it.

### Added

- Added intended usage for custodyChain in spec.
Expand Down
2 changes: 1 addition & 1 deletion context/0.0.1/sourcelume.jsonld
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"@context": {
"@version": 1.1,
"sl": "https://sourcelume.apache.org/ns/0.0.1#",
"sl": "https://sourcelume.apache.org/ns#",
"dct": "http://purl.org/dc/terms/",
"schema": "https://schema.org/",
"spdx": "http://spdx.org/rdf/terms#",
Expand Down
5 changes: 4 additions & 1 deletion schema/0.0.1/sourcelume.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@
"custodyChain"
],
"properties": {
"@context": {},
"@context": {
"const": "https://sourcelume.apache.org/context/0.0.1/sourcelume.jsonld",
"description": "Only the canonical context IRI is accepted. Inline @context objects and any other IRI are rejected, so a record's meaning is fixed by the published context."
},
"id": {
"type": "string",
"format": "uri"
Expand Down
2 changes: 1 addition & 1 deletion schema/0.0.1/sourcelume.shacl.ttl
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix sl: <https://sourcelume.apache.org/ns/0.0.1#> .
@prefix sl: <https://sourcelume.apache.org/ns#> .
@prefix dct: <http://purl.org/dc/terms/> .
@prefix schema: <https://schema.org/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
Expand Down
8 changes: 8 additions & 0 deletions spec/0.0.1/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ this identity, was produced/curated/distributed by these parties, is licensed un
came from this origin, and passed through this chain of custody.* Sourcelume does not verify the
claim's accuracy — it gives the claim a consistent shape so it *can* be verified by someone else.

## JSON-LD context

Records reference the context with the canonical IRI
`https://sourcelume.apache.org/context/0.0.1/sourcelume.jsonld` and no other `@context` value.
The context is published at that address and is not modified after release. Its namespace is the
stable `https://sourcelume.apache.org/ns#`, so term IRIs stay the same across versions. Inline
`@context` objects and other context IRIs are rejected.

## Fields

| Field | Required | Type | Description |
Expand Down
32 changes: 26 additions & 6 deletions tests/test_validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,9 @@ def test_validate_example_with_valid_record(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
example_path = Path(tmp) / "valid.jsonld"

# Load the context from the local file
context_path = REPO_ROOT / "context" / "0.0.1" / "sourcelume.jsonld"
context = json.loads(context_path.read_text())

# Create a minimal valid ProvenanceRecord with inline context
# Create a minimal valid ProvenanceRecord with the canonical context IRI
valid_record = {
"@context": context["@context"],
"@context": validate.CANONICAL_CONTEXT_IRI,
"id": "https://example.org/records/test",
"type": "ProvenanceRecord",
"identifier": "https://example.org/datasets/test",
Expand Down Expand Up @@ -82,6 +78,30 @@ def test_validate_example_with_valid_record(self) -> None:
result = validate.validate_example(example_path, validator, shapes_path)
self.assertTrue(result)

def test_schema_pins_the_canonical_context_iri(self) -> None:
"""The schema's @context const and the validator's constant must agree."""
schema = validate.load_validator().schema
self.assertEqual(schema["properties"]["@context"]["const"], validate.CANONICAL_CONTEXT_IRI)

def test_inline_context_is_rejected(self) -> None:
"""An inline @context object is not accepted, only the canonical IRI."""
validator = validate.load_validator()
context = json.loads((REPO_ROOT / "context" / "0.0.1" / "sourcelume.jsonld").read_text())
record = {"@context": context["@context"], "id": "https://example.org/records/test", "type": "ProvenanceRecord"}
errors = [e for e in validator.iter_errors(record) if list(e.path) == ["@context"]]
self.assertEqual(len(errors), 1)

def test_foreign_context_iri_is_rejected(self) -> None:
"""A context IRI on another host is rejected, even if the path looks the same."""
validator = validate.load_validator()
record = {
"@context": "https://example.org/context/0.0.1/sourcelume.jsonld",
"id": "https://example.org/records/test",
"type": "ProvenanceRecord",
}
errors = [e for e in validator.iter_errors(record) if list(e.path) == ["@context"]]
self.assertEqual(len(errors), 1)

def test_validate_example_with_invalid_json_schema(self) -> None:
"""Test that an invalid record fails JSON Schema validation."""
with tempfile.TemporaryDirectory() as tmp:
Expand Down
7 changes: 6 additions & 1 deletion tools/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
BASE = Path(__file__).resolve().parent.parent
SCHEMA_DIR = BASE / "schema" / SPEC_VERSION
EXAMPLES_DIR = BASE / "examples"
CANONICAL_CONTEXT_IRI = "https://sourcelume.apache.org/context/0.0.1/sourcelume.jsonld"
CONTEXT_FILE = BASE / "context" / SPEC_VERSION / "sourcelume.jsonld"


def load_validator() -> Draft202012Validator:
Expand All @@ -46,8 +48,11 @@ def validate_example(example: Path, validator: Draft202012Validator, shapes_path
print(f" - {list(err.path)}: {err.message}")
return False

# The canonical context IRI is resolved from the bundled copy, never fetched.
# The schema has already guaranteed that @context is exactly that IRI.
context = json.loads(CONTEXT_FILE.read_text())["@context"]
data_graph = Graph()
data_graph.parse(str(example), format="json-ld")
data_graph.parse(data=json.dumps({**doc, "@context": context}), format="json-ld")

conforms, _, results_text = shacl_validate(
data_graph,
Expand Down
Loading