rememberstack 0.1.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.
- rememberstack/__init__.py +9 -0
- rememberstack/adapters/__init__.py +42 -0
- rememberstack/adapters/codex_writer.py +221 -0
- rememberstack/adapters/markitdown_converter.py +42 -0
- rememberstack/adapters/openrouter.py +136 -0
- rememberstack/adapters/selfhost/__init__.py +54 -0
- rememberstack/adapters/selfhost/forget.py +66 -0
- rememberstack/adapters/selfhost/git.py +374 -0
- rememberstack/adapters/selfhost/lance.py +328 -0
- rememberstack/adapters/selfhost/minio.py +279 -0
- rememberstack/adapters/selfhost/mounts.py +249 -0
- rememberstack/adapters/selfhost/object_store.py +130 -0
- rememberstack/adapters/selfhost/projection.py +80 -0
- rememberstack/adapters/selfhost/queue.py +137 -0
- rememberstack/adapters/selfhost/telemetry.py +45 -0
- rememberstack/adapters/selfhost/watcher.py +70 -0
- rememberstack/adapters/testing/__init__.py +15 -0
- rememberstack/adapters/testing/cost_meter.py +13 -0
- rememberstack/adapters/testing/model_provider.py +83 -0
- rememberstack/adapters/testing/queue.py +43 -0
- rememberstack/adapters/testing/telemetry.py +22 -0
- rememberstack/client.py +19 -0
- rememberstack/core/__init__.py +127 -0
- rememberstack/core/blockizer.py +189 -0
- rememberstack/core/chunker.py +216 -0
- rememberstack/core/consumption_skill.py +275 -0
- rememberstack/core/conversion.py +76 -0
- rememberstack/core/core_manifest.py +598 -0
- rememberstack/core/extension_packs.py +124 -0
- rememberstack/core/forget.py +17 -0
- rememberstack/core/knowledge_authored.py +276 -0
- rememberstack/core/knowledge_compile.py +215 -0
- rememberstack/core/knowledge_fact_sheet.py +210 -0
- rememberstack/core/knowledge_hashing.py +68 -0
- rememberstack/core/knowledge_planner.py +64 -0
- rememberstack/core/knowledge_writer.py +175 -0
- rememberstack/core/ranking.py +200 -0
- rememberstack/core/recipe_linter.py +149 -0
- rememberstack/core/section_snap.py +209 -0
- rememberstack/core/storage_routing.py +27 -0
- rememberstack/eval/__init__.py +53 -0
- rememberstack/eval/consumption.py +141 -0
- rememberstack/eval/contradiction.py +184 -0
- rememberstack/eval/harness.py +136 -0
- rememberstack/eval/lifecycle.py +400 -0
- rememberstack/eval/operational_scale.py +49 -0
- rememberstack/eval/resolution.py +255 -0
- rememberstack/eval/retrieval_spikes.py +50 -0
- rememberstack/eval/skeleton.py +231 -0
- rememberstack/llm/__init__.py +1 -0
- rememberstack/model/__init__.py +589 -0
- rememberstack/model/adjudication.py +100 -0
- rememberstack/model/auth.py +27 -0
- rememberstack/model/blocks.py +30 -0
- rememberstack/model/chunks.py +190 -0
- rememberstack/model/claims.py +162 -0
- rememberstack/model/client.py +98 -0
- rememberstack/model/clustering.py +54 -0
- rememberstack/model/component_version.py +124 -0
- rememberstack/model/consumption.py +88 -0
- rememberstack/model/conversion.py +31 -0
- rememberstack/model/deployment.py +53 -0
- rememberstack/model/documents.py +168 -0
- rememberstack/model/envelope.py +513 -0
- rememberstack/model/evaluation.py +72 -0
- rememberstack/model/forget.py +143 -0
- rememberstack/model/git.py +13 -0
- rememberstack/model/knowledge.py +840 -0
- rememberstack/model/knowledge_authored.py +325 -0
- rememberstack/model/knowledge_planner.py +431 -0
- rememberstack/model/lifecycle.py +42 -0
- rememberstack/model/model_provider.py +78 -0
- rememberstack/model/mounts.py +24 -0
- rememberstack/model/object_store.py +21 -0
- rememberstack/model/operational_scale.py +59 -0
- rememberstack/model/operations.py +153 -0
- rememberstack/model/processing.py +228 -0
- rememberstack/model/queue.py +73 -0
- rememberstack/model/recipes.py +83 -0
- rememberstack/model/relations.py +79 -0
- rememberstack/model/resolution.py +83 -0
- rememberstack/model/retrieval_spikes.py +62 -0
- rememberstack/model/sections.py +120 -0
- rememberstack/model/telemetry.py +30 -0
- rememberstack/ports/__init__.py +29 -0
- rememberstack/ports/auth.py +16 -0
- rememberstack/ports/connector.py +23 -0
- rememberstack/ports/cost_meter.py +17 -0
- rememberstack/ports/forget.py +20 -0
- rememberstack/ports/git.py +20 -0
- rememberstack/ports/model_provider.py +28 -0
- rememberstack/ports/mounts.py +16 -0
- rememberstack/ports/object_store.py +27 -0
- rememberstack/ports/p1_index.py +92 -0
- rememberstack/ports/purge.py +93 -0
- rememberstack/ports/queue.py +23 -0
- rememberstack/ports/telemetry.py +21 -0
- rememberstack/profiles/__init__.py +22 -0
- rememberstack/profiles/selfhost.py +324 -0
- rememberstack/profiles/selfhost_forget.py +158 -0
- rememberstack/profiles/selfhost_operations.py +95 -0
- rememberstack/py.typed +1 -0
- rememberstack/spine/__init__.py +93 -0
- rememberstack/spine/admission.py +26 -0
- rememberstack/spine/backfill.py +168 -0
- rememberstack/spine/catalog_contract.py +742 -0
- rememberstack/spine/chunk_catalog.py +237 -0
- rememberstack/spine/claim_catalog.py +298 -0
- rememberstack/spine/clustering.py +740 -0
- rememberstack/spine/component_versions.py +208 -0
- rememberstack/spine/consumption.py +81 -0
- rememberstack/spine/deployment_bootstrap.py +445 -0
- rememberstack/spine/document_catalog.py +621 -0
- rememberstack/spine/entity_registry.py +205 -0
- rememberstack/spine/extension_packs.py +220 -0
- rememberstack/spine/fact_catalog.py +571 -0
- rememberstack/spine/forget.py +1753 -0
- rememberstack/spine/knowledge.py +5467 -0
- rememberstack/spine/lifecycle.py +1071 -0
- rememberstack/spine/migrations/__init__.py +1 -0
- rememberstack/spine/migrations/_helpers.py +153 -0
- rememberstack/spine/migrations/env.py +58 -0
- rememberstack/spine/migrations/script.py.mako +27 -0
- rememberstack/spine/migrations/versions/__init__.py +1 -0
- rememberstack/spine/migrations/versions/p0_02_0001_extensions_enums.py +189 -0
- rememberstack/spine/migrations/versions/p0_02_0002_infrastructure_registries.py +321 -0
- rememberstack/spine/migrations/versions/p0_02_0003_entities_evaluation_e0_e1.py +631 -0
- rememberstack/spine/migrations/versions/p0_02_0004_claims_facts_evidence.py +411 -0
- rememberstack/spine/migrations/versions/p0_02_0005_projection_knowledge_retrieval.py +391 -0
- rememberstack/spine/migrations/versions/p0_02_0006_partitions_views.py +158 -0
- rememberstack/spine/migrations/versions/p2_06_0007_invalidated_outcome.py +26 -0
- rememberstack/spine/migrations/versions/p3_01_0008_document_version_target.py +58 -0
- rememberstack/spine/migrations/versions/p3_05_0009_reconcile_stage.py +27 -0
- rememberstack/spine/migrations/versions/p3_07_0010_lifecycle_eval_suite.py +25 -0
- rememberstack/spine/migrations/versions/p4_01_0011_survivor_view_rewrite.py +57 -0
- rememberstack/spine/migrations/versions/p6_02_0012_knowledge_compile_recovery.py +58 -0
- rememberstack/spine/migrations/versions/p6_04_0013_knowledge_writer_ledger.py +46 -0
- rememberstack/spine/migrations/versions/p6_05_0014_knowledge_planner_runtime.py +217 -0
- rememberstack/spine/migrations/versions/p6_06_0015_authored_dispatch_runtime.py +38 -0
- rememberstack/spine/migrations/versions/p7_02_0016_operational_eval_suite.py +19 -0
- rememberstack/spine/migrations/versions/p7_05_0017_hard_forget.py +55 -0
- rememberstack/spine/observation_adjudication.py +778 -0
- rememberstack/spine/operations.py +298 -0
- rememberstack/spine/projection.py +662 -0
- rememberstack/spine/recipes.py +276 -0
- rememberstack/spine/resolver.py +763 -0
- rememberstack/spine/review.py +650 -0
- rememberstack/spine/settings.py +22 -0
- rememberstack/spine/supersession.py +510 -0
- rememberstack/spine/sync.py +128 -0
- rememberstack/spine/work_ledger.py +816 -0
- rememberstack/surfaces/__init__.py +110 -0
- rememberstack/surfaces/cli.py +447 -0
- rememberstack/surfaces/consumption_skill.py +87 -0
- rememberstack/surfaces/graph_queries.py +698 -0
- rememberstack/surfaces/http_api.py +377 -0
- rememberstack/surfaces/mcp.py +67 -0
- rememberstack/surfaces/query_engine.py +1591 -0
- rememberstack/surfaces/recipe_executor.py +185 -0
- rememberstack/surfaces/recipe_surface.py +219 -0
- rememberstack/surfaces/remote_mcp.py +133 -0
- rememberstack/surfaces/sdk.py +324 -0
- rememberstack/workers/__init__.py +155 -0
- rememberstack/workers/base.py +312 -0
- rememberstack/workers/e0.py +577 -0
- rememberstack/workers/e1.py +425 -0
- rememberstack/workers/e2.py +525 -0
- rememberstack/workers/e3.py +434 -0
- rememberstack/workers/forget.py +299 -0
- rememberstack/workers/knowledge_authored.py +146 -0
- rememberstack/workers/knowledge_driver.py +735 -0
- rememberstack/workers/knowledge_fact_sheet.py +123 -0
- rememberstack/workers/knowledge_planner.py +325 -0
- rememberstack/workers/knowledge_writer.py +393 -0
- rememberstack/workers/operations.py +42 -0
- rememberstack/workers/p1.py +234 -0
- rememberstack/workers/p2.py +513 -0
- rememberstack/workers/p2_analytics.py +276 -0
- rememberstack/workers/p3.py +673 -0
- rememberstack/workers/reconcile.py +485 -0
- rememberstack/workers/sync.py +168 -0
- rememberstack-0.1.0.dist-info/METADATA +213 -0
- rememberstack-0.1.0.dist-info/RECORD +186 -0
- rememberstack-0.1.0.dist-info/WHEEL +4 -0
- rememberstack-0.1.0.dist-info/entry_points.txt +2 -0
- rememberstack-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
"""Plain local-Git remote plus D74 affected-path history erasure."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from pathlib import PurePosixPath
|
|
5
|
+
import shlex
|
|
6
|
+
import subprocess
|
|
7
|
+
from typing import Protocol
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
|
|
10
|
+
from rememberstack.model import KRevision
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class KPathCatalog(Protocol):
|
|
14
|
+
"""Resolve synced owner blockers and manifest-nominated Git paths."""
|
|
15
|
+
|
|
16
|
+
def blocking_k_paths(self, *, deployment_id: UUID, doc_id: UUID) -> tuple[str, ...]:
|
|
17
|
+
"""Return owner-controlled paths with standing lineage citations."""
|
|
18
|
+
...
|
|
19
|
+
|
|
20
|
+
def k_paths_for_artifacts(
|
|
21
|
+
self, *, deployment_id: UUID, artifact_ids: tuple[UUID, ...]
|
|
22
|
+
) -> tuple[str, ...]:
|
|
23
|
+
"""Return exact current body and curation paths for affected artifacts."""
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class LocalGitRepository:
|
|
28
|
+
"""Use one local working repository as self-host Plane-K truth."""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
*,
|
|
33
|
+
repository: Path,
|
|
34
|
+
path_catalog: KPathCatalog,
|
|
35
|
+
author_name: str,
|
|
36
|
+
author_email: str,
|
|
37
|
+
) -> None:
|
|
38
|
+
"""Bind an existing clean repository and explicit commit identity."""
|
|
39
|
+
self._repository = repository.resolve(strict=True)
|
|
40
|
+
self._path_catalog = path_catalog
|
|
41
|
+
self._author_name = author_name
|
|
42
|
+
self._author_email = author_email
|
|
43
|
+
if (
|
|
44
|
+
_output(
|
|
45
|
+
arguments=(
|
|
46
|
+
"git",
|
|
47
|
+
"-C",
|
|
48
|
+
str(self._repository),
|
|
49
|
+
"rev-parse",
|
|
50
|
+
"--is-bare-repository",
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
== "true"
|
|
54
|
+
):
|
|
55
|
+
raise ValueError("self-host Plane-K truth must be a working repository")
|
|
56
|
+
|
|
57
|
+
def checkout(self, *, destination: Path) -> KRevision:
|
|
58
|
+
"""Clone the local truth repository into one driver-owned worktree."""
|
|
59
|
+
_run(
|
|
60
|
+
arguments=(
|
|
61
|
+
"git",
|
|
62
|
+
"clone",
|
|
63
|
+
"--quiet",
|
|
64
|
+
str(self._repository),
|
|
65
|
+
str(destination),
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
return KRevision(
|
|
69
|
+
root=_output(arguments=("git", "-C", str(destination), "rev-parse", "HEAD"))
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def publish(self, *, worktree: Path) -> KRevision:
|
|
73
|
+
"""Commit a prepared driver tree and push its current branch to local truth."""
|
|
74
|
+
_run(arguments=("git", "-C", str(worktree), "add", "-A"))
|
|
75
|
+
if (
|
|
76
|
+
_status(
|
|
77
|
+
arguments=("git", "-C", str(worktree), "diff", "--cached", "--quiet")
|
|
78
|
+
)
|
|
79
|
+
!= 0
|
|
80
|
+
):
|
|
81
|
+
_run(
|
|
82
|
+
arguments=(
|
|
83
|
+
"git",
|
|
84
|
+
"-C",
|
|
85
|
+
str(worktree),
|
|
86
|
+
"-c",
|
|
87
|
+
f"user.name={self._author_name}",
|
|
88
|
+
"-c",
|
|
89
|
+
f"user.email={self._author_email}",
|
|
90
|
+
"commit",
|
|
91
|
+
"--quiet",
|
|
92
|
+
"-m",
|
|
93
|
+
"rememberstack knowledge update",
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
_run(
|
|
97
|
+
arguments=(
|
|
98
|
+
"git",
|
|
99
|
+
"-C",
|
|
100
|
+
str(self._repository),
|
|
101
|
+
"fetch",
|
|
102
|
+
"--quiet",
|
|
103
|
+
str(worktree),
|
|
104
|
+
"HEAD:refs/rememberstack/publish",
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
_run(
|
|
108
|
+
arguments=(
|
|
109
|
+
"git",
|
|
110
|
+
"-C",
|
|
111
|
+
str(self._repository),
|
|
112
|
+
"reset",
|
|
113
|
+
"--hard",
|
|
114
|
+
"refs/rememberstack/publish",
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
_run(
|
|
118
|
+
arguments=(
|
|
119
|
+
"git",
|
|
120
|
+
"-C",
|
|
121
|
+
str(self._repository),
|
|
122
|
+
"update-ref",
|
|
123
|
+
"-d",
|
|
124
|
+
"refs/rememberstack/publish",
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
return KRevision(
|
|
128
|
+
root=_output(arguments=("git", "-C", str(worktree), "rev-parse", "HEAD"))
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def blocking_redaction_paths(
|
|
132
|
+
self, *, deployment_id: UUID, doc_id: UUID
|
|
133
|
+
) -> tuple[str, ...]:
|
|
134
|
+
"""Delegate to current synced K ownership/citation state."""
|
|
135
|
+
return self._path_catalog.blocking_k_paths(
|
|
136
|
+
deployment_id=deployment_id, doc_id=doc_id
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def purge_artifacts(
|
|
140
|
+
self, *, deployment_id: UUID, forget_id: UUID, artifact_ids: tuple[UUID, ...]
|
|
141
|
+
) -> None:
|
|
142
|
+
"""Erase affected paths from every ref and re-add sanitized current files."""
|
|
143
|
+
current_paths = self._validated_paths(
|
|
144
|
+
paths=self._path_catalog.k_paths_for_artifacts(
|
|
145
|
+
deployment_id=deployment_id, artifact_ids=artifact_ids
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
if not current_paths or self._honored(forget_id=forget_id, paths=current_paths):
|
|
149
|
+
return
|
|
150
|
+
paths = self._historical_paths(current_paths=current_paths)
|
|
151
|
+
if _output(
|
|
152
|
+
arguments=("git", "-C", str(self._repository), "status", "--porcelain")
|
|
153
|
+
):
|
|
154
|
+
raise RuntimeError(
|
|
155
|
+
"Plane-K repository must be clean before history erasure"
|
|
156
|
+
)
|
|
157
|
+
current = {
|
|
158
|
+
path: target.read_bytes() if target.is_file() else None
|
|
159
|
+
for path in current_paths
|
|
160
|
+
for target in (self._repository / path,)
|
|
161
|
+
}
|
|
162
|
+
removal = "git rm -r --cached --ignore-unmatch -- " + " ".join(
|
|
163
|
+
shlex.quote(path) for path in paths
|
|
164
|
+
)
|
|
165
|
+
_run(
|
|
166
|
+
arguments=(
|
|
167
|
+
"git",
|
|
168
|
+
"-C",
|
|
169
|
+
str(self._repository),
|
|
170
|
+
"filter-branch",
|
|
171
|
+
"--force",
|
|
172
|
+
"--index-filter",
|
|
173
|
+
removal,
|
|
174
|
+
"--tag-name-filter",
|
|
175
|
+
"cat",
|
|
176
|
+
"--",
|
|
177
|
+
"--all",
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
self._drop_original_refs()
|
|
181
|
+
for path, content in current.items():
|
|
182
|
+
target = self._repository / path
|
|
183
|
+
if content is None:
|
|
184
|
+
target.unlink(missing_ok=True)
|
|
185
|
+
continue
|
|
186
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
187
|
+
target.write_bytes(content)
|
|
188
|
+
_run(
|
|
189
|
+
arguments=(
|
|
190
|
+
"git",
|
|
191
|
+
"-C",
|
|
192
|
+
str(self._repository),
|
|
193
|
+
"add",
|
|
194
|
+
"-A",
|
|
195
|
+
"--",
|
|
196
|
+
*current_paths,
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
if (
|
|
200
|
+
_status(
|
|
201
|
+
arguments=(
|
|
202
|
+
"git",
|
|
203
|
+
"-C",
|
|
204
|
+
str(self._repository),
|
|
205
|
+
"diff",
|
|
206
|
+
"--cached",
|
|
207
|
+
"--quiet",
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
!= 0
|
|
211
|
+
):
|
|
212
|
+
_run(
|
|
213
|
+
arguments=(
|
|
214
|
+
"git",
|
|
215
|
+
"-C",
|
|
216
|
+
str(self._repository),
|
|
217
|
+
"-c",
|
|
218
|
+
f"user.name={self._author_name}",
|
|
219
|
+
"-c",
|
|
220
|
+
f"user.email={self._author_email}",
|
|
221
|
+
"commit",
|
|
222
|
+
"--quiet",
|
|
223
|
+
"-m",
|
|
224
|
+
f"hard-forget {forget_id}",
|
|
225
|
+
)
|
|
226
|
+
)
|
|
227
|
+
head = _output(
|
|
228
|
+
arguments=("git", "-C", str(self._repository), "rev-parse", "HEAD")
|
|
229
|
+
)
|
|
230
|
+
_run(
|
|
231
|
+
arguments=(
|
|
232
|
+
"git",
|
|
233
|
+
"-C",
|
|
234
|
+
str(self._repository),
|
|
235
|
+
"update-ref",
|
|
236
|
+
self._ack_ref(forget_id=forget_id),
|
|
237
|
+
head,
|
|
238
|
+
)
|
|
239
|
+
)
|
|
240
|
+
_run(
|
|
241
|
+
arguments=(
|
|
242
|
+
"git",
|
|
243
|
+
"-C",
|
|
244
|
+
str(self._repository),
|
|
245
|
+
"reflog",
|
|
246
|
+
"expire",
|
|
247
|
+
"--expire=now",
|
|
248
|
+
"--all",
|
|
249
|
+
)
|
|
250
|
+
)
|
|
251
|
+
_run(arguments=("git", "-C", str(self._repository), "gc", "--prune=now"))
|
|
252
|
+
|
|
253
|
+
def verify_artifacts_purged(
|
|
254
|
+
self, *, deployment_id: UUID, forget_id: UUID, artifact_ids: tuple[UUID, ...]
|
|
255
|
+
) -> None:
|
|
256
|
+
"""Prove affected current paths have only their sanitized post-purge history."""
|
|
257
|
+
paths = self._validated_paths(
|
|
258
|
+
paths=self._path_catalog.k_paths_for_artifacts(
|
|
259
|
+
deployment_id=deployment_id, artifact_ids=artifact_ids
|
|
260
|
+
)
|
|
261
|
+
)
|
|
262
|
+
if paths and not self._honored(forget_id=forget_id, paths=paths):
|
|
263
|
+
raise RuntimeError(
|
|
264
|
+
f"Plane-K purge verification failed for forget_id {forget_id}"
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
def _honored(self, *, forget_id: UUID, paths: tuple[str, ...]) -> bool:
|
|
268
|
+
"""Validate the store-local acknowledgement and single current path history."""
|
|
269
|
+
if (
|
|
270
|
+
_status(
|
|
271
|
+
arguments=(
|
|
272
|
+
"git",
|
|
273
|
+
"-C",
|
|
274
|
+
str(self._repository),
|
|
275
|
+
"show-ref",
|
|
276
|
+
"--verify",
|
|
277
|
+
"--quiet",
|
|
278
|
+
self._ack_ref(forget_id=forget_id),
|
|
279
|
+
)
|
|
280
|
+
)
|
|
281
|
+
!= 0
|
|
282
|
+
):
|
|
283
|
+
return False
|
|
284
|
+
commits = _output(
|
|
285
|
+
arguments=(
|
|
286
|
+
"git",
|
|
287
|
+
"-C",
|
|
288
|
+
str(self._repository),
|
|
289
|
+
"log",
|
|
290
|
+
"--all",
|
|
291
|
+
"--format=%H",
|
|
292
|
+
"--",
|
|
293
|
+
*paths,
|
|
294
|
+
)
|
|
295
|
+
).splitlines()
|
|
296
|
+
return len(set(commits)) <= 1
|
|
297
|
+
|
|
298
|
+
def _validated_paths(self, *, paths: tuple[str, ...]) -> tuple[str, ...]:
|
|
299
|
+
"""Reject absolute/traversing Git paths before filesystem or shell use."""
|
|
300
|
+
result: list[str] = []
|
|
301
|
+
for value in sorted(set(paths)):
|
|
302
|
+
path = PurePosixPath(value)
|
|
303
|
+
if not value or path.is_absolute() or ".." in path.parts:
|
|
304
|
+
raise ValueError(f"unsafe Plane-K path {value!r}")
|
|
305
|
+
candidate = (self._repository / path).resolve()
|
|
306
|
+
if not candidate.is_relative_to(self._repository):
|
|
307
|
+
raise ValueError(f"Plane-K path {value!r} escapes the repository")
|
|
308
|
+
result.append(path.as_posix())
|
|
309
|
+
return tuple(result)
|
|
310
|
+
|
|
311
|
+
def _historical_paths(self, *, current_paths: tuple[str, ...]) -> tuple[str, ...]:
|
|
312
|
+
"""Follow renames and return every historical name of affected files."""
|
|
313
|
+
paths = set(current_paths)
|
|
314
|
+
for current in current_paths:
|
|
315
|
+
output = _output(
|
|
316
|
+
arguments=(
|
|
317
|
+
"git",
|
|
318
|
+
"-C",
|
|
319
|
+
str(self._repository),
|
|
320
|
+
"log",
|
|
321
|
+
"--all",
|
|
322
|
+
"--follow",
|
|
323
|
+
"--name-status",
|
|
324
|
+
"--format=",
|
|
325
|
+
"--",
|
|
326
|
+
current,
|
|
327
|
+
)
|
|
328
|
+
)
|
|
329
|
+
for line in output.splitlines():
|
|
330
|
+
fields = line.split("\t")
|
|
331
|
+
if len(fields) >= 2:
|
|
332
|
+
paths.update(fields[1:])
|
|
333
|
+
return self._validated_paths(paths=tuple(paths))
|
|
334
|
+
|
|
335
|
+
def _drop_original_refs(self) -> None:
|
|
336
|
+
"""Delete filter-branch backup refs so forgotten history is unreachable."""
|
|
337
|
+
refs = _output(
|
|
338
|
+
arguments=(
|
|
339
|
+
"git",
|
|
340
|
+
"-C",
|
|
341
|
+
str(self._repository),
|
|
342
|
+
"for-each-ref",
|
|
343
|
+
"--format=%(refname)",
|
|
344
|
+
"refs/original/",
|
|
345
|
+
)
|
|
346
|
+
).splitlines()
|
|
347
|
+
for ref in refs:
|
|
348
|
+
_run(
|
|
349
|
+
arguments=("git", "-C", str(self._repository), "update-ref", "-d", ref)
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
@staticmethod
|
|
353
|
+
def _ack_ref(*, forget_id: UUID) -> str:
|
|
354
|
+
"""Return the receipt ref that disappears with an independently restored repo."""
|
|
355
|
+
return f"refs/rememberstack/forget/{forget_id}"
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _run(*, arguments: tuple[str, ...]) -> None:
|
|
359
|
+
"""Run one Git command and preserve its full failing process exception."""
|
|
360
|
+
subprocess.run(arguments, check=True, capture_output=True, text=True)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _output(*, arguments: tuple[str, ...]) -> str:
|
|
364
|
+
"""Run one Git read and return its exact trimmed stdout."""
|
|
365
|
+
return subprocess.run(
|
|
366
|
+
arguments, check=True, capture_output=True, text=True
|
|
367
|
+
).stdout.strip()
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _status(*, arguments: tuple[str, ...]) -> int:
|
|
371
|
+
"""Run a Git predicate whose nonzero status is an expected false result."""
|
|
372
|
+
return subprocess.run(
|
|
373
|
+
arguments, check=False, capture_output=True, text=True
|
|
374
|
+
).returncode
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"""The embedded-LanceDB P1 chunk index: one table of text + vectors (D8)."""
|
|
2
|
+
|
|
3
|
+
from datetime import timedelta
|
|
4
|
+
import math
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import cast
|
|
7
|
+
from typing import Final
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
|
|
10
|
+
import lancedb
|
|
11
|
+
from lancedb.index import Bitmap
|
|
12
|
+
from lancedb.index import BTree
|
|
13
|
+
from lancedb.index import IvfFlat
|
|
14
|
+
from lancedb.query import LanceVectorQueryBuilder
|
|
15
|
+
from lancedb.table import Table
|
|
16
|
+
|
|
17
|
+
from rememberstack.model import P1ChunkRow
|
|
18
|
+
from rememberstack.model import P1ClaimRow
|
|
19
|
+
from rememberstack.model import P1EntityRow
|
|
20
|
+
from rememberstack.model import P1FactRow
|
|
21
|
+
|
|
22
|
+
_CHUNK_TABLE = "chunks"
|
|
23
|
+
_CLAIM_TABLE = "claims"
|
|
24
|
+
_FACT_TABLE = "facts"
|
|
25
|
+
_ENTITY_TABLE = "entities"
|
|
26
|
+
|
|
27
|
+
LANCE_TARGET_PARTITION_ROWS: Final = 8_192
|
|
28
|
+
"""WP-5.6 IVF_FLAT target: one vector partition per roughly 8k rows."""
|
|
29
|
+
|
|
30
|
+
LANCE_NPROBES: Final = 20
|
|
31
|
+
"""WP-5.6 query probe count for filtered ANN reads."""
|
|
32
|
+
|
|
33
|
+
_MIN_VECTOR_INDEX_ROWS: Final = 256
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class LanceChunkIndex:
|
|
37
|
+
"""The self-host P1 chunk table in an embedded Lance dataset directory."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, *, root: Path) -> None:
|
|
40
|
+
"""Bind the index to its dataset directory, creating it if absent."""
|
|
41
|
+
self._connection = lancedb.connect(str(root))
|
|
42
|
+
|
|
43
|
+
def upsert_chunks(self, *, rows: tuple[P1ChunkRow, ...]) -> None:
|
|
44
|
+
"""Insert or replace rows by chunk_id; re-runs are idempotent."""
|
|
45
|
+
if not rows:
|
|
46
|
+
return
|
|
47
|
+
payload = [
|
|
48
|
+
{
|
|
49
|
+
"chunk_id": str(row.chunk_id),
|
|
50
|
+
"deployment_id": str(row.deployment_id),
|
|
51
|
+
"doc_id": str(row.doc_id),
|
|
52
|
+
"version_id": str(row.version_id),
|
|
53
|
+
"section_role": row.section_role,
|
|
54
|
+
"text": row.text,
|
|
55
|
+
"vector": list(row.vector),
|
|
56
|
+
}
|
|
57
|
+
for row in rows
|
|
58
|
+
]
|
|
59
|
+
self._upsert(table=_CHUNK_TABLE, key="chunk_id", payload=payload)
|
|
60
|
+
|
|
61
|
+
def chunk_vectors(
|
|
62
|
+
self, *, deployment_id: str, chunk_ids: tuple[str, ...]
|
|
63
|
+
) -> dict[str, tuple[float, ...]]:
|
|
64
|
+
"""Stored vectors for the requested ids (absent ids are omitted)."""
|
|
65
|
+
deployment_id = str(UUID(deployment_id))
|
|
66
|
+
if not chunk_ids or _CHUNK_TABLE not in self._connection.table_names():
|
|
67
|
+
return {}
|
|
68
|
+
ids = ", ".join(f"'{UUID(item)}'" for item in chunk_ids)
|
|
69
|
+
rows = (
|
|
70
|
+
self._connection.open_table(_CHUNK_TABLE)
|
|
71
|
+
.search()
|
|
72
|
+
.where(f"deployment_id = '{deployment_id}' AND chunk_id IN ({ids})")
|
|
73
|
+
.limit(len(chunk_ids))
|
|
74
|
+
.to_list()
|
|
75
|
+
)
|
|
76
|
+
return {row["chunk_id"]: tuple(row["vector"]) for row in rows}
|
|
77
|
+
|
|
78
|
+
def upsert_claims(self, *, rows: tuple[P1ClaimRow, ...]) -> None:
|
|
79
|
+
"""Insert or replace claims-channel rows by claim_id; idempotent."""
|
|
80
|
+
self._upsert(
|
|
81
|
+
table=_CLAIM_TABLE,
|
|
82
|
+
key="claim_id",
|
|
83
|
+
payload=[
|
|
84
|
+
{
|
|
85
|
+
"claim_id": str(row.claim_id),
|
|
86
|
+
"deployment_id": str(row.deployment_id),
|
|
87
|
+
"doc_id": str(row.doc_id),
|
|
88
|
+
"chunk_id": str(row.chunk_id),
|
|
89
|
+
"text": row.text,
|
|
90
|
+
"is_current_testimony": row.is_current_testimony,
|
|
91
|
+
"is_attributed": row.is_attributed,
|
|
92
|
+
"vector": list(row.vector),
|
|
93
|
+
}
|
|
94
|
+
for row in rows
|
|
95
|
+
],
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def upsert_facts(self, *, rows: tuple[P1FactRow, ...]) -> None:
|
|
99
|
+
"""Insert or replace facts-channel rows by fact_id; idempotent."""
|
|
100
|
+
self._upsert(
|
|
101
|
+
table=_FACT_TABLE,
|
|
102
|
+
key="fact_id",
|
|
103
|
+
payload=[
|
|
104
|
+
{
|
|
105
|
+
"fact_id": str(row.fact_id),
|
|
106
|
+
"deployment_id": str(row.deployment_id),
|
|
107
|
+
"kind": row.kind,
|
|
108
|
+
"label": row.label,
|
|
109
|
+
"status": row.status,
|
|
110
|
+
"vector": list(row.vector),
|
|
111
|
+
}
|
|
112
|
+
for row in rows
|
|
113
|
+
],
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def search_claims(
|
|
117
|
+
self,
|
|
118
|
+
*,
|
|
119
|
+
deployment_id: str,
|
|
120
|
+
vector: tuple[float, ...],
|
|
121
|
+
k: int,
|
|
122
|
+
current_only: bool,
|
|
123
|
+
) -> tuple[str, ...]:
|
|
124
|
+
"""Nominate claim ids by vector similarity (D48: nomination, not truth).
|
|
125
|
+
|
|
126
|
+
The DEFAULT claims channel filters to current testimony via the
|
|
127
|
+
stored scalar (retrieval §5); hydration against the spine confirms.
|
|
128
|
+
"""
|
|
129
|
+
deployment_id = str(UUID(deployment_id)) # refuse filter injection
|
|
130
|
+
if _CLAIM_TABLE not in self._connection.table_names():
|
|
131
|
+
return ()
|
|
132
|
+
query = (
|
|
133
|
+
cast(
|
|
134
|
+
"LanceVectorQueryBuilder",
|
|
135
|
+
self._connection.open_table(_CLAIM_TABLE)
|
|
136
|
+
.search(list(vector))
|
|
137
|
+
.where(
|
|
138
|
+
f"deployment_id = '{deployment_id}'"
|
|
139
|
+
+ (" AND is_current_testimony" if current_only else ""),
|
|
140
|
+
prefilter=True,
|
|
141
|
+
),
|
|
142
|
+
)
|
|
143
|
+
.nprobes(LANCE_NPROBES)
|
|
144
|
+
.limit(k)
|
|
145
|
+
)
|
|
146
|
+
return tuple(row["claim_id"] for row in query.to_list())
|
|
147
|
+
|
|
148
|
+
def search_facts(
|
|
149
|
+
self, *, deployment_id: str, vector: tuple[float, ...], k: int, kind: str | None
|
|
150
|
+
) -> tuple[str, ...]:
|
|
151
|
+
"""Nominate fact ids (relations/observations) by label similarity."""
|
|
152
|
+
deployment_id = str(UUID(deployment_id)) # refuse filter injection
|
|
153
|
+
if kind is not None and kind not in ("relation", "observation"):
|
|
154
|
+
raise ValueError(f"unknown facts-channel kind {kind!r}")
|
|
155
|
+
if _FACT_TABLE not in self._connection.table_names():
|
|
156
|
+
return ()
|
|
157
|
+
where = f"deployment_id = '{deployment_id}'"
|
|
158
|
+
if kind is not None:
|
|
159
|
+
where += f" AND kind = '{kind}'"
|
|
160
|
+
query = (
|
|
161
|
+
cast(
|
|
162
|
+
"LanceVectorQueryBuilder",
|
|
163
|
+
self._connection.open_table(_FACT_TABLE)
|
|
164
|
+
.search(list(vector))
|
|
165
|
+
.where(where, prefilter=True),
|
|
166
|
+
)
|
|
167
|
+
.nprobes(LANCE_NPROBES)
|
|
168
|
+
.limit(k)
|
|
169
|
+
)
|
|
170
|
+
return tuple(row["fact_id"] for row in query.to_list())
|
|
171
|
+
|
|
172
|
+
def build_search_indexes(self) -> None:
|
|
173
|
+
"""Build the measured scalar + IVF_FLAT indexes after a bulk load.
|
|
174
|
+
|
|
175
|
+
This is explicit rather than hidden in every upsert: index construction
|
|
176
|
+
is a maintenance/backfill operation, while inline P1 writes must stay
|
|
177
|
+
cheap. Lance still searches unindexed tail fragments after the build.
|
|
178
|
+
"""
|
|
179
|
+
available = set(self._connection.list_tables().tables or ())
|
|
180
|
+
if _CLAIM_TABLE in available:
|
|
181
|
+
claims = self._connection.open_table(_CLAIM_TABLE)
|
|
182
|
+
claims.create_index("deployment_id", config=BTree())
|
|
183
|
+
claims.create_index("is_current_testimony", config=Bitmap())
|
|
184
|
+
self._build_vector_index(table=claims)
|
|
185
|
+
if _FACT_TABLE in available:
|
|
186
|
+
facts = self._connection.open_table(_FACT_TABLE)
|
|
187
|
+
facts.create_index("deployment_id", config=BTree())
|
|
188
|
+
facts.create_index("kind", config=Bitmap())
|
|
189
|
+
self._build_vector_index(table=facts)
|
|
190
|
+
|
|
191
|
+
@staticmethod
|
|
192
|
+
def _build_vector_index(*, table: Table) -> None:
|
|
193
|
+
"""Build one vector index when the table is large enough to train it."""
|
|
194
|
+
rows = table.count_rows()
|
|
195
|
+
if rows < _MIN_VECTOR_INDEX_ROWS:
|
|
196
|
+
return
|
|
197
|
+
table.create_index(
|
|
198
|
+
"vector",
|
|
199
|
+
config=IvfFlat(
|
|
200
|
+
distance_type="l2",
|
|
201
|
+
num_partitions=max(1, math.ceil(rows / LANCE_TARGET_PARTITION_ROWS)),
|
|
202
|
+
target_partition_size=LANCE_TARGET_PARTITION_ROWS,
|
|
203
|
+
),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
def upsert_entities(self, *, rows: tuple[P1EntityRow, ...]) -> None:
|
|
207
|
+
"""Insert or replace entity-profile rows by entity_id; idempotent."""
|
|
208
|
+
self._upsert(
|
|
209
|
+
table=_ENTITY_TABLE,
|
|
210
|
+
key="entity_id",
|
|
211
|
+
payload=[
|
|
212
|
+
{
|
|
213
|
+
"entity_id": str(row.entity_id),
|
|
214
|
+
"deployment_id": str(row.deployment_id),
|
|
215
|
+
"type": row.type,
|
|
216
|
+
"canonical_name": row.canonical_name,
|
|
217
|
+
"vector": list(row.vector),
|
|
218
|
+
}
|
|
219
|
+
for row in rows
|
|
220
|
+
],
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
def entity_vectors(
|
|
224
|
+
self, *, deployment_id: str, entity_ids: tuple[str, ...]
|
|
225
|
+
) -> dict[str, tuple[float, ...]]:
|
|
226
|
+
"""Profile vectors for the requested ids (absent ids are omitted)."""
|
|
227
|
+
deployment_id = str(UUID(deployment_id))
|
|
228
|
+
if not entity_ids or _ENTITY_TABLE not in self._connection.table_names():
|
|
229
|
+
return {}
|
|
230
|
+
ids = ", ".join(f"'{UUID(item)}'" for item in entity_ids)
|
|
231
|
+
rows = (
|
|
232
|
+
self._connection.open_table(_ENTITY_TABLE)
|
|
233
|
+
.search()
|
|
234
|
+
.where(f"deployment_id = '{deployment_id}' AND entity_id IN ({ids})")
|
|
235
|
+
.limit(len(entity_ids))
|
|
236
|
+
.to_list()
|
|
237
|
+
)
|
|
238
|
+
return {row["entity_id"]: tuple(row["vector"]) for row in rows}
|
|
239
|
+
|
|
240
|
+
def purge_rows(
|
|
241
|
+
self,
|
|
242
|
+
*,
|
|
243
|
+
deployment_id: UUID,
|
|
244
|
+
chunk_ids: tuple[UUID, ...],
|
|
245
|
+
claim_ids: tuple[UUID, ...],
|
|
246
|
+
fact_ids: tuple[UUID, ...],
|
|
247
|
+
entity_ids: tuple[UUID, ...],
|
|
248
|
+
) -> None:
|
|
249
|
+
"""Delete exact deployment-owned rows and prune obsolete Lance versions."""
|
|
250
|
+
for table, key, ids in (
|
|
251
|
+
(_CHUNK_TABLE, "chunk_id", chunk_ids),
|
|
252
|
+
(_CLAIM_TABLE, "claim_id", claim_ids),
|
|
253
|
+
(_FACT_TABLE, "fact_id", fact_ids),
|
|
254
|
+
(_ENTITY_TABLE, "entity_id", entity_ids),
|
|
255
|
+
):
|
|
256
|
+
self._purge_table_rows(
|
|
257
|
+
table=table, key=key, deployment_id=deployment_id, ids=ids
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
def verify_rows_purged(
|
|
261
|
+
self,
|
|
262
|
+
*,
|
|
263
|
+
deployment_id: UUID,
|
|
264
|
+
chunk_ids: tuple[UUID, ...],
|
|
265
|
+
claim_ids: tuple[UUID, ...],
|
|
266
|
+
fact_ids: tuple[UUID, ...],
|
|
267
|
+
entity_ids: tuple[UUID, ...],
|
|
268
|
+
) -> None:
|
|
269
|
+
"""Prove no nominated UUID remains in its deployment-scoped P1 table."""
|
|
270
|
+
remaining: dict[str, int] = {}
|
|
271
|
+
for table, key, ids in (
|
|
272
|
+
(_CHUNK_TABLE, "chunk_id", chunk_ids),
|
|
273
|
+
(_CLAIM_TABLE, "claim_id", claim_ids),
|
|
274
|
+
(_FACT_TABLE, "fact_id", fact_ids),
|
|
275
|
+
(_ENTITY_TABLE, "entity_id", entity_ids),
|
|
276
|
+
):
|
|
277
|
+
if not ids or table not in self._connection.table_names():
|
|
278
|
+
continue
|
|
279
|
+
rendered_ids = ", ".join(f"'{item}'" for item in ids)
|
|
280
|
+
count = self._connection.open_table(table).count_rows(
|
|
281
|
+
f"deployment_id = '{deployment_id}' AND {key} IN ({rendered_ids})"
|
|
282
|
+
)
|
|
283
|
+
if count:
|
|
284
|
+
remaining[table] = count
|
|
285
|
+
if remaining:
|
|
286
|
+
raise RuntimeError(f"P1 purge verification found rows: {remaining!r}")
|
|
287
|
+
|
|
288
|
+
def table_count(self, *, table: str) -> int:
|
|
289
|
+
"""Total rows in one P1 table (0 before its first write)."""
|
|
290
|
+
if table not in self._connection.table_names():
|
|
291
|
+
return 0
|
|
292
|
+
return self._connection.open_table(table).count_rows()
|
|
293
|
+
|
|
294
|
+
def _upsert(
|
|
295
|
+
self, *, table: str, key: str, payload: list[dict[str, object]]
|
|
296
|
+
) -> None:
|
|
297
|
+
"""Create-or-merge one table's rows by its key column."""
|
|
298
|
+
if not payload:
|
|
299
|
+
return
|
|
300
|
+
if table not in self._connection.table_names():
|
|
301
|
+
self._connection.create_table(table, data=payload)
|
|
302
|
+
return
|
|
303
|
+
(
|
|
304
|
+
self._connection.open_table(table)
|
|
305
|
+
.merge_insert(key)
|
|
306
|
+
.when_matched_update_all()
|
|
307
|
+
.when_not_matched_insert_all()
|
|
308
|
+
.execute(payload)
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
def _purge_table_rows(
|
|
312
|
+
self, *, table: str, key: str, deployment_id: UUID, ids: tuple[UUID, ...]
|
|
313
|
+
) -> None:
|
|
314
|
+
"""Delete one exact UUID set and physically prune its obsolete versions."""
|
|
315
|
+
if not ids or table not in self._connection.table_names():
|
|
316
|
+
return
|
|
317
|
+
rendered_ids = ", ".join(f"'{item}'" for item in ids)
|
|
318
|
+
lance_table = self._connection.open_table(table)
|
|
319
|
+
lance_table.delete(
|
|
320
|
+
f"deployment_id = '{deployment_id}' AND {key} IN ({rendered_ids})"
|
|
321
|
+
)
|
|
322
|
+
lance_table.optimize(cleanup_older_than=timedelta(0), delete_unverified=True)
|
|
323
|
+
|
|
324
|
+
def row_count(self) -> int:
|
|
325
|
+
"""Total rows in the chunk table (0 before the first write)."""
|
|
326
|
+
if _CHUNK_TABLE not in self._connection.table_names():
|
|
327
|
+
return 0
|
|
328
|
+
return self._connection.open_table(_CHUNK_TABLE).count_rows()
|