mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-08-29 04:26:20 +00:00
fix(export): close the four review gaps in the default-graph change
All four are in the branch that recognises an already-converted document, which has to survive every shape JSON-LD allows rather than the one shape Semantica happens to produce. A knowledge graph carrying a context of its own took the already-JSON-LD branch and skipped its own conversion, leaving entity ids, relationship endpoints, types and confidences as raw keys. The entities/relationships test now runs first, and a converted document never has those keys, so the double-conversion guard is unaffected. A context that is a URL or an array cannot be merged key by key, and was being dropped in favour of Semantica's defaults, silently changing how every term expands. Both are kept as an array now, the caller's winning, which is the same precedence the dictionary branch already used. An explicit null is left alone on purpose: in an array it resets the active context and would take the semantica prefix with it. @graph may be a single node object as well as an array. list() on a dictionary yields its keys, so an object-valued graph was replaced by a list of strings. A caller may hand us a document that is deliberately a named graph. That name is theirs to keep, so it is no longer flattened; it is nested one level and the export's own provenance goes beside it, in the default graph, where a plain reader can see it. Four tests, one per case, all failing before this commit.
This commit is contained in:
@@ -416,7 +416,13 @@ class JSONExporter:
|
||||
|
||||
# Convert data based on type
|
||||
if isinstance(data, dict):
|
||||
if _is_jsonld_document(data):
|
||||
# A knowledge graph is converted even when it carries a context of
|
||||
# its own: the specialized conversion is what mints entity ids and
|
||||
# relationship endpoints, and skipping it leaves them raw keys.
|
||||
if "entities" in data or "relationships" in data:
|
||||
# Knowledge graph structure - use specialized conversion
|
||||
jsonld.update(self._convert_kg_to_jsonld(data, **options))
|
||||
elif _is_jsonld_document(data):
|
||||
# Already JSON-LD: merge it rather than nesting it. Wrapping a
|
||||
# converted document in @graph re-typed the payload as a named
|
||||
# graph and doubled the @context, which is what happened when
|
||||
@@ -424,10 +430,15 @@ class JSONExporter:
|
||||
context = data.get("@context")
|
||||
if isinstance(context, dict):
|
||||
jsonld["@context"].update(context)
|
||||
elif context is not None:
|
||||
# A context may also be a URL or an array of them, which
|
||||
# cannot be merged key by key. Keeping both as an array
|
||||
# preserves the caller's term expansion, which wins over
|
||||
# ours, while still defining the semantica prefix. An
|
||||
# explicit null is left alone: in an array it would reset
|
||||
# the active context and take our own terms with it.
|
||||
jsonld["@context"] = [jsonld["@context"], context]
|
||||
jsonld.update({k: v for k, v in data.items() if k != "@context"})
|
||||
elif "entities" in data or "relationships" in data:
|
||||
# Knowledge graph structure - use specialized conversion
|
||||
jsonld.update(self._convert_kg_to_jsonld(data, **options))
|
||||
else:
|
||||
# Generic dictionary - wrap in @graph
|
||||
jsonld["@graph"] = [data]
|
||||
@@ -463,20 +474,31 @@ class JSONExporter:
|
||||
jsonld: Document being built, modified in place
|
||||
include_provenance: Whether to record how and when it was exported
|
||||
"""
|
||||
# A caller may hand us a document that is deliberately a named graph.
|
||||
# That name is theirs to keep, but our own statements must not end up
|
||||
# inside it, where a default-graph reader would never see them.
|
||||
payload_is_named_graph = "@id" in jsonld and "@graph" in jsonld
|
||||
|
||||
document: Dict[str, Any] = {}
|
||||
# Do not overwrite an identifier the payload already carries: the
|
||||
# knowledge-graph conversion names its own document node.
|
||||
if "@id" not in jsonld:
|
||||
if "@id" not in jsonld or payload_is_named_graph:
|
||||
document["@id"] = f"https://semantica.dev/data/{utc_now_iso()}"
|
||||
if include_provenance:
|
||||
document["semantica:exportedAt"] = utc_now_iso()
|
||||
document["semantica:format"] = "json-ld"
|
||||
|
||||
if "@graph" in jsonld:
|
||||
document.setdefault(
|
||||
"@id", f"https://semantica.dev/data/{utc_now_iso()}"
|
||||
)
|
||||
jsonld["@graph"] = list(jsonld["@graph"]) + [document]
|
||||
if payload_is_named_graph:
|
||||
named = {key: value for key, value in jsonld.items() if key != "@context"}
|
||||
for key in [key for key in jsonld if key != "@context"]:
|
||||
del jsonld[key]
|
||||
jsonld["@graph"] = [named, document]
|
||||
elif "@graph" in jsonld:
|
||||
# @graph may be a single node object as well as an array. list() on
|
||||
# a dictionary yields its keys, which would discard the node.
|
||||
members = jsonld["@graph"]
|
||||
members = list(members) if isinstance(members, list) else [members]
|
||||
jsonld["@graph"] = members + [document]
|
||||
else:
|
||||
jsonld.update(document)
|
||||
|
||||
|
||||
@@ -141,3 +141,78 @@ def test_document_provenance_still_reaches_the_default_graph(tmp_path):
|
||||
str(s) for s in graph.subjects()
|
||||
}
|
||||
assert {"A", "B"} <= {str(o) for o in graph.objects()}
|
||||
|
||||
|
||||
# The document a caller hands to export() need not be one Semantica built, and
|
||||
# the branch that recognises an already-converted document has to survive every
|
||||
# shape JSON-LD allows. Each of the four cases below regressed when that branch
|
||||
# was first written.
|
||||
|
||||
|
||||
def test_a_url_valued_context_is_not_thrown_away(tmp_path):
|
||||
"""@context may be a URL or an array, not only an object."""
|
||||
payload = {
|
||||
"@context": "https://schema.org/",
|
||||
"@id": "https://example.org/thing",
|
||||
"name": "Acme Corp",
|
||||
}
|
||||
path = _write(payload, tmp_path)
|
||||
context = json.loads(path.read_text())["@context"]
|
||||
|
||||
flattened = context if isinstance(context, list) else [context]
|
||||
assert "https://schema.org/" in flattened, (
|
||||
"the caller's context was replaced by Semantica's defaults, "
|
||||
"which silently changes how every term expands"
|
||||
)
|
||||
|
||||
|
||||
def test_a_graph_given_as_one_node_object_survives(tmp_path):
|
||||
"""@graph may be a single node object; list() on it yields its keys."""
|
||||
payload = {
|
||||
"@context": {"rdfs": "http://www.w3.org/2000/01/rdf-schema#"},
|
||||
"@graph": {"@id": "https://example.org/only", "rdfs:label": "Only"},
|
||||
}
|
||||
path = _write(payload, tmp_path)
|
||||
|
||||
graph = Graph()
|
||||
graph.parse(str(path), format="json-ld")
|
||||
assert "Only" in {str(o) for o in graph.objects()}
|
||||
|
||||
|
||||
def test_a_caller_supplied_named_graph_keeps_our_provenance_readable(tmp_path):
|
||||
"""A deliberate named graph stays named, but must not swallow the export's own metadata."""
|
||||
payload = {
|
||||
"@context": {"rdfs": "http://www.w3.org/2000/01/rdf-schema#"},
|
||||
"@id": "https://example.org/named",
|
||||
"@graph": [{"@id": "https://example.org/n1", "rdfs:label": "N1"}],
|
||||
}
|
||||
path = _write(payload, tmp_path)
|
||||
document = json.loads(path.read_text())
|
||||
|
||||
assert "@id" not in document or "@graph" not in document
|
||||
|
||||
graph = Graph()
|
||||
graph.parse(str(path), format="json-ld")
|
||||
predicates = {str(p) for p in graph.predicates()}
|
||||
assert "https://semantica.dev/ns#exportedAt" in predicates, (
|
||||
"the export's provenance was written inside the caller's named graph, "
|
||||
"where a default-graph reader cannot see it"
|
||||
)
|
||||
|
||||
dataset = Dataset()
|
||||
dataset.parse(str(path), format="json-ld")
|
||||
names = {str(c.identifier) for c in dataset.graphs()}
|
||||
assert "https://example.org/named" in names, "the caller's graph lost its name"
|
||||
|
||||
|
||||
def test_a_knowledge_graph_carrying_a_context_is_still_converted(tmp_path):
|
||||
"""entities/relationships must win over the already-JSON-LD branch."""
|
||||
payload = dict(KG, **{"@context": {"ex": "https://example.org/ns#"}})
|
||||
path = _write(payload, tmp_path)
|
||||
document = json.loads(path.read_text())
|
||||
|
||||
assert "semantica:entities" in document, (
|
||||
"the knowledge graph skipped its own conversion, so entity ids, "
|
||||
"endpoints, types and confidences were left as raw keys"
|
||||
)
|
||||
assert "entities" not in document
|
||||
|
||||
Reference in New Issue
Block a user