ontolith 1.0.0__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.
Files changed (67) hide show
  1. ontolith/__init__.py +47 -0
  2. ontolith/core/__init__.py +80 -0
  3. ontolith/core/assertion.py +132 -0
  4. ontolith/core/clock.py +116 -0
  5. ontolith/core/embedder.py +141 -0
  6. ontolith/core/entity.py +34 -0
  7. ontolith/core/errors.py +159 -0
  8. ontolith/core/ids.py +146 -0
  9. ontolith/core/namespace.py +30 -0
  10. ontolith/core/observability.py +216 -0
  11. ontolith/govern/__init__.py +66 -0
  12. ontolith/govern/conflict.py +234 -0
  13. ontolith/govern/contradiction.py +112 -0
  14. ontolith/govern/policy.py +886 -0
  15. ontolith/govern/proposal.py +114 -0
  16. ontolith/govern/provenance.py +44 -0
  17. ontolith/identity/__init__.py +45 -0
  18. ontolith/identity/admin_event.py +57 -0
  19. ontolith/identity/credential.py +44 -0
  20. ontolith/identity/ports.py +33 -0
  21. ontolith/identity/principal.py +67 -0
  22. ontolith/identity/token_auth.py +52 -0
  23. ontolith/interfaces/__init__.py +0 -0
  24. ontolith/interfaces/cli.py +1135 -0
  25. ontolith/interfaces/graphql.py +1203 -0
  26. ontolith/interfaces/mcp.py +1046 -0
  27. ontolith/interfaces/rest.py +1407 -0
  28. ontolith/observe/__init__.py +0 -0
  29. ontolith/ontology.py +3614 -0
  30. ontolith/plugins/__init__.py +40 -0
  31. ontolith/plugins/manifest.py +79 -0
  32. ontolith/plugins/ports.py +148 -0
  33. ontolith/plugins/reference/__init__.py +33 -0
  34. ontolith/plugins/reference/csv_importer.py +159 -0
  35. ontolith/plugins/reference/json_exporter.py +56 -0
  36. ontolith/plugins/reference/rdf_exporter.py +172 -0
  37. ontolith/plugins/reference/required_fields_validator.py +103 -0
  38. ontolith/plugins/registry.py +476 -0
  39. ontolith/plugins/sandbox/__init__.py +18 -0
  40. ontolith/plugins/sandbox/enforcement.py +417 -0
  41. ontolith/plugins/sandbox/protocol.py +151 -0
  42. ontolith/plugins/sandbox/remote_view.py +175 -0
  43. ontolith/plugins/sandbox/runner.py +769 -0
  44. ontolith/plugins/sandbox/wire.py +369 -0
  45. ontolith/plugins/views.py +150 -0
  46. ontolith/py.typed +0 -0
  47. ontolith/query/__init__.py +9 -0
  48. ontolith/query/builder.py +578 -0
  49. ontolith/schema/__init__.py +58 -0
  50. ontolith/schema/dsl.py +425 -0
  51. ontolith/schema/ir.py +228 -0
  52. ontolith/schema/linkml.py +367 -0
  53. ontolith/schema/rdf.py +245 -0
  54. ontolith/store/__init__.py +5 -0
  55. ontolith/store/base.py +926 -0
  56. ontolith/store/duckdb/__init__.py +11 -0
  57. ontolith/store/duckdb/backend.py +2279 -0
  58. ontolith/store/duckdb/migrations.py +430 -0
  59. ontolith/store/migrations.py +72 -0
  60. ontolith/store/sqlite/__init__.py +9 -0
  61. ontolith/store/sqlite/backend.py +2549 -0
  62. ontolith/store/sqlite/migrations.py +462 -0
  63. ontolith-1.0.0.dist-info/METADATA +137 -0
  64. ontolith-1.0.0.dist-info/RECORD +67 -0
  65. ontolith-1.0.0.dist-info/WHEEL +4 -0
  66. ontolith-1.0.0.dist-info/entry_points.txt +8 -0
  67. ontolith-1.0.0.dist-info/licenses/LICENSE +201 -0
ontolith/__init__.py ADDED
@@ -0,0 +1,47 @@
1
+ """Ontolith: A Python framework for collaborative knowledge bases.
2
+
3
+ Where humans and AI agents are co-equal authors of a shared, governed ontology.
4
+ """
5
+
6
+ from ontolith.core import Assertion, Entity, FixedClock, Namespace, SequentialIdProvider
7
+ from ontolith.identity import Principal
8
+ from ontolith.ontology import AsOfView, Ontology
9
+ from ontolith.schema import (
10
+ JSON,
11
+ URI,
12
+ Boolean,
13
+ Concept,
14
+ Date,
15
+ DateTime,
16
+ Float,
17
+ Integer,
18
+ Property,
19
+ Ref,
20
+ Relation,
21
+ Text,
22
+ )
23
+
24
+ __version__ = "1.0.0"
25
+
26
+ __all__ = [
27
+ "Ontology",
28
+ "AsOfView",
29
+ "Entity",
30
+ "Assertion",
31
+ "Namespace",
32
+ "Principal",
33
+ "FixedClock",
34
+ "SequentialIdProvider",
35
+ "Concept",
36
+ "Relation",
37
+ "Property",
38
+ "Ref",
39
+ "Text",
40
+ "Integer",
41
+ "Float",
42
+ "Boolean",
43
+ "Date",
44
+ "DateTime",
45
+ "URI",
46
+ "JSON",
47
+ ]
@@ -0,0 +1,80 @@
1
+ """Core abstractions and ports for Ontolith.
2
+
3
+ This module contains the fundamental building blocks:
4
+ - Clock and IdProvider ports for deterministic behavior
5
+ - Embedder port for hybrid retrieval (SPEC §11.3)
6
+ - ObservabilitySink port for metrics/events/structured logs (SPEC §18, ADR-0044)
7
+ - Error taxonomy for stable error handling
8
+ - Entity and Assertion value objects
9
+
10
+ The meta-model/IR/validation this docstring once listed as "Future" now
11
+ lives in `ontolith.schema` (`SchemaIR`, `ConceptDef`/`PropertyDef`/
12
+ `RelationDef`, the class DSL) — not `ontolith.core`, and shipped in M1/M3.
13
+ """
14
+
15
+ from ontolith.core.assertion import Assertion, AssertionEvent
16
+ from ontolith.core.clock import Clock, FixedClock, SystemClock
17
+ from ontolith.core.embedder import Embedder, HashingEmbedder, LookupEmbedder
18
+ from ontolith.core.entity import Entity
19
+ from ontolith.core.errors import (
20
+ AuthError,
21
+ CapabilityError,
22
+ ConflictError,
23
+ NotFoundError,
24
+ OntolithError,
25
+ PluginError,
26
+ PolicyDenied,
27
+ SchemaError,
28
+ StorageError,
29
+ ValidationError,
30
+ )
31
+ from ontolith.core.ids import (
32
+ FixedIdProvider,
33
+ IdProvider,
34
+ SequentialIdProvider,
35
+ UlidProvider,
36
+ )
37
+ from ontolith.core.namespace import Namespace
38
+ from ontolith.core.observability import (
39
+ NullObservabilitySink,
40
+ ObservabilitySink,
41
+ RecordingObservabilitySink,
42
+ StdlibLoggingSink,
43
+ )
44
+
45
+ __all__ = [
46
+ # Clock
47
+ "Clock",
48
+ "SystemClock",
49
+ "FixedClock",
50
+ # IDs
51
+ "IdProvider",
52
+ "UlidProvider",
53
+ "SequentialIdProvider",
54
+ "FixedIdProvider",
55
+ # Embedder
56
+ "Embedder",
57
+ "HashingEmbedder",
58
+ "LookupEmbedder",
59
+ # Observability
60
+ "ObservabilitySink",
61
+ "StdlibLoggingSink",
62
+ "NullObservabilitySink",
63
+ "RecordingObservabilitySink",
64
+ # Domain models
65
+ "Entity",
66
+ "Assertion",
67
+ "AssertionEvent",
68
+ "Namespace",
69
+ # Errors
70
+ "OntolithError",
71
+ "SchemaError",
72
+ "ValidationError",
73
+ "AuthError",
74
+ "CapabilityError",
75
+ "PolicyDenied",
76
+ "ConflictError",
77
+ "NotFoundError",
78
+ "StorageError",
79
+ "PluginError",
80
+ ]
@@ -0,0 +1,132 @@
1
+ """Assertion value object - atomic unit of knowledge.
2
+
3
+ Per SPEC §5.3: Assertions are append-only. Only status, valid_to, and
4
+ supersedes links may be modified after creation.
5
+ """
6
+
7
+ from datetime import datetime
8
+ from typing import Any, Literal
9
+
10
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
11
+
12
+
13
+ class Assertion(BaseModel):
14
+ """Atomic unit of knowledge with full provenance.
15
+
16
+ Attributes:
17
+ id: Unique identifier (ULID)
18
+ namespace: Namespace slug
19
+ subject: Entity ID this assertion is about
20
+ predicate: Qualified property/relation name (e.g., "Person.name")
21
+ value_kind: Whether value is a literal or entity reference
22
+ value_type: Type of literal value (if value_kind is "literal")
23
+ value: The actual value (literal or entity ID)
24
+ author: Principal ID who made this assertion
25
+ acting_as: Principal ID if delegated (optional)
26
+ source: Source of information (URI or description)
27
+ confidence: Author's stated belief (0.0-1.0)
28
+ rationale: Why this assertion was made
29
+ model: Model family+version for AI authors
30
+ asserted_at: When we learned this fact
31
+ valid_from: When the fact became/becomes true (defaults to asserted_at)
32
+ valid_to: When the fact stopped being true (NULL = still valid)
33
+ status: Current lifecycle state
34
+ proposal_id: Proposal that introduced this assertion
35
+ supersedes: Previous assertion ID if this supersedes another
36
+ metadata: Open JSON blob for future evolution
37
+ """
38
+
39
+ # Core identity
40
+ id: str
41
+ namespace: str
42
+ subject: str
43
+ predicate: str
44
+
45
+ # Value
46
+ value_kind: Literal["literal", "ref"]
47
+ value_type: str | None = None # Required when value_kind="literal"
48
+ value: str # Literal value or entity ID
49
+
50
+ # Provenance
51
+ author: str
52
+ acting_as: str | None = None
53
+ source: str | None = None
54
+ confidence: float | None = Field(None, ge=0.0, le=1.0)
55
+ rationale: str | None = None
56
+ model: str | None = None # Required for AI authors
57
+
58
+ # Temporal
59
+ asserted_at: datetime
60
+ valid_from: datetime | None = None # Defaults to asserted_at if not provided
61
+ valid_to: datetime | None = None
62
+
63
+ # Lifecycle
64
+ status: Literal["active", "superseded", "retracted", "flagged"] = "active"
65
+ proposal_id: str | None = None
66
+ supersedes: str | None = None
67
+
68
+ # Future evolution
69
+ metadata: dict[str, Any] = Field(default_factory=dict)
70
+
71
+ @model_validator(mode="before")
72
+ @classmethod
73
+ def set_defaults_and_validate(cls, values: dict[str, Any]) -> dict[str, Any]:
74
+ """Set valid_from default and validate value_type based on value_kind."""
75
+ # Default valid_from to asserted_at (SPEC §5.3)
76
+ if "valid_from" not in values or values["valid_from"] is None:
77
+ values["valid_from"] = values.get("asserted_at")
78
+
79
+ # Validate value_type based on value_kind
80
+ value_kind = values.get("value_kind")
81
+ value_type = values.get("value_type")
82
+
83
+ if value_kind == "literal" and value_type is None:
84
+ raise ValueError("value_type is required when value_kind is 'literal'")
85
+ if value_kind == "ref" and value_type is not None:
86
+ raise ValueError("value_type must be None when value_kind is 'ref'")
87
+
88
+ return values
89
+
90
+ model_config = ConfigDict(
91
+ frozen=True, # Immutable by default
92
+ # Only status, valid_to, supersedes can be "mutated" via copy(update={...})
93
+ )
94
+
95
+
96
+ class AssertionEvent(BaseModel):
97
+ """Append-only audit record of an assertion status mutation.
98
+
99
+ Assertions themselves are append-only (only status/valid_to/supersedes
100
+ mutate in place) — this event log makes each such mutation independently
101
+ attributable and timestamped, since the assertion row itself is
102
+ overwritten in place and doesn't retain who caused a given transition
103
+ or when it happened (only its current status).
104
+
105
+ Attributes:
106
+ id: Unique event ID (ULID)
107
+ assertion_id: Assertion this event was recorded against
108
+ actor: Principal ID accountable for the transition — the author of
109
+ the assertion that triggered supersession/contradiction during
110
+ conflict routing, or the principal who explicitly retracted/
111
+ resolved it
112
+ action: Which status transition this event records
113
+ at: When the transition occurred
114
+ successor_id: For action="superseded", the id of the assertion that
115
+ caused this supersession. Assertion.supersedes only records the
116
+ first predecessor when one incoming assertion supersedes several
117
+ concurrently-overlapping ones (KI-008) — querying events by
118
+ successor_id recovers the full predecessor set. None for other
119
+ actions.
120
+ """
121
+
122
+ id: str
123
+ assertion_id: str
124
+ actor: str
125
+ action: Literal["superseded", "flagged", "retracted", "reactivated"]
126
+ at: datetime
127
+ successor_id: str | None = None
128
+
129
+ model_config = ConfigDict(frozen=True)
130
+
131
+
132
+ __all__ = ["Assertion", "AssertionEvent"]
ontolith/core/clock.py ADDED
@@ -0,0 +1,116 @@
1
+ """Clock abstraction for deterministic time handling.
2
+
3
+ The Clock port ensures bitemporal logic is reproducible in tests by injecting
4
+ time rather than using datetime.now() directly in domain logic.
5
+ """
6
+
7
+ from abc import ABC, abstractmethod
8
+ from datetime import UTC, datetime, timedelta
9
+
10
+
11
+ class Clock(ABC):
12
+ """Abstract clock for deterministic time handling."""
13
+
14
+ @abstractmethod
15
+ def now(self) -> datetime:
16
+ """Return the current time.
17
+
18
+ Returns:
19
+ Current datetime in UTC.
20
+ """
21
+ ...
22
+
23
+
24
+ class SystemClock(Clock):
25
+ """Production clock using system time."""
26
+
27
+ def now(self) -> datetime:
28
+ """Return the current system time in UTC.
29
+
30
+ Returns:
31
+ Current datetime in UTC timezone.
32
+ """
33
+ return datetime.now(UTC)
34
+
35
+
36
+ class FixedClock(Clock):
37
+ """Test clock that returns a fixed time.
38
+
39
+ Useful for testing time-sensitive logic without flakiness.
40
+
41
+ Args:
42
+ fixed_time: The time to always return, or current time if None.
43
+
44
+ Example:
45
+ >>> clock = FixedClock("2025-01-01T00:00:00Z")
46
+ >>> clock.now()
47
+ datetime.datetime(2025, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)
48
+ >>> clock.advance(days=1)
49
+ >>> clock.now()
50
+ datetime.datetime(2025, 1, 2, 0, 0, tzinfo=datetime.timezone.utc)
51
+ """
52
+
53
+ def __init__(self, fixed_time: str | datetime | None = None) -> None:
54
+ """Initialize with a fixed time.
55
+
56
+ Args:
57
+ fixed_time: ISO-8601 string, datetime, or None for current time.
58
+ """
59
+ if fixed_time is None:
60
+ self._time = datetime.now(UTC)
61
+ elif isinstance(fixed_time, str):
62
+ self._time = datetime.fromisoformat(fixed_time.replace("Z", "+00:00"))
63
+ else:
64
+ self._time = fixed_time
65
+
66
+ # Ensure timezone-aware
67
+ if self._time.tzinfo is None:
68
+ self._time = self._time.replace(tzinfo=UTC)
69
+
70
+ def now(self) -> datetime:
71
+ """Return the fixed time.
72
+
73
+ Returns:
74
+ The configured fixed datetime.
75
+ """
76
+ return self._time
77
+
78
+ def set(self, time: str | datetime) -> None:
79
+ """Set the clock to a new time.
80
+
81
+ Args:
82
+ time: ISO-8601 string or datetime to set.
83
+ """
84
+ if isinstance(time, str):
85
+ self._time = datetime.fromisoformat(time.replace("Z", "+00:00"))
86
+ else:
87
+ self._time = time
88
+
89
+ if self._time.tzinfo is None:
90
+ self._time = self._time.replace(tzinfo=UTC)
91
+
92
+ def advance(
93
+ self,
94
+ *,
95
+ days: int = 0,
96
+ hours: int = 0,
97
+ minutes: int = 0,
98
+ seconds: int = 0,
99
+ ) -> None:
100
+ """Advance the clock by the specified duration.
101
+
102
+ Args:
103
+ days: Number of days to advance.
104
+ hours: Number of hours to advance.
105
+ minutes: Number of minutes to advance.
106
+ seconds: Number of seconds to advance.
107
+ """
108
+ self._time += timedelta(
109
+ days=days,
110
+ hours=hours,
111
+ minutes=minutes,
112
+ seconds=seconds,
113
+ )
114
+
115
+
116
+ __all__ = ["Clock", "SystemClock", "FixedClock"]
@@ -0,0 +1,141 @@
1
+ """Embedder port for hybrid retrieval (SPEC §11.3, §14).
2
+
3
+ The Embedder port converts text into vectors for semantic search over the
4
+ vector store (StorageBackend.vector_upsert/vector_search). It is a pure
5
+ text-to-vector transform with no knowledge of KB state — the same shape as
6
+ Clock and IdProvider, not a capability-scoped plugin actor like Importer or
7
+ Exporter (see plugins/ports.py's docstring for that distinction).
8
+
9
+ Every Embedder implementation MUST return L2-unit-normalized vectors: this
10
+ makes ranking by ascending L2 distance equivalent to ranking by descending
11
+ cosine similarity (for unit vectors u, v: ||u-v||^2 = 2 - 2*cos_sim(u, v)),
12
+ which lets StorageBackend.vector_search use one distance metric consistently
13
+ across backends without an explicit similarity-function parameter.
14
+ """
15
+
16
+ import hashlib
17
+ import math
18
+ import re
19
+ from typing import Protocol
20
+
21
+ _TOKEN_RE = re.compile(r"\w+")
22
+
23
+
24
+ class Embedder(Protocol):
25
+ """Converts text into L2-unit-normalized embedding vectors."""
26
+
27
+ name: str
28
+ dim: int
29
+
30
+ def embed(self, texts: list[str]) -> list[list[float]]:
31
+ """Embed a batch of texts.
32
+
33
+ Args:
34
+ texts: Texts to embed.
35
+
36
+ Returns:
37
+ One L2-unit-normalized vector of length `dim` per input text, in
38
+ the same order. A text with no extractable signal (empty or
39
+ whitespace-only) yields the zero vector, not an error.
40
+ """
41
+ ...
42
+
43
+
44
+ class HashingEmbedder:
45
+ """Deterministic, dependency-free default Embedder.
46
+
47
+ Implements the feature-hashing trick (Weinberger et al., 2009): each
48
+ token hashes to a bucket index and a sign, avoiding the need for a
49
+ pre-built vocabulary or any numeric library. This is a placeholder for
50
+ real semantic quality, not a substitute for a model-backed Embedder —
51
+ it guarantees deterministic, reproducible vectors for a given text, not
52
+ meaningful semantic similarity between unrelated texts.
53
+
54
+ Uses hashlib.sha256, not Python's built-in hash(), because str hashing
55
+ is randomly salted per process (PYTHONHASHSEED) and would break
56
+ determinism across runs.
57
+ """
58
+
59
+ name = "hashing-embedder"
60
+
61
+ def __init__(self, dim: int = 256) -> None:
62
+ """Initialize with a fixed output dimensionality.
63
+
64
+ Args:
65
+ dim: Length of each output vector.
66
+ """
67
+ self.dim = dim
68
+
69
+ def embed(self, texts: list[str]) -> list[list[float]]:
70
+ """Embed a batch of texts via feature hashing.
71
+
72
+ Args:
73
+ texts: Texts to embed.
74
+
75
+ Returns:
76
+ One L2-unit-normalized vector of length `dim` per input text.
77
+ """
78
+ return [self._embed_one(text) for text in texts]
79
+
80
+ def _embed_one(self, text: str) -> list[float]:
81
+ """Embed a single text via feature hashing."""
82
+ vec = [0.0] * self.dim
83
+ for token in _TOKEN_RE.findall(text.lower()):
84
+ digest = hashlib.sha256(token.encode("utf-8")).digest()
85
+ bucket = int.from_bytes(digest[:8], "big") % self.dim
86
+ sign = 1.0 if digest[8] & 1 else -1.0
87
+ vec[bucket] += sign
88
+
89
+ norm = math.sqrt(sum(v * v for v in vec))
90
+ if norm == 0.0:
91
+ return vec
92
+ return [v / norm for v in vec]
93
+
94
+
95
+ class LookupEmbedder:
96
+ """Deterministic test double backed by an explicit text-to-vector map.
97
+
98
+ Unlike HashingEmbedder, output is fully predictable — use this in tests
99
+ that need to assert exact rank order from semantic search.
100
+ """
101
+
102
+ name = "lookup-embedder"
103
+
104
+ def __init__(
105
+ self, mapping: dict[str, list[float]], dim: int, default: list[float] | None = None
106
+ ) -> None:
107
+ """Initialize with an explicit text-to-vector mapping.
108
+
109
+ Args:
110
+ mapping: Exact text to vector lookup.
111
+ dim: Length of vectors in `mapping` and `default`.
112
+ default: Vector to return for unmapped texts, or None to raise.
113
+ """
114
+ self._mapping = mapping
115
+ self.dim = dim
116
+ self._default = default
117
+
118
+ def embed(self, texts: list[str]) -> list[list[float]]:
119
+ """Look up each text's vector.
120
+
121
+ Args:
122
+ texts: Texts to look up.
123
+
124
+ Returns:
125
+ The mapped (or default) vector per input text.
126
+
127
+ Raises:
128
+ KeyError: A text is not in `mapping` and no `default` was given.
129
+ """
130
+ result = []
131
+ for text in texts:
132
+ if text in self._mapping:
133
+ result.append(self._mapping[text])
134
+ elif self._default is not None:
135
+ result.append(self._default)
136
+ else:
137
+ raise KeyError(f"LookupEmbedder: no vector mapped for {text!r}")
138
+ return result
139
+
140
+
141
+ __all__ = ["Embedder", "HashingEmbedder", "LookupEmbedder"]
@@ -0,0 +1,34 @@
1
+ """Entity value object - concrete individual of a concept.
2
+
3
+ Per SPEC §5.2: Entities carry no attribute values directly - all attributes
4
+ are Assertions about the entity.
5
+ """
6
+
7
+ from datetime import datetime
8
+
9
+ from pydantic import BaseModel, ConfigDict
10
+
11
+
12
+ class Entity(BaseModel):
13
+ """Concrete individual of a concept.
14
+
15
+ Attributes:
16
+ id: Unique identifier (ULID)
17
+ namespace: Namespace slug
18
+ concept: Concept name (e.g., "Person", "Organization")
19
+ natural_key: Optional unique key within (namespace, concept)
20
+ created_at: When this entity was created
21
+ created_by: Principal ID who created this entity
22
+ """
23
+
24
+ id: str
25
+ namespace: str
26
+ concept: str
27
+ natural_key: str | None = None
28
+ created_at: datetime
29
+ created_by: str
30
+
31
+ model_config = ConfigDict(frozen=True) # Immutable
32
+
33
+
34
+ __all__ = ["Entity"]