lorebase-core 0.2.3__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 (94) hide show
  1. lorebase_core/__init__.py +1 -0
  2. lorebase_core/canonical/__init__.py +35 -0
  3. lorebase_core/canonical/hashing.py +128 -0
  4. lorebase_core/canonical/jcs.py +162 -0
  5. lorebase_core/expr/__init__.py +118 -0
  6. lorebase_core/expr/ast.py +443 -0
  7. lorebase_core/expr/checker.py +428 -0
  8. lorebase_core/expr/compiler.py +87 -0
  9. lorebase_core/expr/context.py +82 -0
  10. lorebase_core/expr/derivations.py +306 -0
  11. lorebase_core/expr/environment.py +176 -0
  12. lorebase_core/expr/errors.py +98 -0
  13. lorebase_core/expr/evaluator.py +414 -0
  14. lorebase_core/expr/functions.py +898 -0
  15. lorebase_core/expr/limits.py +52 -0
  16. lorebase_core/expr/operations.py +479 -0
  17. lorebase_core/expr/ops.py +243 -0
  18. lorebase_core/expr/resolver.py +96 -0
  19. lorebase_core/expr/rows.py +148 -0
  20. lorebase_core/expr/sensitivity.py +337 -0
  21. lorebase_core/expr/types.py +227 -0
  22. lorebase_core/expr/validation.py +51 -0
  23. lorebase_core/expr/values.py +169 -0
  24. lorebase_core/expr/visibility.py +363 -0
  25. lorebase_core/infer/__init__.py +36 -0
  26. lorebase_core/infer/columns.py +621 -0
  27. lorebase_core/infer/engine.py +546 -0
  28. lorebase_core/infer/keys.py +164 -0
  29. lorebase_core/infer/names.py +145 -0
  30. lorebase_core/infer/options.py +290 -0
  31. lorebase_core/infer/profile.py +54 -0
  32. lorebase_core/infer/proposal.py +122 -0
  33. lorebase_core/infer/references.py +314 -0
  34. lorebase_core/infer/suggestions.py +731 -0
  35. lorebase_core/infer/tables.py +81 -0
  36. lorebase_core/merge/__init__.py +89 -0
  37. lorebase_core/merge/conflicts.py +262 -0
  38. lorebase_core/merge/consistency.py +440 -0
  39. lorebase_core/merge/data_merge.py +522 -0
  40. lorebase_core/merge/engine.py +476 -0
  41. lorebase_core/merge/index.py +100 -0
  42. lorebase_core/merge/objects.py +204 -0
  43. lorebase_core/merge/policies.py +322 -0
  44. lorebase_core/merge/resolve.py +321 -0
  45. lorebase_core/merge/schema_merge.py +429 -0
  46. lorebase_core/merge/state.py +443 -0
  47. lorebase_core/model/__init__.py +245 -0
  48. lorebase_core/model/attribute.py +72 -0
  49. lorebase_core/model/base.py +61 -0
  50. lorebase_core/model/derivation.py +95 -0
  51. lorebase_core/model/domain.py +71 -0
  52. lorebase_core/model/embedding.py +193 -0
  53. lorebase_core/model/ids.py +78 -0
  54. lorebase_core/model/kind.py +85 -0
  55. lorebase_core/model/lineage.py +114 -0
  56. lorebase_core/model/operation.py +161 -0
  57. lorebase_core/model/paths.py +141 -0
  58. lorebase_core/model/record.py +127 -0
  59. lorebase_core/model/relation.py +83 -0
  60. lorebase_core/model/schema.py +219 -0
  61. lorebase_core/model/source.py +204 -0
  62. lorebase_core/model/trait.py +37 -0
  63. lorebase_core/model/vault.py +43 -0
  64. lorebase_core/model/yaml_schema.py +1058 -0
  65. lorebase_core/query/__init__.py +97 -0
  66. lorebase_core/query/dsl.py +227 -0
  67. lorebase_core/query/resolve.py +794 -0
  68. lorebase_core/revision/__init__.py +59 -0
  69. lorebase_core/revision/ancestry.py +272 -0
  70. lorebase_core/revision/changeset.py +62 -0
  71. lorebase_core/revision/revision.py +121 -0
  72. lorebase_core/tree/__init__.py +76 -0
  73. lorebase_core/tree/diff.py +119 -0
  74. lorebase_core/tree/hamt.py +333 -0
  75. lorebase_core/tree/node.py +280 -0
  76. lorebase_core/tree/params.py +33 -0
  77. lorebase_core/tree/root.py +70 -0
  78. lorebase_core/tree/store.py +98 -0
  79. lorebase_core/types/__init__.py +71 -0
  80. lorebase_core/types/casts.py +271 -0
  81. lorebase_core/types/constraints.py +299 -0
  82. lorebase_core/types/encoding.py +538 -0
  83. lorebase_core/types/errors.py +41 -0
  84. lorebase_core/types/matching.py +40 -0
  85. lorebase_core/types/types.py +187 -0
  86. lorebase_core/validate/__init__.py +49 -0
  87. lorebase_core/validate/changes.py +70 -0
  88. lorebase_core/validate/changeset.py +551 -0
  89. lorebase_core/validate/data_rules.py +291 -0
  90. lorebase_core/validate/issues.py +123 -0
  91. lorebase_core/validate/schema_rules.py +1304 -0
  92. lorebase_core-0.2.3.dist-info/METADATA +9 -0
  93. lorebase_core-0.2.3.dist-info/RECORD +94 -0
  94. lorebase_core-0.2.3.dist-info/WHEEL +4 -0
@@ -0,0 +1,59 @@
1
+ """Revisions, changesets, ancestry and merge base (design 6.3, 6.4, 6.8)."""
2
+
3
+ from lorebase_core.revision.ancestry import (
4
+ InMemoryRevisionGraph,
5
+ MergeBaseError,
6
+ MergeBaseResult,
7
+ MergeStatus,
8
+ RevisionGraph,
9
+ UnknownRevisionError,
10
+ VirtualBaseBuilder,
11
+ VirtualRevision,
12
+ is_ancestor,
13
+ lowest_common_ancestors,
14
+ merge_base,
15
+ )
16
+ from lorebase_core.revision.changeset import (
17
+ ChangesetEntry,
18
+ changeset_between,
19
+ changeset_stats,
20
+ changeset_tree_changes,
21
+ )
22
+ from lorebase_core.revision.revision import (
23
+ ROOT_GENERATION,
24
+ Author,
25
+ Hash,
26
+ Origin,
27
+ Revision,
28
+ RevisionStats,
29
+ format_timestamp,
30
+ next_generation,
31
+ revision_id,
32
+ )
33
+
34
+ __all__ = [
35
+ "ROOT_GENERATION",
36
+ "Author",
37
+ "ChangesetEntry",
38
+ "Hash",
39
+ "InMemoryRevisionGraph",
40
+ "MergeBaseError",
41
+ "MergeBaseResult",
42
+ "MergeStatus",
43
+ "Origin",
44
+ "Revision",
45
+ "RevisionGraph",
46
+ "RevisionStats",
47
+ "UnknownRevisionError",
48
+ "VirtualBaseBuilder",
49
+ "VirtualRevision",
50
+ "changeset_between",
51
+ "changeset_stats",
52
+ "changeset_tree_changes",
53
+ "format_timestamp",
54
+ "is_ancestor",
55
+ "lowest_common_ancestors",
56
+ "merge_base",
57
+ "next_generation",
58
+ "revision_id",
59
+ ]
@@ -0,0 +1,272 @@
1
+ """Revision ancestry: generation numbers, lowest common ancestors and merge base.
2
+
3
+ Design 6.4 and 6.8 step 0, AC-13.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import heapq
9
+ from collections.abc import Iterable, Sequence
10
+ from dataclasses import dataclass
11
+ from enum import StrEnum
12
+ from typing import Protocol, runtime_checkable
13
+
14
+ from lorebase_core.revision.revision import Revision, next_generation, revision_id
15
+
16
+ __all__ = [
17
+ "InMemoryRevisionGraph",
18
+ "MergeBaseError",
19
+ "MergeBaseResult",
20
+ "MergeStatus",
21
+ "RevisionGraph",
22
+ "UnknownRevisionError",
23
+ "VirtualBaseBuilder",
24
+ "VirtualRevision",
25
+ "is_ancestor",
26
+ "lowest_common_ancestors",
27
+ "merge_base",
28
+ ]
29
+
30
+
31
+ class UnknownRevisionError(KeyError):
32
+ """Raised when a Revision id is not in the graph."""
33
+
34
+
35
+ class MergeBaseError(ValueError):
36
+ """Raised when a merge base cannot be determined."""
37
+
38
+
39
+ @runtime_checkable
40
+ class RevisionGraph(Protocol):
41
+ """Read access to the Revision DAG."""
42
+
43
+ def parents(self, revision_id: bytes) -> Sequence[bytes]: ...
44
+
45
+ def generation(self, revision_id: bytes) -> int: ...
46
+
47
+
48
+ class InMemoryRevisionGraph:
49
+ """Dictionary-backed :class:`RevisionGraph`."""
50
+
51
+ def __init__(self) -> None:
52
+ self._parents: dict[bytes, tuple[bytes, ...]] = {}
53
+ self._generations: dict[bytes, int] = {}
54
+
55
+ def add(self, revision: bytes, parents: Iterable[bytes] = ()) -> int:
56
+ """Register a Revision whose parents are already known; return its generation."""
57
+ parent_ids = tuple(parents)
58
+ known = self._parents.get(revision)
59
+ if known is not None:
60
+ if known != parent_ids:
61
+ raise ValueError("revision already registered with different parents")
62
+ return self._generations[revision]
63
+ generation = next_generation(self.generation(p) for p in parent_ids)
64
+ self._parents[revision] = parent_ids
65
+ self._generations[revision] = generation
66
+ return generation
67
+
68
+ def add_revision(self, revision: Revision) -> bytes:
69
+ """Register a Revision object and return its id."""
70
+ rev_id = revision_id(revision)
71
+ self.add(rev_id, revision.parents)
72
+ return rev_id
73
+
74
+ def parents(self, revision_id: bytes) -> Sequence[bytes]:
75
+ try:
76
+ return self._parents[revision_id]
77
+ except KeyError:
78
+ raise UnknownRevisionError(revision_id.hex()) from None
79
+
80
+ def generation(self, revision_id: bytes) -> int:
81
+ try:
82
+ return self._generations[revision_id]
83
+ except KeyError:
84
+ raise UnknownRevisionError(revision_id.hex()) from None
85
+
86
+ def __contains__(self, revision_id: object) -> bool:
87
+ return revision_id in self._parents
88
+
89
+
90
+ def is_ancestor(graph: RevisionGraph, ancestor: bytes, descendant: bytes) -> bool:
91
+ """True when ``ancestor`` is ``descendant`` or reachable from it through parents."""
92
+ if ancestor == descendant:
93
+ graph.generation(ancestor)
94
+ return True
95
+ floor = graph.generation(ancestor)
96
+ seen = {descendant}
97
+ stack = [descendant]
98
+ while stack:
99
+ for parent in graph.parents(stack.pop()):
100
+ if parent == ancestor:
101
+ return True
102
+ # Parents always have a lower generation, so nothing at or below
103
+ # the ancestor's generation can lead to it.
104
+ if parent not in seen and graph.generation(parent) > floor:
105
+ seen.add(parent)
106
+ stack.append(parent)
107
+ return False
108
+
109
+
110
+ _OURS = 1
111
+ _THEIRS = 2
112
+ _STALE = 4
113
+ _BOTH = _OURS | _THEIRS
114
+
115
+
116
+ def lowest_common_ancestors(graph: RevisionGraph, a: bytes, b: bytes) -> tuple[bytes, ...]:
117
+ """All lowest common ancestors of ``a`` and ``b``, ordered by (generation, id).
118
+
119
+ Walks both histories at once in decreasing generation order. Parents always
120
+ have a lower generation than their children, so when a Revision is visited
121
+ every path from ``a`` or ``b`` to it has already been seen: a Revision
122
+ reached from both sides is a common ancestor, and everything below it is
123
+ marked stale. The walk stops as soon as no unvisited, non-stale Revision
124
+ is reachable from both sides.
125
+ """
126
+ if a == b:
127
+ graph.generation(a)
128
+ return (a,)
129
+ flags = {a: _OURS, b: _THEIRS}
130
+ heap = [(-graph.generation(a), a), (-graph.generation(b), b)]
131
+ heapq.heapify(heap)
132
+ pending: dict[int, set[bytes]] = {_OURS: {a}, _THEIRS: {b}}
133
+ visited: set[bytes] = set()
134
+ found: list[bytes] = []
135
+ while pending[_OURS] and pending[_THEIRS]:
136
+ _, current = heapq.heappop(heap)
137
+ if current in visited:
138
+ continue
139
+ visited.add(current)
140
+ pending[_OURS].discard(current)
141
+ pending[_THEIRS].discard(current)
142
+ state = flags[current]
143
+ if state & _BOTH == _BOTH and not state & _STALE:
144
+ found.append(current)
145
+ state |= _STALE
146
+ flags[current] = state
147
+ for parent in graph.parents(current):
148
+ old = flags.get(parent, 0)
149
+ new = old | state
150
+ if new == old:
151
+ continue
152
+ flags[parent] = new
153
+ heapq.heappush(heap, (-graph.generation(parent), parent))
154
+ for side in (_OURS, _THEIRS):
155
+ if new & side and not new & _STALE:
156
+ pending[side].add(parent)
157
+ else:
158
+ pending[side].discard(parent)
159
+ return tuple(sorted(found, key=lambda rev: (graph.generation(rev), rev)))
160
+
161
+
162
+ class MergeStatus(StrEnum):
163
+ UP_TO_DATE = "up_to_date"
164
+ """Theirs is already contained in ours: nothing to merge."""
165
+ FAST_FORWARD = "fast_forward"
166
+ """Ours is an ancestor of theirs: the target can move to theirs."""
167
+ MERGE = "merge"
168
+ """Histories diverged: a three-way merge against ``base`` is needed."""
169
+ UNRELATED = "unrelated"
170
+ """No common ancestor: merge against the empty tree."""
171
+
172
+
173
+ class VirtualBaseBuilder(Protocol):
174
+ """Builds a synthetic merge of two Revisions and returns its Revision id.
175
+
176
+ Called with the merge base of the pair (``None`` when they share no
177
+ ancestor). Implemented by the merge engine; the result is never committed.
178
+ """
179
+
180
+ def __call__(self, base: bytes | None, ours: bytes, theirs: bytes) -> bytes: ...
181
+
182
+
183
+ @dataclass(frozen=True, slots=True)
184
+ class VirtualRevision:
185
+ """A synthetic Revision produced while building a virtual merge base."""
186
+
187
+ id: bytes
188
+ parents: tuple[bytes, bytes]
189
+
190
+
191
+ @dataclass(frozen=True, slots=True)
192
+ class MergeBaseResult:
193
+ status: MergeStatus
194
+ base: bytes | None
195
+ """Revision to use as merge base: a real ancestor, a virtual Revision, or ``None``."""
196
+ common_ancestors: tuple[bytes, ...]
197
+ """The lowest common ancestors, ordered by (generation, id)."""
198
+ virtual_revisions: tuple[VirtualRevision, ...] = ()
199
+ """Synthetic Revisions built for criss-cross histories, in creation order."""
200
+
201
+ @property
202
+ def virtual(self) -> bool:
203
+ return bool(self.virtual_revisions)
204
+
205
+
206
+ def merge_base(
207
+ graph: RevisionGraph,
208
+ ours: bytes,
209
+ theirs: bytes,
210
+ build_virtual: VirtualBaseBuilder | None = None,
211
+ ) -> MergeBaseResult:
212
+ """Determine how to merge ``theirs`` (source head) into ``ours`` (target head).
213
+
214
+ With several lowest common ancestors (criss-cross history) they are merged
215
+ pairwise and recursively into a virtual base through ``build_virtual``.
216
+ """
217
+ common = lowest_common_ancestors(graph, ours, theirs)
218
+ if theirs in common:
219
+ return MergeBaseResult(MergeStatus.UP_TO_DATE, theirs, common)
220
+ if ours in common:
221
+ return MergeBaseResult(MergeStatus.FAST_FORWARD, ours, common)
222
+ if not common:
223
+ return MergeBaseResult(MergeStatus.UNRELATED, None, common)
224
+ if len(common) == 1:
225
+ return MergeBaseResult(MergeStatus.MERGE, common[0], common)
226
+ if build_virtual is None:
227
+ raise MergeBaseError("several lowest common ancestors and no virtual base builder")
228
+ overlay = _OverlayGraph(graph)
229
+ base = _virtual_base(overlay, common, build_virtual)
230
+ return MergeBaseResult(MergeStatus.MERGE, base, common, tuple(overlay.added))
231
+
232
+
233
+ def _virtual_base(
234
+ graph: _OverlayGraph, ancestors: Sequence[bytes], build: VirtualBaseBuilder
235
+ ) -> bytes:
236
+ merged = ancestors[0]
237
+ for following in ancestors[1:]:
238
+ inner = lowest_common_ancestors(graph, merged, following)
239
+ if not inner:
240
+ base: bytes | None = None
241
+ elif len(inner) == 1:
242
+ base = inner[0]
243
+ else:
244
+ base = _virtual_base(graph, inner, build)
245
+ virtual = build(base, merged, following)
246
+ graph.add_virtual(virtual, (merged, following))
247
+ merged = virtual
248
+ return merged
249
+
250
+
251
+ class _OverlayGraph:
252
+ """A graph extended with the virtual Revisions of one merge-base computation."""
253
+
254
+ def __init__(self, base: RevisionGraph) -> None:
255
+ self._base = base
256
+ self._virtual: dict[bytes, tuple[tuple[bytes, bytes], int]] = {}
257
+ self.added: list[VirtualRevision] = []
258
+
259
+ def add_virtual(self, revision: bytes, parents: tuple[bytes, bytes]) -> None:
260
+ if revision in self._virtual:
261
+ raise MergeBaseError("virtual base builder returned a duplicate Revision id")
262
+ generation = next_generation(self.generation(p) for p in parents)
263
+ self._virtual[revision] = (parents, generation)
264
+ self.added.append(VirtualRevision(revision, parents))
265
+
266
+ def parents(self, revision_id: bytes) -> Sequence[bytes]:
267
+ entry = self._virtual.get(revision_id)
268
+ return entry[0] if entry is not None else self._base.parents(revision_id)
269
+
270
+ def generation(self, revision_id: bytes) -> int:
271
+ entry = self._virtual.get(revision_id)
272
+ return entry[1] if entry is not None else self._base.generation(revision_id)
@@ -0,0 +1,62 @@
1
+ """Changesets: the delta of a Revision against its first parent (design 6.3)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable, Mapping
6
+ from dataclasses import dataclass, field
7
+ from types import MappingProxyType
8
+
9
+ from lorebase_core.canonical import HASH_SIZE
10
+ from lorebase_core.revision.revision import RevisionStats
11
+ from lorebase_core.tree import NodeStore, TreeChange, tree_diff
12
+
13
+ __all__ = ["ChangesetEntry", "changeset_between", "changeset_stats", "changeset_tree_changes"]
14
+
15
+ _NO_PROVENANCE: Mapping[str, str] = MappingProxyType({})
16
+
17
+
18
+ @dataclass(frozen=True, slots=True)
19
+ class ChangesetEntry:
20
+ """One changed element: ``None`` hashes mean absent.
21
+
22
+ ``provenance`` maps each changed Attribute id to its provenance entry id;
23
+ it is not part of any content hash (AC-08).
24
+ """
25
+
26
+ partition: str
27
+ key: str
28
+ old_hash: bytes | None
29
+ new_hash: bytes | None
30
+ provenance: Mapping[str, str] = field(default=_NO_PROVENANCE, compare=False)
31
+
32
+ def __post_init__(self) -> None:
33
+ if self.old_hash == self.new_hash:
34
+ raise ValueError("a changeset entry must change the element")
35
+ for h in (self.old_hash, self.new_hash):
36
+ if h is not None and len(h) != HASH_SIZE:
37
+ raise ValueError(f"hashes are {HASH_SIZE} bytes")
38
+
39
+
40
+ def changeset_between(
41
+ store: NodeStore, parent_root: bytes | None, root: bytes
42
+ ) -> list[ChangesetEntry]:
43
+ """Changeset of ``root`` against its first parent's root, without provenance."""
44
+ return [
45
+ ChangesetEntry(d.partition, d.key, d.hash_a, d.hash_b)
46
+ for d in tree_diff(store, parent_root, root)
47
+ ]
48
+
49
+
50
+ def changeset_tree_changes(entries: Iterable[ChangesetEntry]) -> list[TreeChange]:
51
+ """Tree changes that turn the parent tree into the Revision tree."""
52
+ return [TreeChange(e.partition, e.key, e.new_hash) for e in entries]
53
+
54
+
55
+ def changeset_stats(entries: Iterable[ChangesetEntry]) -> RevisionStats:
56
+ """Revision stats derived from a changeset."""
57
+ partitions: set[str] = set()
58
+ changes = 0
59
+ for entry in entries:
60
+ partitions.add(entry.partition)
61
+ changes += 1
62
+ return RevisionStats(partitions_changed=len(partitions), changes=changes)
@@ -0,0 +1,121 @@
1
+ """Revision objects and their content hash (design 6.4)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable
6
+ from datetime import UTC, datetime
7
+ from typing import Annotated
8
+
9
+ from pydantic import BaseModel, BeforeValidator, ConfigDict, Field, field_validator
10
+
11
+ from lorebase_core.canonical import HASH_SIZE, from_hex, hex, revision_hash
12
+
13
+ __all__ = [
14
+ "ROOT_GENERATION",
15
+ "Author",
16
+ "Hash",
17
+ "Origin",
18
+ "Revision",
19
+ "RevisionStats",
20
+ "format_timestamp",
21
+ "next_generation",
22
+ "revision_id",
23
+ ]
24
+
25
+ ROOT_GENERATION = 1
26
+ """Generation number of a Revision without parents."""
27
+
28
+
29
+ def _parse_hash(value: object) -> object:
30
+ if isinstance(value, str):
31
+ return from_hex(value)
32
+ if isinstance(value, bytes) and len(value) != HASH_SIZE:
33
+ raise ValueError(f"hashes are {HASH_SIZE} bytes")
34
+ return value
35
+
36
+
37
+ Hash = Annotated[bytes, BeforeValidator(_parse_hash)]
38
+ """A 32-byte hash; accepts raw bytes or lowercase hex."""
39
+
40
+
41
+ class _Frozen(BaseModel):
42
+ model_config = ConfigDict(frozen=True, extra="forbid")
43
+
44
+
45
+ class Author(_Frozen):
46
+ principal: str = Field(min_length=1)
47
+ on_behalf_of: str | None = None
48
+
49
+
50
+ class Origin(_Frozen):
51
+ type: str = Field(min_length=1)
52
+ ref: str | None = None
53
+
54
+
55
+ class RevisionStats(_Frozen):
56
+ partitions_changed: int = Field(ge=0)
57
+ changes: int = Field(ge=0)
58
+
59
+
60
+ class Revision(_Frozen):
61
+ """An immutable snapshot: tree root, parents and metadata.
62
+
63
+ ``parents[0]`` is the first parent (the target branch for merges).
64
+ """
65
+
66
+ parents: tuple[Hash, ...] = ()
67
+ root: Hash
68
+ domain: str = Field(min_length=1)
69
+ author: Author
70
+ message: str
71
+ origin: Origin
72
+ created_at: datetime
73
+ stats: RevisionStats
74
+
75
+ @field_validator("parents")
76
+ @classmethod
77
+ def _distinct_parents(cls, parents: tuple[bytes, ...]) -> tuple[bytes, ...]:
78
+ if len(set(parents)) != len(parents):
79
+ raise ValueError("parents must be distinct")
80
+ return parents
81
+
82
+ @field_validator("created_at")
83
+ @classmethod
84
+ def _utc(cls, value: datetime) -> datetime:
85
+ if value.tzinfo is None or value.utcoffset() is None:
86
+ raise ValueError("created_at must carry a time zone")
87
+ return value.astimezone(UTC)
88
+
89
+ def canonical_payload(self) -> dict[str, object]:
90
+ """The JSON document whose canonical form is hashed into the Revision id."""
91
+ return {
92
+ "parents": [hex(p) for p in self.parents],
93
+ "root": hex(self.root),
94
+ "domain": self.domain,
95
+ "author": self.author.model_dump(),
96
+ "message": self.message,
97
+ "origin": self.origin.model_dump(),
98
+ "created_at": format_timestamp(self.created_at),
99
+ "stats": self.stats.model_dump(),
100
+ }
101
+
102
+
103
+ def revision_id(revision: Revision) -> bytes:
104
+ """Revision id: ``sha256("lb1\\x00revision\\x00" + canonical_json)``."""
105
+ return revision_hash(revision.canonical_payload())
106
+
107
+
108
+ def next_generation(parent_generations: Iterable[int]) -> int:
109
+ """Generation of a Revision: ``1 + max(parent generations)``, or 1 without parents."""
110
+ return 1 + max(parent_generations, default=ROOT_GENERATION - 1)
111
+
112
+
113
+ def format_timestamp(value: datetime) -> str:
114
+ """RFC 3339 UTC with exactly six fractional digits."""
115
+ if value.tzinfo is None:
116
+ raise ValueError("timestamp must carry a time zone")
117
+ utc = value.astimezone(UTC)
118
+ return (
119
+ f"{utc.year:04d}-{utc.month:02d}-{utc.day:02d}"
120
+ f"T{utc.hour:02d}:{utc.minute:02d}:{utc.second:02d}.{utc.microsecond:06d}Z"
121
+ )
@@ -0,0 +1,76 @@
1
+ """Partitioned canonical HAMT: the Revision manifest (design 6.2, 6.7)."""
2
+
3
+ from lorebase_core.tree.diff import DiffEntry, tree_diff
4
+ from lorebase_core.tree.hamt import (
5
+ TreeChange,
6
+ empty_tree,
7
+ tree_apply,
8
+ tree_count,
9
+ tree_entries,
10
+ tree_get,
11
+ )
12
+ from lorebase_core.tree.node import (
13
+ BranchNode,
14
+ LeafNode,
15
+ Node,
16
+ NodeFormatError,
17
+ NodeType,
18
+ PartitionRef,
19
+ RootNode,
20
+ decode_node,
21
+ encode_node,
22
+ key_hash,
23
+ )
24
+ from lorebase_core.tree.params import DEFAULT_LEAF_SIZE, FANOUT, TreeParams
25
+ from lorebase_core.tree.root import (
26
+ LINK_KEY_SEPARATOR,
27
+ PARTITION_SCHEMA,
28
+ Tree,
29
+ kind_partition,
30
+ link_key,
31
+ relation_partition,
32
+ )
33
+ from lorebase_core.tree.store import (
34
+ InMemoryNodeStore,
35
+ MemoryNodeStore,
36
+ MissingNodeError,
37
+ NodeStore,
38
+ StagingNodeStore,
39
+ read_node,
40
+ )
41
+
42
+ __all__ = [
43
+ "DEFAULT_LEAF_SIZE",
44
+ "FANOUT",
45
+ "LINK_KEY_SEPARATOR",
46
+ "PARTITION_SCHEMA",
47
+ "BranchNode",
48
+ "DiffEntry",
49
+ "InMemoryNodeStore",
50
+ "LeafNode",
51
+ "MemoryNodeStore",
52
+ "MissingNodeError",
53
+ "Node",
54
+ "NodeFormatError",
55
+ "NodeStore",
56
+ "NodeType",
57
+ "PartitionRef",
58
+ "RootNode",
59
+ "StagingNodeStore",
60
+ "Tree",
61
+ "TreeChange",
62
+ "TreeParams",
63
+ "decode_node",
64
+ "empty_tree",
65
+ "encode_node",
66
+ "key_hash",
67
+ "kind_partition",
68
+ "link_key",
69
+ "read_node",
70
+ "relation_partition",
71
+ "tree_apply",
72
+ "tree_count",
73
+ "tree_diff",
74
+ "tree_entries",
75
+ "tree_get",
76
+ ]