cks-core 1.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cks/__init__.py +124 -0
- cks/adapters/cks_to_jsonld.py +62 -0
- cks/adapters/cks_to_rdf.py +69 -0
- cks/adapters/jsonld_to_cks.py +121 -0
- cks/adapters/rdf_to_cks.py +88 -0
- cks/cli/__init__.py +75 -0
- cks/cli/commands/__init__.py +0 -0
- cks/cli/commands/convert.py +54 -0
- cks/cli/commands/evolve.py +97 -0
- cks/cli/commands/export.py +60 -0
- cks/cli/commands/inspect.py +58 -0
- cks/cli/commands/parse.py +29 -0
- cks/cli/commands/plugin.py +22 -0
- cks/cli/commands/schema.py +36 -0
- cks/cli/commands/validate.py +81 -0
- cks/cli/formatters.py +108 -0
- cks/constraints/__init__.py +10 -0
- cks/constraints/base.py +33 -0
- cks/constraints/builtin.py +30 -0
- cks/constraints/registry.py +155 -0
- cks/constraints/semantic.py +141 -0
- cks/constraints/structural.py +146 -0
- cks/core.py +400 -0
- cks/diagnostics.py +414 -0
- cks/engine.py +251 -0
- cks/evolution.py +222 -0
- cks/interface.py +200 -0
- cks/plugin.py +114 -0
- cks/result.py +243 -0
- cks/schema.py +50 -0
- cks/serialization.py +507 -0
- cks/validation.py +47 -0
- cks/validator.py +170 -0
- cks_core-1.1.1.dist-info/METADATA +384 -0
- cks_core-1.1.1.dist-info/RECORD +39 -0
- cks_core-1.1.1.dist-info/WHEEL +5 -0
- cks_core-1.1.1.dist-info/entry_points.txt +2 -0
- cks_core-1.1.1.dist-info/licenses/LICENSE +21 -0
- cks_core-1.1.1.dist-info/top_level.txt +1 -0
cks/__init__.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Canonical Knowledge Structure (CKS).
|
|
3
|
+
|
|
4
|
+
Reference implementation of the Canonical Knowledge Structure (CKS)
|
|
5
|
+
specifications.
|
|
6
|
+
|
|
7
|
+
The package exposes the canonical public API defined by CKS-007 together
|
|
8
|
+
with the immutable canonical data model, structural evolution operators,
|
|
9
|
+
and a plugin system for external constraints.
|
|
10
|
+
|
|
11
|
+
Typical usage:
|
|
12
|
+
|
|
13
|
+
import cks
|
|
14
|
+
|
|
15
|
+
structure = cks.parse(source)
|
|
16
|
+
result = cks.validate(structure)
|
|
17
|
+
|
|
18
|
+
Advanced users may instantiate their own ReferenceEngine or build
|
|
19
|
+
custom validation pipelines using the lower-level modules.
|
|
20
|
+
|
|
21
|
+
Only symbols defined here should normally be imported by user code.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from .core import (
|
|
27
|
+
CanonicalRelation,
|
|
28
|
+
KnowledgeObject,
|
|
29
|
+
KnowledgeStructure,
|
|
30
|
+
ObjectIdentity,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
from .diagnostics import (
|
|
34
|
+
Diagnostic,
|
|
35
|
+
DiagnosticCollection,
|
|
36
|
+
DiagnosticSeverity,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
from .engine import ReferenceEngine
|
|
40
|
+
|
|
41
|
+
from .evolution import (
|
|
42
|
+
AddObject,
|
|
43
|
+
AddRelation,
|
|
44
|
+
RemoveObject,
|
|
45
|
+
RemoveRelation,
|
|
46
|
+
compose,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
from .plugin import load_external_constraints
|
|
50
|
+
|
|
51
|
+
from .result import ValidationResult
|
|
52
|
+
|
|
53
|
+
from .serialization import SerializationError
|
|
54
|
+
|
|
55
|
+
from .interface import (
|
|
56
|
+
compare,
|
|
57
|
+
construct,
|
|
58
|
+
diagnose,
|
|
59
|
+
evolve,
|
|
60
|
+
extract,
|
|
61
|
+
inspect,
|
|
62
|
+
parse,
|
|
63
|
+
project,
|
|
64
|
+
serialize,
|
|
65
|
+
validate,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
# Bootstrap external constraint plugins
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
_EXTERNAL_COUNT = load_external_constraints()
|
|
73
|
+
|
|
74
|
+
__version__ = "1.1.1"
|
|
75
|
+
|
|
76
|
+
VERSION = tuple(
|
|
77
|
+
int(part)
|
|
78
|
+
for part in __version__.split(".")
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
__all__ = [
|
|
82
|
+
# Public API
|
|
83
|
+
"construct",
|
|
84
|
+
"parse",
|
|
85
|
+
"serialize",
|
|
86
|
+
"validate",
|
|
87
|
+
"diagnose",
|
|
88
|
+
"inspect",
|
|
89
|
+
"compare",
|
|
90
|
+
"extract",
|
|
91
|
+
"project",
|
|
92
|
+
"evolve",
|
|
93
|
+
|
|
94
|
+
# Core model
|
|
95
|
+
"ObjectIdentity",
|
|
96
|
+
"KnowledgeObject",
|
|
97
|
+
"CanonicalRelation",
|
|
98
|
+
"KnowledgeStructure",
|
|
99
|
+
|
|
100
|
+
# Evolution operators
|
|
101
|
+
"AddObject",
|
|
102
|
+
"AddRelation",
|
|
103
|
+
"RemoveObject",
|
|
104
|
+
"RemoveRelation",
|
|
105
|
+
"compose",
|
|
106
|
+
|
|
107
|
+
# Diagnostics
|
|
108
|
+
"DiagnosticSeverity",
|
|
109
|
+
"Diagnostic",
|
|
110
|
+
"DiagnosticCollection",
|
|
111
|
+
|
|
112
|
+
# Validation
|
|
113
|
+
"ValidationResult",
|
|
114
|
+
|
|
115
|
+
# Engine
|
|
116
|
+
"ReferenceEngine",
|
|
117
|
+
|
|
118
|
+
# Exceptions
|
|
119
|
+
"SerializationError",
|
|
120
|
+
|
|
121
|
+
# Package metadata
|
|
122
|
+
"__version__",
|
|
123
|
+
"VERSION",
|
|
124
|
+
]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CKS Adapter — CKS to JSON‑LD Converter.
|
|
3
|
+
|
|
4
|
+
Converts a Canonical Knowledge Structure into a JSON‑LD document.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Dict, List
|
|
10
|
+
|
|
11
|
+
from ..core import KnowledgeStructure, CanonicalRelation
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CksToJsonLdConverter:
|
|
15
|
+
"""Transform a KnowledgeStructure into a JSON‑LD document."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, structure: KnowledgeStructure) -> None:
|
|
18
|
+
self._structure = structure
|
|
19
|
+
|
|
20
|
+
def convert(self) -> Dict[str, Any]:
|
|
21
|
+
"""Run the conversion and return a JSON‑LD dictionary."""
|
|
22
|
+
graph: List[Dict[str, Any]] = []
|
|
23
|
+
|
|
24
|
+
# 1. Convert every KnowledgeObject to a JSON‑LD entity
|
|
25
|
+
for obj in self._structure.objects:
|
|
26
|
+
if isinstance(obj, CanonicalRelation):
|
|
27
|
+
continue # handled separately
|
|
28
|
+
entity: Dict[str, Any] = {
|
|
29
|
+
"@id": obj.identity.id,
|
|
30
|
+
"@type": obj.identity.type,
|
|
31
|
+
"name": obj.identity.name,
|
|
32
|
+
}
|
|
33
|
+
# Add remaining structure fields
|
|
34
|
+
for key, value in obj.structure.items():
|
|
35
|
+
if key not in ("participants", "relation_type"):
|
|
36
|
+
entity[key] = value
|
|
37
|
+
graph.append(entity)
|
|
38
|
+
|
|
39
|
+
# 2. Convert every CanonicalRelation to JSON‑LD properties
|
|
40
|
+
for rel in self._structure.relations():
|
|
41
|
+
# Find the subject entity and add the relation as a property
|
|
42
|
+
subj_id = rel.participants[0]
|
|
43
|
+
obj_id = rel.participants[1] if len(rel.participants) > 1 else None
|
|
44
|
+
if obj_id is None:
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
# Locate the subject entity in the graph
|
|
48
|
+
for entity in graph:
|
|
49
|
+
if entity["@id"] == subj_id:
|
|
50
|
+
if rel.relation_type not in entity:
|
|
51
|
+
entity[rel.relation_type] = []
|
|
52
|
+
entity[rel.relation_type].append({"@id": obj_id})
|
|
53
|
+
break
|
|
54
|
+
else:
|
|
55
|
+
# Subject not found — create a minimal entity
|
|
56
|
+
graph.append({
|
|
57
|
+
"@id": subj_id,
|
|
58
|
+
"@type": "Entity",
|
|
59
|
+
rel.relation_type: [{"@id": obj_id}],
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
return {"@graph": graph}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CKS Adapter — CKS to RDF Converter.
|
|
3
|
+
|
|
4
|
+
Converts a Canonical Knowledge Structure into an RDF graph
|
|
5
|
+
(Turtle or RDF/XML).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
from rdflib import RDF, RDFS, Graph, Literal, URIRef
|
|
12
|
+
|
|
13
|
+
from ..core import CanonicalRelation, KnowledgeStructure
|
|
14
|
+
|
|
15
|
+
# Base namespace for CKS entities that don't have an absolute URI.
|
|
16
|
+
CKS_NS = "http://cks.org/"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _to_uri(raw: str) -> URIRef:
|
|
20
|
+
"""Return *raw* as an absolute URI, prepending CKS_NS if necessary."""
|
|
21
|
+
if "://" in raw:
|
|
22
|
+
return URIRef(raw)
|
|
23
|
+
return URIRef(CKS_NS + raw)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CksToRdfConverter:
|
|
27
|
+
"""Transform a KnowledgeStructure into an RDF graph."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, structure: KnowledgeStructure) -> None:
|
|
30
|
+
self._structure = structure
|
|
31
|
+
self._graph = Graph()
|
|
32
|
+
|
|
33
|
+
def convert(self) -> Graph:
|
|
34
|
+
"""Run the conversion and return an rdflib Graph."""
|
|
35
|
+
# 1. Convert every KnowledgeObject to an RDF resource
|
|
36
|
+
for obj in self._structure.objects:
|
|
37
|
+
if isinstance(obj, CanonicalRelation):
|
|
38
|
+
continue
|
|
39
|
+
subject = _to_uri(obj.identity.id)
|
|
40
|
+
# Type
|
|
41
|
+
self._graph.add((subject, RDF.type, _to_uri(obj.identity.type)))
|
|
42
|
+
# Name / label
|
|
43
|
+
self._graph.add((subject, RDFS.label, Literal(obj.identity.name)))
|
|
44
|
+
# Additional structure fields
|
|
45
|
+
for key, value in obj.structure.items():
|
|
46
|
+
if key in ("participants", "relation_type"):
|
|
47
|
+
continue
|
|
48
|
+
if isinstance(value, (str, int, float)):
|
|
49
|
+
self._graph.add((subject, _to_uri(key), Literal(value)))
|
|
50
|
+
|
|
51
|
+
# 2. Convert every CanonicalRelation to RDF triples
|
|
52
|
+
for rel in self._structure.relations():
|
|
53
|
+
subj_id = rel.participants[0]
|
|
54
|
+
obj_id = rel.participants[1] if len(rel.participants) > 1 else None
|
|
55
|
+
if obj_id is None:
|
|
56
|
+
continue
|
|
57
|
+
self._graph.add(
|
|
58
|
+
(_to_uri(subj_id), _to_uri(rel.relation_type), _to_uri(obj_id))
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return self._graph
|
|
62
|
+
|
|
63
|
+
def to_turtle(self) -> str:
|
|
64
|
+
"""Return the RDF graph serialised as Turtle."""
|
|
65
|
+
return self._graph.serialize(format="turtle")
|
|
66
|
+
|
|
67
|
+
def to_rdfxml(self) -> str:
|
|
68
|
+
"""Return the RDF graph serialised as RDF/XML."""
|
|
69
|
+
return self._graph.serialize(format="xml")
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CKS Adapter — JSON‑LD to CKS Converter.
|
|
3
|
+
|
|
4
|
+
Converts a JSON‑LD document into a Canonical Knowledge Structure.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
from ..core import (
|
|
12
|
+
CanonicalRelation,
|
|
13
|
+
KnowledgeObject,
|
|
14
|
+
KnowledgeStructure,
|
|
15
|
+
ObjectIdentity,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class JsonLdToCksConverter:
|
|
20
|
+
"""Transform a JSON‑LD document into a KnowledgeStructure."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, jsonld_data: Dict[str, Any]) -> None:
|
|
23
|
+
self._data = jsonld_data
|
|
24
|
+
|
|
25
|
+
# ------------------------------------------------------------------
|
|
26
|
+
# Public API
|
|
27
|
+
# ------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
def convert(self) -> KnowledgeStructure:
|
|
30
|
+
"""Run the conversion and return a KnowledgeStructure."""
|
|
31
|
+
objects: List[KnowledgeObject] = []
|
|
32
|
+
|
|
33
|
+
# 1. Merge entities with the same @id
|
|
34
|
+
merged_entities = self._merge_entities()
|
|
35
|
+
|
|
36
|
+
# 2. Convert each merged entity to a KnowledgeObject
|
|
37
|
+
for entity in merged_entities.values():
|
|
38
|
+
objects.append(self._entity_to_ko(entity))
|
|
39
|
+
|
|
40
|
+
# 3. Convert relations
|
|
41
|
+
for relation in self._iter_relations():
|
|
42
|
+
objects.append(relation)
|
|
43
|
+
|
|
44
|
+
return KnowledgeStructure(objects)
|
|
45
|
+
|
|
46
|
+
# ------------------------------------------------------------------
|
|
47
|
+
# Internal helpers
|
|
48
|
+
# ------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
def _merge_entities(self) -> Dict[str, Dict[str, Any]]:
|
|
51
|
+
"""Merge all nodes that share the same @id."""
|
|
52
|
+
merged: Dict[str, Dict[str, Any]] = {}
|
|
53
|
+
for entity in self._iter_entities():
|
|
54
|
+
oid = entity.get("@id", "unknown")
|
|
55
|
+
if oid not in merged:
|
|
56
|
+
merged[oid] = dict(entity)
|
|
57
|
+
else:
|
|
58
|
+
# Merge: later properties override earlier ones
|
|
59
|
+
merged[oid].update(entity)
|
|
60
|
+
return merged
|
|
61
|
+
|
|
62
|
+
def _iter_entities(self) -> List[Dict[str, Any]]:
|
|
63
|
+
"""Yield every entity described in the JSON‑LD document."""
|
|
64
|
+
graph = self._data.get("@graph")
|
|
65
|
+
if isinstance(graph, list):
|
|
66
|
+
return graph
|
|
67
|
+
if isinstance(self._data, dict):
|
|
68
|
+
return [self._data]
|
|
69
|
+
return []
|
|
70
|
+
|
|
71
|
+
def _entity_to_ko(self, entity: Dict[str, Any]) -> KnowledgeObject:
|
|
72
|
+
oid = entity.get("@id", "unknown")
|
|
73
|
+
otype = self._pick_type(entity)
|
|
74
|
+
name = entity.get("name", entity.get("rdfs:label", oid))
|
|
75
|
+
|
|
76
|
+
identity = ObjectIdentity(id=oid, type=otype, name=str(name))
|
|
77
|
+
structure = dict(entity)
|
|
78
|
+
return KnowledgeObject(identity=identity, structure=structure)
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def _pick_type(entity: Dict[str, Any]) -> str:
|
|
82
|
+
types = entity.get("@type", [])
|
|
83
|
+
if isinstance(types, list) and types:
|
|
84
|
+
return str(types[0])
|
|
85
|
+
if isinstance(types, str):
|
|
86
|
+
return types
|
|
87
|
+
return "Entity"
|
|
88
|
+
|
|
89
|
+
def _iter_relations(self) -> List[CanonicalRelation]:
|
|
90
|
+
relations: List[CanonicalRelation] = []
|
|
91
|
+
for entity in self._iter_entities():
|
|
92
|
+
subject_id = entity.get("@id")
|
|
93
|
+
if not subject_id:
|
|
94
|
+
continue
|
|
95
|
+
for predicate, objects in entity.items():
|
|
96
|
+
if predicate in ("@id", "@type", "@context", "@graph", "name", "rdfs:label"):
|
|
97
|
+
continue
|
|
98
|
+
if not isinstance(objects, list):
|
|
99
|
+
objects = [objects]
|
|
100
|
+
for obj in objects:
|
|
101
|
+
obj_id = self._object_to_id(obj)
|
|
102
|
+
if obj_id is None:
|
|
103
|
+
continue
|
|
104
|
+
rel_id = f"{subject_id}-{predicate}-{obj_id}"
|
|
105
|
+
relation = CanonicalRelation(
|
|
106
|
+
identity=ObjectIdentity(
|
|
107
|
+
id=rel_id, type="Relation", name=predicate
|
|
108
|
+
),
|
|
109
|
+
participants=[subject_id, obj_id],
|
|
110
|
+
relation_type=predicate,
|
|
111
|
+
)
|
|
112
|
+
relations.append(relation)
|
|
113
|
+
return relations
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
def _object_to_id(obj: Any) -> Optional[str]:
|
|
117
|
+
if isinstance(obj, str):
|
|
118
|
+
return obj
|
|
119
|
+
if isinstance(obj, dict):
|
|
120
|
+
return obj.get("@id")
|
|
121
|
+
return None
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CKS Adapter — RDF to CKS Converter.
|
|
3
|
+
|
|
4
|
+
Converts an RDF graph (RDF/XML, Turtle, etc.) into a Canonical Knowledge Structure.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
import rdflib
|
|
11
|
+
|
|
12
|
+
from ..core import (
|
|
13
|
+
CanonicalRelation,
|
|
14
|
+
KnowledgeObject,
|
|
15
|
+
KnowledgeStructure,
|
|
16
|
+
ObjectIdentity,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RdfToCksConverter:
|
|
21
|
+
"""Transform an RDF graph into a KnowledgeStructure."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, rdf_data: str, format: str = "turtle") -> None:
|
|
24
|
+
self._graph = rdflib.Graph()
|
|
25
|
+
self._graph.parse(data=rdf_data, format=format)
|
|
26
|
+
|
|
27
|
+
def convert(self) -> KnowledgeStructure:
|
|
28
|
+
"""Run the conversion and return a KnowledgeStructure."""
|
|
29
|
+
objects: list[KnowledgeObject] = []
|
|
30
|
+
seen_ids: set[str] = set()
|
|
31
|
+
relations: list[CanonicalRelation] = []
|
|
32
|
+
|
|
33
|
+
# 1. Convert every subject to a KnowledgeObject
|
|
34
|
+
for subject in self._graph.subjects():
|
|
35
|
+
oid = str(subject)
|
|
36
|
+
if oid not in seen_ids:
|
|
37
|
+
seen_ids.add(oid)
|
|
38
|
+
objects.append(self._subject_to_ko(subject))
|
|
39
|
+
|
|
40
|
+
# 2. Convert triples
|
|
41
|
+
for s, p, o in self._graph:
|
|
42
|
+
if isinstance(o, rdflib.Literal):
|
|
43
|
+
# Skip literal objects – they are not KnowledgeObjects
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
subj_id = str(s)
|
|
47
|
+
pred = str(p)
|
|
48
|
+
obj_id = str(o)
|
|
49
|
+
|
|
50
|
+
# Ensure participants exist
|
|
51
|
+
for pid in (subj_id, obj_id):
|
|
52
|
+
if pid not in seen_ids:
|
|
53
|
+
seen_ids.add(pid)
|
|
54
|
+
objects.append(
|
|
55
|
+
KnowledgeObject(
|
|
56
|
+
identity=ObjectIdentity(id=pid, type="Entity", name=pid),
|
|
57
|
+
structure={},
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
rel_id = f"{subj_id}-{pred}-{obj_id}"
|
|
62
|
+
relation = CanonicalRelation(
|
|
63
|
+
identity=ObjectIdentity(id=rel_id, type="Relation", name=pred),
|
|
64
|
+
participants=[subj_id, obj_id],
|
|
65
|
+
relation_type=pred,
|
|
66
|
+
)
|
|
67
|
+
relations.append(relation)
|
|
68
|
+
|
|
69
|
+
all_objects = objects + list(relations)
|
|
70
|
+
return KnowledgeStructure(all_objects)
|
|
71
|
+
|
|
72
|
+
def _subject_to_ko(self, subject: rdflib.term.Node) -> KnowledgeObject:
|
|
73
|
+
oid = str(subject)
|
|
74
|
+
# Try to get a human-readable label
|
|
75
|
+
label = None
|
|
76
|
+
for _, _, lbl in self._graph.triples((subject, rdflib.RDFS.label, None)):
|
|
77
|
+
label = str(lbl)
|
|
78
|
+
break
|
|
79
|
+
name = label or oid
|
|
80
|
+
|
|
81
|
+
# Try to get a type
|
|
82
|
+
otype = "Entity"
|
|
83
|
+
for _, _, t in self._graph.triples((subject, rdflib.RDF.type, None)):
|
|
84
|
+
otype = str(t).split("#")[-1] if "#" in str(t) else str(t)
|
|
85
|
+
break
|
|
86
|
+
|
|
87
|
+
identity = ObjectIdentity(id=oid, type=otype, name=name)
|
|
88
|
+
return KnowledgeObject(identity=identity, structure={})
|
cks/cli/__init__.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CKS Command-Line Interface.
|
|
3
|
+
|
|
4
|
+
Canonical entry point for interacting with CKS from the terminal.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import sys
|
|
10
|
+
from typing import Optional, Sequence
|
|
11
|
+
|
|
12
|
+
from .commands import (
|
|
13
|
+
validate,
|
|
14
|
+
parse,
|
|
15
|
+
inspect,
|
|
16
|
+
evolve,
|
|
17
|
+
convert,
|
|
18
|
+
export,
|
|
19
|
+
schema,
|
|
20
|
+
plugin,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _create_parser() -> argparse.ArgumentParser:
|
|
25
|
+
parser = argparse.ArgumentParser(
|
|
26
|
+
prog="cks",
|
|
27
|
+
description="Canonical Knowledge Structure — CLI",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
parser.add_argument("--strict", action="store_true", help="Fail on any plugin loading error")
|
|
31
|
+
|
|
32
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
33
|
+
|
|
34
|
+
validate.add_parser(sub)
|
|
35
|
+
parse.add_parser(sub)
|
|
36
|
+
inspect.add_parser(sub)
|
|
37
|
+
evolve.add_parser(sub)
|
|
38
|
+
convert.add_parser(sub)
|
|
39
|
+
export.add_parser(sub)
|
|
40
|
+
schema.add_parser(sub)
|
|
41
|
+
plugin.add_parser(sub)
|
|
42
|
+
|
|
43
|
+
return parser
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def main(argv: Optional[Sequence[str]] = None) -> None:
|
|
47
|
+
parser = _create_parser()
|
|
48
|
+
args = parser.parse_args(argv)
|
|
49
|
+
|
|
50
|
+
from cks.plugin import load_external_constraints
|
|
51
|
+
load_external_constraints(strict=args.strict)
|
|
52
|
+
|
|
53
|
+
if args.command == "validate":
|
|
54
|
+
validate.handle(args)
|
|
55
|
+
elif args.command == "parse":
|
|
56
|
+
parse.handle(args)
|
|
57
|
+
elif args.command == "inspect":
|
|
58
|
+
inspect.handle(args)
|
|
59
|
+
elif args.command == "evolve":
|
|
60
|
+
evolve.handle(args)
|
|
61
|
+
elif args.command == "convert":
|
|
62
|
+
convert.handle(args)
|
|
63
|
+
elif args.command == "export":
|
|
64
|
+
export.handle(args)
|
|
65
|
+
elif args.command == "schema":
|
|
66
|
+
schema.handle(args)
|
|
67
|
+
elif args.command == "plugin":
|
|
68
|
+
plugin.handle(args)
|
|
69
|
+
else:
|
|
70
|
+
parser.print_help()
|
|
71
|
+
sys.exit(1)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
if __name__ == "__main__":
|
|
75
|
+
main()
|
|
File without changes
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI command: convert.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from ...serialization import serialize as cks_serialize
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def add_parser(subparsers):
|
|
14
|
+
parser = subparsers.add_parser("convert", help="Convert JSON‑LD, Turtle, or RDF/XML to CKS")
|
|
15
|
+
parser.add_argument("input", type=Path, help="Path to input file")
|
|
16
|
+
parser.add_argument("--output", "-o", type=Path, default=None, help="Write CKS JSON to file")
|
|
17
|
+
parser.add_argument(
|
|
18
|
+
"--format", "-f",
|
|
19
|
+
choices=("json-ld", "rdf-xml", "turtle"),
|
|
20
|
+
default="json-ld",
|
|
21
|
+
help="Input format (default: json-ld)",
|
|
22
|
+
)
|
|
23
|
+
return parser
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def handle(args):
|
|
27
|
+
try:
|
|
28
|
+
raw = args.input.read_text(encoding="utf-8")
|
|
29
|
+
data = json.loads(raw) if args.format == "json-ld" else raw
|
|
30
|
+
except FileNotFoundError:
|
|
31
|
+
print(f"File not found: {args.input}", file=sys.stderr)
|
|
32
|
+
sys.exit(1)
|
|
33
|
+
except json.JSONDecodeError as exc:
|
|
34
|
+
print(f"Invalid JSON in {args.input}: {exc}", file=sys.stderr)
|
|
35
|
+
sys.exit(1)
|
|
36
|
+
|
|
37
|
+
if args.format == "json-ld":
|
|
38
|
+
from ...adapters.jsonld_to_cks import JsonLdToCksConverter
|
|
39
|
+
converter = JsonLdToCksConverter(data)
|
|
40
|
+
structure = converter.convert()
|
|
41
|
+
elif args.format in ("rdf-xml", "turtle"):
|
|
42
|
+
from ...adapters.rdf_to_cks import RdfToCksConverter
|
|
43
|
+
rdf_format = "xml" if args.format == "rdf-xml" else args.format
|
|
44
|
+
converter = RdfToCksConverter(data, format=rdf_format)
|
|
45
|
+
structure = converter.convert()
|
|
46
|
+
else:
|
|
47
|
+
print(f"Unknown format: {args.format}", file=sys.stderr)
|
|
48
|
+
sys.exit(1)
|
|
49
|
+
|
|
50
|
+
output = cks_serialize(structure)
|
|
51
|
+
if args.output is None:
|
|
52
|
+
print(output)
|
|
53
|
+
else:
|
|
54
|
+
args.output.write_text(output, encoding="utf-8")
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI command: evolve.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from ...serialization import parse as cks_parse, serialize as cks_serialize, SerializationError
|
|
11
|
+
from ...evolution import compose, AddObject, AddRelation, RemoveObject, RemoveRelation
|
|
12
|
+
from ...core import KnowledgeObject, CanonicalRelation, ObjectIdentity
|
|
13
|
+
from ...evolution import StructuralOperator
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def add_parser(subparsers):
|
|
17
|
+
parser = subparsers.add_parser("evolve", help="Apply structural evolution")
|
|
18
|
+
parser.add_argument("input", type=Path, help="Path to canonical JSON file")
|
|
19
|
+
parser.add_argument("operations", type=Path, help="JSON file describing operations")
|
|
20
|
+
parser.add_argument("--output", "-o", type=Path, default=None, help="Write result to file")
|
|
21
|
+
return parser
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def handle(args):
|
|
25
|
+
try:
|
|
26
|
+
raw = args.input.read_text(encoding="utf-8")
|
|
27
|
+
structure = cks_parse(raw)
|
|
28
|
+
except FileNotFoundError:
|
|
29
|
+
print(f"File not found: {args.input}", file=sys.stderr)
|
|
30
|
+
sys.exit(1)
|
|
31
|
+
except SerializationError as exc:
|
|
32
|
+
print(f"Serialization error: {exc}", file=sys.stderr)
|
|
33
|
+
sys.exit(1)
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
ops_data = json.loads(args.operations.read_text(encoding="utf-8"))
|
|
37
|
+
operators = _parse_operations(ops_data)
|
|
38
|
+
except json.JSONDecodeError as exc:
|
|
39
|
+
print(f"Invalid JSON in operations file: {exc}", file=sys.stderr)
|
|
40
|
+
sys.exit(1)
|
|
41
|
+
except ValueError as exc:
|
|
42
|
+
print(f"Invalid operations: {exc}", file=sys.stderr)
|
|
43
|
+
sys.exit(1)
|
|
44
|
+
|
|
45
|
+
new_structure = compose(structure, operators)
|
|
46
|
+
result = cks_serialize(new_structure)
|
|
47
|
+
|
|
48
|
+
if args.output is None:
|
|
49
|
+
print(result)
|
|
50
|
+
else:
|
|
51
|
+
args.output.write_text(result, encoding="utf-8")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _parse_operations(ops_data: list[dict]) -> list:
|
|
55
|
+
operators: list[StructuralOperator] = []
|
|
56
|
+
for i, op in enumerate(ops_data):
|
|
57
|
+
op_type = op.get("type")
|
|
58
|
+
if op_type is None:
|
|
59
|
+
raise ValueError(f"Operation #{i}: missing 'type' field")
|
|
60
|
+
if op_type == "add_object":
|
|
61
|
+
identity_data = op.get("identity")
|
|
62
|
+
if identity_data is None:
|
|
63
|
+
raise ValueError(f"Operation #{i}: missing 'identity' field")
|
|
64
|
+
identity = ObjectIdentity(**identity_data)
|
|
65
|
+
obj = KnowledgeObject(identity=identity, structure=op.get("structure", {}))
|
|
66
|
+
operators.append(AddObject(obj))
|
|
67
|
+
elif op_type == "add_relation":
|
|
68
|
+
identity_data = op.get("identity")
|
|
69
|
+
if identity_data is None:
|
|
70
|
+
raise ValueError(f"Operation #{i}: missing 'identity' field")
|
|
71
|
+
identity = ObjectIdentity(**identity_data)
|
|
72
|
+
participants = op.get("participants")
|
|
73
|
+
if participants is None:
|
|
74
|
+
raise ValueError(f"Operation #{i}: missing 'participants' field")
|
|
75
|
+
relation_type = op.get("relation_type")
|
|
76
|
+
if relation_type is None:
|
|
77
|
+
raise ValueError(f"Operation #{i}: missing 'relation_type' field")
|
|
78
|
+
relation = CanonicalRelation(
|
|
79
|
+
identity=identity,
|
|
80
|
+
participants=participants,
|
|
81
|
+
relation_type=relation_type,
|
|
82
|
+
structure=op.get("structure", {}),
|
|
83
|
+
)
|
|
84
|
+
operators.append(AddRelation(relation))
|
|
85
|
+
elif op_type == "remove_object":
|
|
86
|
+
object_id = op.get("object_id")
|
|
87
|
+
if object_id is None:
|
|
88
|
+
raise ValueError(f"Operation #{i}: missing 'object_id' field")
|
|
89
|
+
operators.append(RemoveObject(object_id))
|
|
90
|
+
elif op_type == "remove_relation":
|
|
91
|
+
relation_id = op.get("relation_id")
|
|
92
|
+
if relation_id is None:
|
|
93
|
+
raise ValueError(f"Operation #{i}: missing 'relation_id' field")
|
|
94
|
+
operators.append(RemoveRelation(relation_id))
|
|
95
|
+
else:
|
|
96
|
+
raise ValueError(f"Operation #{i}: unknown operation type '{op_type}'")
|
|
97
|
+
return operators
|