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
23 changes: 22 additions & 1 deletion docs/standards/coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Generated from [requirements.json](requirements.json) by `python scripts/check_requirements.py --render`.
The default command checks this table without modifying it.

49 supported requirements; 5 explicit profile exclusions; 905 distinct collected tests; 1034 requirement-to-test links.
50 supported requirements; 5 explicit profile exclusions; 907 distinct collected tests; 1036 requirement-to-test links.

Counts describe collected evidence, not executed or passing tests. One test can support several rows.
This is not a percentage of all clauses in the copied specifications or a complete Cartesian test matrix.
Expand Down Expand Up @@ -74,6 +74,7 @@ harness skips are reported by pytest when executing the suite, not hidden by col
| [WL-IDENTITY](#wl-identity) | policy / supported | WL relabelling returns a fresh isomorphic quad list, preserves blank-node cardinality and leaves input quads untouched. | 10 |
| [WL-COMPONENTS](#wl-components) | policy / supported | Unrelated additions, edits and removals do not advance a disconnected component's default refinement, including an added class in a complete generated-schema document. | 11 |
| [WL-TIES](#wl-ties) | policy / supported | Structurally tied nodes remain injective and collision suffixes follow numeric canonical numbering. | 4 |
| [WL-LABEL-FORM](#wl-label-form) | policy / supported | Generated blank-node labels have the documented form: 'b' followed by 12 hex digits, with an optional numeric collision suffix. | 2 |
| [WL-GRAPHS](#wl-graphs) | policy / supported | Named graphs influence labels; blank graph-name identifiers remain consistent when also used as object terms. | 4 |
| [WL-ITERATIONS](#wl-iterations) | policy / supported | Default refinement reaches component fixpoints, explicit rounds remain synchronous, zero is valid and negative counts are refused. | 7 |
| [WL-BOUNDARY](#wl-boundary) | policy / supported | Embedded triple objects are rejected consistently at zero, explicit and default refinement rounds. | 33 |
Expand Down Expand Up @@ -912,6 +913,26 @@ Project contract; no normative standard algorithm is claimed.
| subprocess | Not applicable: This row has no process-state-specific obligation; process determinism is mapped separately. |
| end-to-end | Not applicable: This row isolates a contract dimension; executable composed workflows are mapped in API-EXAMPLES. |

### WL-LABEL-FORM

Generated blank-node labels have the documented form: 'b' followed by 12 hex digits, with an optional numeric collision suffix.

Features: `wl_blank_node_labels`, `wl_relabel_quads`.

Project contract; no normative standard algorithm is claimed.

- local: [`diffable_rdf.wl:wl_blank_node_labels`](../../src/diffable_rdf/wl.py).
- local: [`diffable_rdf.wl:wl_relabel_quads`](../../src/diffable_rdf/wl.py).

| Dimension | Collected evidence or applicability |
| --- | --- |
| positive | `tests/wl/test_wl_contracts.py::test_labels_have_the_documented_form` (1) |
| negative | Not applicable: This is a transformation of admitted values, not a syntax parser or a new input-rejection API. |
| boundary | `tests/wl/test_wl_contracts.py::test_tied_labels_share_one_base_and_add_numeric_suffixes` (1) |
| property | Not applicable: No independent randomized property is claimed for this row; examples establish only the stated dimensions. |
| subprocess | Not applicable: This row has no process-state-specific obligation; process determinism is mapped separately. |
| end-to-end | Not applicable: This row isolates the label form; composed documents are mapped in RDF-ISOMORPHISM and WL-COMPONENTS. |

### WL-GRAPHS

Named graphs influence labels; blank graph-name identifiers remain consistent when also used as object terms.
Expand Down
38 changes: 38 additions & 0 deletions docs/standards/requirements.json
Original file line number Diff line number Diff line change
Expand Up @@ -2120,6 +2120,44 @@
"end-to-end": "This row isolates a contract dimension; executable composed workflows are mapped in API-EXAMPLES."
}
},
{
"id": "WL-LABEL-FORM",
"description": "Generated blank-node labels have the documented form: 'b' followed by 12 hex digits, with an optional numeric collision suffix.",
"features": [
"wl_blank_node_labels",
"wl_relabel_quads"
],
"category": "policy",
"clauses": [],
"owners": [
{
"role": "local",
"target": "diffable_rdf.wl:wl_blank_node_labels",
"path": "src/diffable_rdf/wl.py"
},
{
"role": "local",
"target": "diffable_rdf.wl:wl_relabel_quads",
"path": "src/diffable_rdf/wl.py"
}
],
"status": "supported",
"reason": null,
"evidence": {
"positive": [
"tests/wl/test_wl_contracts.py::test_labels_have_the_documented_form"
],
"boundary": [
"tests/wl/test_wl_contracts.py::test_tied_labels_share_one_base_and_add_numeric_suffixes"
]
},
"not_applicable": {
"negative": "This is a transformation of admitted values, not a syntax parser or a new input-rejection API.",
"property": "No independent randomized property is claimed for this row; examples establish only the stated dimensions.",
"subprocess": "This row has no process-state-specific obligation; process determinism is mapped separately.",
"end-to-end": "This row isolates the label form; composed documents are mapped in RDF-ISOMORPHISM and WL-COMPONENTS."
}
},
{
"id": "WL-GRAPHS",
"description": "Named graphs influence labels; blank graph-name identifiers remain consistent when also used as object terms.",
Expand Down
15 changes: 10 additions & 5 deletions tests/integration/test_naturalistic_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ def rename(term):


def _reordered(graph: Graph) -> Graph:
"""The same graph with its triples inserted in the opposite order."""
"""The same graph with its triples inserted in descending term order."""
reordered = Graph()
for prefix, namespace in graph.namespaces():
reordered.bind(prefix, namespace)
Expand All @@ -171,6 +171,11 @@ def _renderings() -> list:
]


def _render_only() -> list:
"""The same renderings, for checks that never re-parse the output."""
return [pytest.param(case.values[0], id=case.id) for case in _renderings()]


def test_fixture_contains_the_shapes_the_other_tests_rely_on() -> None:
"""Guard the fixture: every hard shape must really be present.

Expand Down Expand Up @@ -218,14 +223,14 @@ def test_every_rendering_is_idempotent(render, parse_format: str) -> None:
assert second == first


@pytest.mark.parametrize(("render", "parse_format"), _renderings())
def test_every_rendering_ignores_incoming_blank_node_names(render, parse_format: str) -> None:
@pytest.mark.parametrize("render", _render_only())
def test_every_rendering_ignores_incoming_blank_node_names(render) -> None:
"""Renaming every blank node must not change a single byte."""
assert render(_relabelled(_schema_graph())) == render(_schema_graph())


@pytest.mark.parametrize(("render", "parse_format"), _renderings())
def test_every_rendering_ignores_insertion_order(render, parse_format: str) -> None:
@pytest.mark.parametrize("render", _render_only())
def test_every_rendering_ignores_insertion_order(render) -> None:
"""Inserting the same triples in another order must not change a single byte."""
assert render(_reordered(_schema_graph())) == render(_schema_graph())

Expand Down
69 changes: 68 additions & 1 deletion tests/wl/test_wl_contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

from __future__ import annotations

import re

import pyoxigraph as ox
import pytest
from rdflib import BNode, Graph, Namespace
from rdflib import BNode, Graph, Literal, Namespace
from rdflib.compare import isomorphic

from diffable_rdf import wl_blank_node_labels, wl_relabel_quads
Expand Down Expand Up @@ -299,3 +301,68 @@ def test_wl_fixpoint_resolves_collisions_that_few_rounds_leave_behind() -> None:

assert any("_" in label for label in four_rounds.values()), "four iterations leave tied chain nodes"
assert all("_" not in label for label in fixpoint.values()), "fixpoint labels require no collision suffix"


#: The label form documented in docs/api.md: ``b`` followed by 12 hex digits,
#: with an optional numeric collision suffix.
_LABEL_FORM = re.compile(r"b[0-9a-f]{12}(?:_[1-9][0-9]*)?")


def _distinct_blank_nodes(count: int) -> Graph:
"""A graph whose blank nodes are all structurally distinguishable."""
graph = Graph()
graph.bind("ex", EX)
for index in range(count):
node = BNode()
graph.add((EX.root, EX.has, node))
graph.add((node, EX.position, Literal(index)))
return graph


def _tied_blank_nodes(names: tuple[str, ...]) -> list:
"""One structurally indistinguishable blank node per input name."""
quads = []
for name in names:
node = ox.BlankNode(name)
quads.append(ox.Quad(ox.NamedNode("http://ex/root"), ox.NamedNode("http://ex/has"), node, ox.DefaultGraph()))
quads.append(ox.Quad(node, ox.NamedNode("http://ex/kind"), ox.Literal("same"), ox.DefaultGraph()))
return quads


def test_labels_have_the_documented_form() -> None:
"""Every label is ``b`` plus exactly 12 hex digits.

This form is documented in docs/api.md, and consumers match on it rather than
merely checking that labels changed: linkml asserts this exact shape in three
separate modules to confirm its ``--diff-stable`` flag reached the serializer.
Widening the hash or changing the prefix therefore breaks them, so the form is
pinned here instead of being discovered downstream.
"""
labels = wl_blank_node_labels(_canonical_quads(_distinct_blank_nodes(4)))

assert len(labels) == 4
assert len(set(labels.values())) == 4
for label in labels.values():
assert _LABEL_FORM.fullmatch(label), label
assert len(label) == 13, "an untied label carries no collision suffix"


def test_tied_labels_share_one_base_and_add_numeric_suffixes() -> None:
"""Tied nodes keep the documented base form and differ only by the suffix."""
names = ("c14n0", "c14n1", "c14n2")
quads = _tied_blank_nodes(names)

labels = wl_blank_node_labels(quads)
relabelled = {
term.value
for quad in wl_relabel_quads(quads)
for term in (quad.subject, quad.object)
if isinstance(term, ox.BlankNode)
}

assert len(set(labels.values())) == len(names), "labels must stay injective"
assert set(labels.values()) == relabelled, "relabelled quads carry the reported labels"
assert len({label.partition("_")[0] for label in labels.values()}) == 1, "tied nodes share one base"
for label in labels.values():
assert _LABEL_FORM.fullmatch(label), label
assert sorted(label.partition("_")[2] for label in labels.values()) == ["", "1", "2"]
Loading