From 09d8f9cce5d11c2b723cbfbd3efdc57cf72a8207 Mon Sep 17 00:00:00 2001 From: Lukas Gold Date: Wed, 2 Sep 2026 13:06:42 +0200 Subject: [PATCH 1/3] docs(validation): give the context walker a true justification - pyld does expose the flattened context via public process_context - what it does not do is resolve a term's scoped @context - callers expand with no document loader, so it cannot resolve later - successive calls share no memory, so the cycle bound stays here --- src/oold/validation/context_resolution.py | 25 ++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/oold/validation/context_resolution.py b/src/oold/validation/context_resolution.py index a767ad7..8d2f849 100644 --- a/src/oold/validation/context_resolution.py +++ b/src/oold/validation/context_resolution.py @@ -7,10 +7,9 @@ exactly what OO-LD's rule ``OOLD-CMP-b926`` guarantees by requiring a schema to be directly usable as a context. -What a processor does not expose is the *flattened active context itself* as a value a caller can -inspect. Expanding a document tells you the resulting triples, not which terms were in scope or -where each came from. This module exists for the callers that need that: reporting which terms a -schema defines, and the per-property attribution in :mod:`~oold.validation.predicates`. +The callers here need the flattened active context as a *value*: which terms a schema defines, +and the per-property attribution in :mod:`~oold.validation.predicates`. Expanding a document +gives the resulting triples, not that. ``@context`` entries are usually relative siblings, referencing *other OO-LD schemas*:: @@ -28,9 +27,21 @@ of context objects rather than being merged by hand, so JSON-LD's own override semantics still apply. -Replacing this walk with a JSON-LD processor's own context resolution is worth evaluating, if a -future need exposes that flattened form through a stable API; this module exists because none -does today, not because a processor could not in principle resolve the chain itself. +A processor does expose that value. pyld returns it from the public +``JsonLdProcessor.process_context``, one hop at a time, carrying ``@type``, ``@container``, +``protected``, ``reverse`` and prefix flags per term; an earlier version of this docstring said +no processor did, and that was wrong (issue #118). Two things keep the walk here anyway. + +``process_context`` never resolves a term's *scoped* ``@context``. It leaves the value exactly as +authored and defers it to expansion, so ``{"@id": ..., "@context": "Pet.schema.json"}`` survives +as an unresolved reference. The callers here expand without a document loader, so by the time +that reference is reached there is nothing left to resolve it against. Something has to embed the +scoped content eagerly, which is most of what :func:`_resolve_inline` does. + +And nothing in the processor stops a caller re-entering it around a reference cycle: successive +calls share no memory of prior hops. The stack in :func:`_walk_reference` and ``max_scoped_depth`` +in :func:`_resolve_inline` are that bound. No committed fixture is cyclic, so a replacement could +not be validated against the corpus here either. """ from __future__ import annotations From 74ab2f9c287e188d21e9f0a41b8bcd60fc2bc72a Mon Sep 17 00:00:00 2001 From: SimonTaurus Date: Fri, 11 Sep 2026 12:24:26 +0200 Subject: [PATCH 2/3] docs(validation): correct how pyld actually fails on each count Both conclusions held, both mechanisms were wrong. process_context does not defer a scoped @context to expansion; it resolves eagerly and keeps nothing, raising invalid scoped context when no loader is available. And pyld does detect cyclic @context URLs within one call - what defeats the guard is driving the chain a hop at a time, which is what provenance requires. --- src/oold/validation/context_resolution.py | 24 +++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/oold/validation/context_resolution.py b/src/oold/validation/context_resolution.py index 8d2f849..6dc5e19 100644 --- a/src/oold/validation/context_resolution.py +++ b/src/oold/validation/context_resolution.py @@ -32,16 +32,20 @@ ``protected``, ``reverse`` and prefix flags per term; an earlier version of this docstring said no processor did, and that was wrong (issue #118). Two things keep the walk here anyway. -``process_context`` never resolves a term's *scoped* ``@context``. It leaves the value exactly as -authored and defers it to expansion, so ``{"@id": ..., "@context": "Pet.schema.json"}`` survives -as an unresolved reference. The callers here expand without a document loader, so by the time -that reference is reached there is nothing left to resolve it against. Something has to embed the -scoped content eagerly, which is most of what :func:`_resolve_inline` does. - -And nothing in the processor stops a caller re-entering it around a reference cycle: successive -calls share no memory of prior hops. The stack in :func:`_walk_reference` and ``max_scoped_depth`` -in :func:`_resolve_inline` are that bound. No committed fixture is cyclic, so a replacement could -not be validated against the corpus here either. +``process_context`` resolves a term's *scoped* ``@context`` and then discards it. The fetch is +eager, through the document loader, and without one it raises ``invalid scoped context`` rather +than deferring anything; but the term mapping it returns still carries the value as authored, so +``{"@id": ..., "@context": "Pet.schema.json"}`` comes back with that string intact and nothing +reachable behind it. Something has to embed the scoped content itself, which is most of what +:func:`_resolve_inline` does. + +And pyld's own cycle guard does not survive the way this module has to call it. One +``process_context`` over a loop raises ``Cyclical @context URLs detected``, but attributing a term +to the hop that defined it means driving the chain a hop at a time, and successive calls share no +memory of prior hops. Provenance is therefore what costs the guard. The stack in +:func:`_walk_reference` and ``max_scoped_depth`` in :func:`_resolve_inline` replace it. No +committed fixture is cyclic, so a replacement could not be validated against the corpus here +either. """ from __future__ import annotations From 6f171836bbafccc636dab6815397e67d275bed0f Mon Sep 17 00:00:00 2001 From: SimonTaurus Date: Fri, 11 Sep 2026 12:27:57 +0200 Subject: [PATCH 3/3] test(validation): pin the pyld equivalence the docstring argues from The justification names what pyld does not return; that only holds while the two agree on everything else, and nothing checked it. Both are facts now: the effective term set matches across all 13 committed schemas, and a scoped @context comes back from process_context exactly as authored. Where a caller needs only the effective mapping, process_context is the better source - it applies JSON-LD's override semantics rather than terms()'s later-key-wins flatten. --- tests/test_validation/test_jsonld.py | 70 ++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/tests/test_validation/test_jsonld.py b/tests/test_validation/test_jsonld.py index a280294..6cf5a61 100644 --- a/tests/test_validation/test_jsonld.py +++ b/tests/test_validation/test_jsonld.py @@ -221,6 +221,76 @@ def test_unresolvable_context_reference_is_reported(resolver, broken_dir): assert context.errors and "NoSuchSchema" in context.errors[0] +def test_the_walker_and_pyld_agree_on_the_effective_term_set(resolver, data_dir): + """Pins the claim the module docstring rests on, across the whole committed corpus. + + The docstring justifies a hand-written walk by naming what pyld does *not* give back: the + scoped `@context` content, and a cycle bound across hop-by-hop calls. That argument is only + honest while the two agree on everything else. Where a caller needs the effective mapping and + nothing more, `process_context` is the better source - it applies JSON-LD's own override + semantics rather than `terms()`'s later-key-wins flatten - and a divergence here is the signal + that the justification needs revisiting rather than repeating. + + Key sets, not values: this module reports terms as authored (`schema:name`), pyld reports them + resolved (`http://schema.org/name`). + """ + from pyld.jsonld import JsonLdProcessor + + from oold.validation.loader import DocumentLoader + + loader = DocumentLoader(resolver, directory=data_dir) + processor = JsonLdProcessor() + # pyld reads the processing mode off the *initial* context, not the per-call options, and + # defaulting to 1.0 rejects every scoped context as "a term definition must not contain + # @context" - a failure of the harness that reads exactly like a failure of the subject. + options = {"processingMode": "json-ld-1.1"} + + compared = 0 + for path in sorted(data_dir.glob("*.schema.json")): + loaded = resolver.load(path) + if "@context" not in loaded.schema: + continue + context = resolve_context(loaded.schema, loaded.base_uri, resolver) + if context.errors or context.is_empty: + continue + + active = processor.process_context( + processor._get_initial_context(options), + context.as_jsonld(), + {**options, "base": loader.url_for(path.name), "documentLoader": loader}, + ) + theirs = {term for term in active["mappings"] if not term.startswith("@")} + assert set(context.terms()) == theirs, path.name + compared += 1 + + assert compared >= 13, f"only {compared} schemas carried a resolvable @context" + + +def test_pyld_keeps_a_scoped_context_unresolved(resolver, data_dir): + """The specific capability the walker adds, as a fact rather than an assertion in prose. + + `process_context` resolves a scoped `@context` eagerly and then discards the result, so the + term mapping still holds whatever was authored. Embedding it is `_resolve_inline`'s job. + """ + from pyld.jsonld import JsonLdProcessor + + from oold.validation.loader import DocumentLoader + + loaded = resolver.load(data_dir / "PersonWithPet.schema.json") + context = resolve_context(loaded.schema, loaded.base_uri, resolver) + loader = DocumentLoader(resolver, directory=data_dir) + options = {"processingMode": "json-ld-1.1"} + + active = JsonLdProcessor().process_context( + JsonLdProcessor()._get_initial_context(options), + context.as_jsonld(), + {**options, "base": loader.url_for("PersonWithPet.schema.json"), "documentLoader": loader}, + ) + # The walker embedded Pet's terms; pyld hands back the same value it was given and no way to + # reach behind it. + assert active["mappings"]["pets"]["@context"] == context.terms()["pets"]["@context"] + + def test_find_alias_keys_discovers_id_and_type_terms(): assert find_alias_keys({"id": "@id", "type": "@type"}) == ("id", "type") assert find_alias_keys({"identifier": {"@id": "@id"}}) == ("identifier", "@type")