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.
Files changed (186) hide show
  1. rememberstack/__init__.py +9 -0
  2. rememberstack/adapters/__init__.py +42 -0
  3. rememberstack/adapters/codex_writer.py +221 -0
  4. rememberstack/adapters/markitdown_converter.py +42 -0
  5. rememberstack/adapters/openrouter.py +136 -0
  6. rememberstack/adapters/selfhost/__init__.py +54 -0
  7. rememberstack/adapters/selfhost/forget.py +66 -0
  8. rememberstack/adapters/selfhost/git.py +374 -0
  9. rememberstack/adapters/selfhost/lance.py +328 -0
  10. rememberstack/adapters/selfhost/minio.py +279 -0
  11. rememberstack/adapters/selfhost/mounts.py +249 -0
  12. rememberstack/adapters/selfhost/object_store.py +130 -0
  13. rememberstack/adapters/selfhost/projection.py +80 -0
  14. rememberstack/adapters/selfhost/queue.py +137 -0
  15. rememberstack/adapters/selfhost/telemetry.py +45 -0
  16. rememberstack/adapters/selfhost/watcher.py +70 -0
  17. rememberstack/adapters/testing/__init__.py +15 -0
  18. rememberstack/adapters/testing/cost_meter.py +13 -0
  19. rememberstack/adapters/testing/model_provider.py +83 -0
  20. rememberstack/adapters/testing/queue.py +43 -0
  21. rememberstack/adapters/testing/telemetry.py +22 -0
  22. rememberstack/client.py +19 -0
  23. rememberstack/core/__init__.py +127 -0
  24. rememberstack/core/blockizer.py +189 -0
  25. rememberstack/core/chunker.py +216 -0
  26. rememberstack/core/consumption_skill.py +275 -0
  27. rememberstack/core/conversion.py +76 -0
  28. rememberstack/core/core_manifest.py +598 -0
  29. rememberstack/core/extension_packs.py +124 -0
  30. rememberstack/core/forget.py +17 -0
  31. rememberstack/core/knowledge_authored.py +276 -0
  32. rememberstack/core/knowledge_compile.py +215 -0
  33. rememberstack/core/knowledge_fact_sheet.py +210 -0
  34. rememberstack/core/knowledge_hashing.py +68 -0
  35. rememberstack/core/knowledge_planner.py +64 -0
  36. rememberstack/core/knowledge_writer.py +175 -0
  37. rememberstack/core/ranking.py +200 -0
  38. rememberstack/core/recipe_linter.py +149 -0
  39. rememberstack/core/section_snap.py +209 -0
  40. rememberstack/core/storage_routing.py +27 -0
  41. rememberstack/eval/__init__.py +53 -0
  42. rememberstack/eval/consumption.py +141 -0
  43. rememberstack/eval/contradiction.py +184 -0
  44. rememberstack/eval/harness.py +136 -0
  45. rememberstack/eval/lifecycle.py +400 -0
  46. rememberstack/eval/operational_scale.py +49 -0
  47. rememberstack/eval/resolution.py +255 -0
  48. rememberstack/eval/retrieval_spikes.py +50 -0
  49. rememberstack/eval/skeleton.py +231 -0
  50. rememberstack/llm/__init__.py +1 -0
  51. rememberstack/model/__init__.py +589 -0
  52. rememberstack/model/adjudication.py +100 -0
  53. rememberstack/model/auth.py +27 -0
  54. rememberstack/model/blocks.py +30 -0
  55. rememberstack/model/chunks.py +190 -0
  56. rememberstack/model/claims.py +162 -0
  57. rememberstack/model/client.py +98 -0
  58. rememberstack/model/clustering.py +54 -0
  59. rememberstack/model/component_version.py +124 -0
  60. rememberstack/model/consumption.py +88 -0
  61. rememberstack/model/conversion.py +31 -0
  62. rememberstack/model/deployment.py +53 -0
  63. rememberstack/model/documents.py +168 -0
  64. rememberstack/model/envelope.py +513 -0
  65. rememberstack/model/evaluation.py +72 -0
  66. rememberstack/model/forget.py +143 -0
  67. rememberstack/model/git.py +13 -0
  68. rememberstack/model/knowledge.py +840 -0
  69. rememberstack/model/knowledge_authored.py +325 -0
  70. rememberstack/model/knowledge_planner.py +431 -0
  71. rememberstack/model/lifecycle.py +42 -0
  72. rememberstack/model/model_provider.py +78 -0
  73. rememberstack/model/mounts.py +24 -0
  74. rememberstack/model/object_store.py +21 -0
  75. rememberstack/model/operational_scale.py +59 -0
  76. rememberstack/model/operations.py +153 -0
  77. rememberstack/model/processing.py +228 -0
  78. rememberstack/model/queue.py +73 -0
  79. rememberstack/model/recipes.py +83 -0
  80. rememberstack/model/relations.py +79 -0
  81. rememberstack/model/resolution.py +83 -0
  82. rememberstack/model/retrieval_spikes.py +62 -0
  83. rememberstack/model/sections.py +120 -0
  84. rememberstack/model/telemetry.py +30 -0
  85. rememberstack/ports/__init__.py +29 -0
  86. rememberstack/ports/auth.py +16 -0
  87. rememberstack/ports/connector.py +23 -0
  88. rememberstack/ports/cost_meter.py +17 -0
  89. rememberstack/ports/forget.py +20 -0
  90. rememberstack/ports/git.py +20 -0
  91. rememberstack/ports/model_provider.py +28 -0
  92. rememberstack/ports/mounts.py +16 -0
  93. rememberstack/ports/object_store.py +27 -0
  94. rememberstack/ports/p1_index.py +92 -0
  95. rememberstack/ports/purge.py +93 -0
  96. rememberstack/ports/queue.py +23 -0
  97. rememberstack/ports/telemetry.py +21 -0
  98. rememberstack/profiles/__init__.py +22 -0
  99. rememberstack/profiles/selfhost.py +324 -0
  100. rememberstack/profiles/selfhost_forget.py +158 -0
  101. rememberstack/profiles/selfhost_operations.py +95 -0
  102. rememberstack/py.typed +1 -0
  103. rememberstack/spine/__init__.py +93 -0
  104. rememberstack/spine/admission.py +26 -0
  105. rememberstack/spine/backfill.py +168 -0
  106. rememberstack/spine/catalog_contract.py +742 -0
  107. rememberstack/spine/chunk_catalog.py +237 -0
  108. rememberstack/spine/claim_catalog.py +298 -0
  109. rememberstack/spine/clustering.py +740 -0
  110. rememberstack/spine/component_versions.py +208 -0
  111. rememberstack/spine/consumption.py +81 -0
  112. rememberstack/spine/deployment_bootstrap.py +445 -0
  113. rememberstack/spine/document_catalog.py +621 -0
  114. rememberstack/spine/entity_registry.py +205 -0
  115. rememberstack/spine/extension_packs.py +220 -0
  116. rememberstack/spine/fact_catalog.py +571 -0
  117. rememberstack/spine/forget.py +1753 -0
  118. rememberstack/spine/knowledge.py +5467 -0
  119. rememberstack/spine/lifecycle.py +1071 -0
  120. rememberstack/spine/migrations/__init__.py +1 -0
  121. rememberstack/spine/migrations/_helpers.py +153 -0
  122. rememberstack/spine/migrations/env.py +58 -0
  123. rememberstack/spine/migrations/script.py.mako +27 -0
  124. rememberstack/spine/migrations/versions/__init__.py +1 -0
  125. rememberstack/spine/migrations/versions/p0_02_0001_extensions_enums.py +189 -0
  126. rememberstack/spine/migrations/versions/p0_02_0002_infrastructure_registries.py +321 -0
  127. rememberstack/spine/migrations/versions/p0_02_0003_entities_evaluation_e0_e1.py +631 -0
  128. rememberstack/spine/migrations/versions/p0_02_0004_claims_facts_evidence.py +411 -0
  129. rememberstack/spine/migrations/versions/p0_02_0005_projection_knowledge_retrieval.py +391 -0
  130. rememberstack/spine/migrations/versions/p0_02_0006_partitions_views.py +158 -0
  131. rememberstack/spine/migrations/versions/p2_06_0007_invalidated_outcome.py +26 -0
  132. rememberstack/spine/migrations/versions/p3_01_0008_document_version_target.py +58 -0
  133. rememberstack/spine/migrations/versions/p3_05_0009_reconcile_stage.py +27 -0
  134. rememberstack/spine/migrations/versions/p3_07_0010_lifecycle_eval_suite.py +25 -0
  135. rememberstack/spine/migrations/versions/p4_01_0011_survivor_view_rewrite.py +57 -0
  136. rememberstack/spine/migrations/versions/p6_02_0012_knowledge_compile_recovery.py +58 -0
  137. rememberstack/spine/migrations/versions/p6_04_0013_knowledge_writer_ledger.py +46 -0
  138. rememberstack/spine/migrations/versions/p6_05_0014_knowledge_planner_runtime.py +217 -0
  139. rememberstack/spine/migrations/versions/p6_06_0015_authored_dispatch_runtime.py +38 -0
  140. rememberstack/spine/migrations/versions/p7_02_0016_operational_eval_suite.py +19 -0
  141. rememberstack/spine/migrations/versions/p7_05_0017_hard_forget.py +55 -0
  142. rememberstack/spine/observation_adjudication.py +778 -0
  143. rememberstack/spine/operations.py +298 -0
  144. rememberstack/spine/projection.py +662 -0
  145. rememberstack/spine/recipes.py +276 -0
  146. rememberstack/spine/resolver.py +763 -0
  147. rememberstack/spine/review.py +650 -0
  148. rememberstack/spine/settings.py +22 -0
  149. rememberstack/spine/supersession.py +510 -0
  150. rememberstack/spine/sync.py +128 -0
  151. rememberstack/spine/work_ledger.py +816 -0
  152. rememberstack/surfaces/__init__.py +110 -0
  153. rememberstack/surfaces/cli.py +447 -0
  154. rememberstack/surfaces/consumption_skill.py +87 -0
  155. rememberstack/surfaces/graph_queries.py +698 -0
  156. rememberstack/surfaces/http_api.py +377 -0
  157. rememberstack/surfaces/mcp.py +67 -0
  158. rememberstack/surfaces/query_engine.py +1591 -0
  159. rememberstack/surfaces/recipe_executor.py +185 -0
  160. rememberstack/surfaces/recipe_surface.py +219 -0
  161. rememberstack/surfaces/remote_mcp.py +133 -0
  162. rememberstack/surfaces/sdk.py +324 -0
  163. rememberstack/workers/__init__.py +155 -0
  164. rememberstack/workers/base.py +312 -0
  165. rememberstack/workers/e0.py +577 -0
  166. rememberstack/workers/e1.py +425 -0
  167. rememberstack/workers/e2.py +525 -0
  168. rememberstack/workers/e3.py +434 -0
  169. rememberstack/workers/forget.py +299 -0
  170. rememberstack/workers/knowledge_authored.py +146 -0
  171. rememberstack/workers/knowledge_driver.py +735 -0
  172. rememberstack/workers/knowledge_fact_sheet.py +123 -0
  173. rememberstack/workers/knowledge_planner.py +325 -0
  174. rememberstack/workers/knowledge_writer.py +393 -0
  175. rememberstack/workers/operations.py +42 -0
  176. rememberstack/workers/p1.py +234 -0
  177. rememberstack/workers/p2.py +513 -0
  178. rememberstack/workers/p2_analytics.py +276 -0
  179. rememberstack/workers/p3.py +673 -0
  180. rememberstack/workers/reconcile.py +485 -0
  181. rememberstack/workers/sync.py +168 -0
  182. rememberstack-0.1.0.dist-info/METADATA +213 -0
  183. rememberstack-0.1.0.dist-info/RECORD +186 -0
  184. rememberstack-0.1.0.dist-info/WHEEL +4 -0
  185. rememberstack-0.1.0.dist-info/entry_points.txt +2 -0
  186. rememberstack-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,80 @@
1
+ """D74 purge acknowledgement for self-host projection bytes and caches."""
2
+
3
+ from pathlib import Path
4
+ from pathlib import PurePosixPath
5
+ import shutil
6
+ from uuid import UUID
7
+
8
+ from rememberstack.model import ObjectKey
9
+ from rememberstack.ports import ObjectPurgePort
10
+ from rememberstack.spine import ProjectionCatalog
11
+
12
+
13
+ class SelfHostProjectionPurger:
14
+ """Erase old durable P2/P3 prefixes, registry rows, and local serving copies."""
15
+
16
+ def __init__(
17
+ self,
18
+ *,
19
+ object_purger: ObjectPurgePort,
20
+ catalog: ProjectionCatalog,
21
+ p2_cache_root: Path,
22
+ mount_root: Path,
23
+ ) -> None:
24
+ """Bind explicit durable and local-cache roots without inventing providers."""
25
+ self._object_purger = object_purger
26
+ self._catalog = catalog
27
+ self._p2_cache_root = p2_cache_root.resolve()
28
+ self._mount_root = mount_root.resolve()
29
+
30
+ def purge_projections(
31
+ self, *, deployment_id: UUID, prefixes: tuple[ObjectKey, ...]
32
+ ) -> None:
33
+ """Idempotently erase every manifest-nominated old projection copy."""
34
+ self._object_purger.purge_objects(keys=(), prefixes=prefixes)
35
+ self._catalog.purge_snapshot_prefixes(
36
+ deployment_id=deployment_id,
37
+ prefixes=tuple(prefix.root for prefix in prefixes),
38
+ )
39
+ for prefix in prefixes:
40
+ for path in self._serving_paths(deployment_id=deployment_id, prefix=prefix):
41
+ self._remove(path=path)
42
+
43
+ def verify_projections_purged(
44
+ self, *, deployment_id: UUID, prefixes: tuple[ObjectKey, ...]
45
+ ) -> None:
46
+ """Prove durable bytes, registry rows, caches, and mounts are absent."""
47
+ self._object_purger.verify_objects_purged(keys=(), prefixes=prefixes)
48
+ roots = tuple(prefix.root for prefix in prefixes)
49
+ if self._catalog.snapshot_prefixes_exist(
50
+ deployment_id=deployment_id, prefixes=roots
51
+ ):
52
+ raise RuntimeError("projection purge verification found registry rows")
53
+ remaining = [
54
+ str(path)
55
+ for prefix in prefixes
56
+ for path in self._serving_paths(deployment_id=deployment_id, prefix=prefix)
57
+ if path.exists() or path.is_symlink()
58
+ ]
59
+ if remaining:
60
+ raise RuntimeError(
61
+ f"projection purge verification found serving copies: {remaining!r}"
62
+ )
63
+
64
+ def _serving_paths(
65
+ self, *, deployment_id: UUID, prefix: ObjectKey
66
+ ) -> tuple[Path, Path]:
67
+ """Return the exact P2 cache and P3 versioned mount for one prefix."""
68
+ version = PurePosixPath(prefix.root).name
69
+ return (
70
+ self._p2_cache_root / str(deployment_id) / version,
71
+ self._mount_root / str(deployment_id) / f"p3-{version}",
72
+ )
73
+
74
+ @staticmethod
75
+ def _remove(*, path: Path) -> None:
76
+ """Remove one exact serving copy, treating absence as success."""
77
+ if path.is_symlink() or path.is_file():
78
+ path.unlink(missing_ok=True)
79
+ elif path.is_dir():
80
+ shutil.rmtree(path)
@@ -0,0 +1,137 @@
1
+ """The self-host delivery shell: LISTEN/NOTIFY wake-ups + SKIP LOCKED claiming (D62/D67).
2
+
3
+ Work is Postgres rows; this shell only delivers wake-ups. The schema-owned
4
+ insert trigger provides the transactional initial wake; `announce` re-announces
5
+ existing rows through the spine's notification primitive; the worker loop
6
+ sleeps on LISTEN with a slow fallback poll and claims only its configured
7
+ route, consulting a token bucket around each claim.
8
+ """
9
+
10
+ from collections.abc import Callable
11
+ import logging
12
+ import time
13
+ from uuid import UUID
14
+
15
+ import psycopg
16
+
17
+ from rememberstack.model import PipelineStage
18
+ from rememberstack.model import ProcessingLane
19
+ from rememberstack.model import QueueRoute
20
+ from rememberstack.model import RunResultOutcome
21
+ from rememberstack.model import UTCDateTime
22
+ from rememberstack.spine.work_ledger import WorkLedger
23
+ from rememberstack.workers import RunResult
24
+ from rememberstack.workers import Worker
25
+
26
+ _logger = logging.getLogger(__name__)
27
+
28
+ _WAKE_CHANNEL = "queue_wake"
29
+
30
+
31
+ class SelfHostTaskQueue:
32
+ """The delivery-only announce adapter: wake an existing committed row.
33
+
34
+ Satisfies the task-queue port. Route and due-time snapshots are accepted
35
+ per the port contract but carry no state — the woken worker re-reads
36
+ Postgres, which is the only authority (D67).
37
+ """
38
+
39
+ def __init__(self, *, ledger: WorkLedger) -> None:
40
+ """Bind the adapter to the spine's notification primitive."""
41
+ self._ledger = ledger
42
+
43
+ def announce(
44
+ self,
45
+ *,
46
+ processing_id: UUID,
47
+ route_snapshot: QueueRoute,
48
+ not_before_snapshot: UTCDateTime,
49
+ ) -> None:
50
+ """Schedule at-least-once delivery by waking listeners for the row."""
51
+ del route_snapshot, not_before_snapshot # snapshots are hints, never state
52
+ self._ledger.wake(processing_id=processing_id)
53
+
54
+
55
+ class TokenBucket:
56
+ """A per-route claim rate limit: refill at a fixed rate up to a capacity."""
57
+
58
+ def __init__(
59
+ self,
60
+ *,
61
+ rate_per_s: float,
62
+ capacity: float,
63
+ clock: Callable[[], float] = time.monotonic,
64
+ ) -> None:
65
+ """Start full; `clock` is injectable for deterministic tests."""
66
+ self._rate_per_s = rate_per_s
67
+ self._capacity = capacity
68
+ self._clock = clock
69
+ self._tokens = capacity
70
+ self._refilled_at = clock()
71
+
72
+ def try_acquire(self) -> bool:
73
+ """Take one token if available; never blocks."""
74
+ now = self._clock()
75
+ self._tokens = min(
76
+ self._capacity, self._tokens + (now - self._refilled_at) * self._rate_per_s
77
+ )
78
+ self._refilled_at = now
79
+ if self._tokens < 1.0:
80
+ return False
81
+ self._tokens -= 1.0
82
+ return True
83
+
84
+
85
+ class SelfHostWorkerLoop:
86
+ """One route's worker loop: sleep on LISTEN, drain due work, fall back to polling."""
87
+
88
+ def __init__(
89
+ self,
90
+ *,
91
+ worker: Worker,
92
+ deployment_id: UUID,
93
+ stage: PipelineStage,
94
+ lane: ProcessingLane | None,
95
+ bucket: TokenBucket,
96
+ database_url: str,
97
+ fallback_poll_s: float = 30.0,
98
+ ) -> None:
99
+ """Bind the loop to its route, rate limit, and LISTEN connection source."""
100
+ self._worker = worker
101
+ self._deployment_id = deployment_id
102
+ self._stage = stage
103
+ self._lane = lane
104
+ self._bucket = bucket
105
+ self._database_url = database_url
106
+ self._fallback_poll_s = fallback_poll_s
107
+
108
+ def drain_due(self) -> tuple[RunResult, ...]:
109
+ """Claim and run due work on the route until none remains or tokens run out."""
110
+ results: list[RunResult] = []
111
+ while self._bucket.try_acquire():
112
+ result = self._worker.run_one(
113
+ deployment_id=self._deployment_id, stage=self._stage, lane=self._lane
114
+ )
115
+ if result.outcome is RunResultOutcome.NO_WORK:
116
+ break
117
+ results.append(result)
118
+ return tuple(results)
119
+
120
+ def run_for(self, *, duration_s: float) -> tuple[RunResult, ...]:
121
+ """Listen, drain on every wake, and fall back to polling, for a bounded time.
122
+
123
+ The production loop is this method called with a large duration under a
124
+ supervisor; tests call it with a short one. Missed notifications are
125
+ covered by the fallback poll, per the packaging design.
126
+ """
127
+ results: list[RunResult] = list(self.drain_due())
128
+ deadline = time.monotonic() + duration_s
129
+ with psycopg.connect(self._database_url, autocommit=True) as connection:
130
+ connection.execute(f"LISTEN {_WAKE_CHANNEL}")
131
+ while (remaining := deadline - time.monotonic()) > 0:
132
+ timeout = min(remaining, self._fallback_poll_s)
133
+ woken = list(connection.notifies(timeout=timeout, stop_after=1))
134
+ if woken:
135
+ _logger.debug("wake received: %s", woken[0].payload)
136
+ results.extend(self.drain_due())
137
+ return tuple(results)
@@ -0,0 +1,45 @@
1
+ """Simple JSON-lines telemetry for self-hosted process logs."""
2
+
3
+ import json
4
+ import sys
5
+ from threading import Lock
6
+ import traceback
7
+ from typing import TextIO
8
+
9
+ from rememberstack.model import TelemetryEvent
10
+
11
+
12
+ class JsonLineTelemetry:
13
+ """Write one complete structured event per line to a process-owned stream."""
14
+
15
+ def __init__(self, *, stream: TextIO | None = None) -> None:
16
+ """Use stdout by default and serialize concurrent worker writes."""
17
+ self._stream = stream or sys.stdout
18
+ self._lock = Lock()
19
+
20
+ def export_event(self, *, event: TelemetryEvent) -> None:
21
+ """Write and flush one ordinary event."""
22
+ self._write(payload=event.model_dump(mode="json"))
23
+
24
+ def export_exception(
25
+ self, *, event: TelemetryEvent, exception: BaseException
26
+ ) -> None:
27
+ """Write the event plus the supplied exception's complete cause chain."""
28
+ payload = event.model_dump(mode="json")
29
+ payload["exception"] = {
30
+ "type": type(exception).__qualname__,
31
+ "message": str(exception),
32
+ "traceback": "".join(
33
+ traceback.TracebackException.from_exception(exception).format(
34
+ chain=True
35
+ )
36
+ ),
37
+ }
38
+ self._write(payload=payload)
39
+
40
+ def _write(self, *, payload: dict[str, object]) -> None:
41
+ """Keep every event atomic at the stream boundary."""
42
+ line = json.dumps(payload, sort_keys=True, separators=(",", ":"))
43
+ with self._lock:
44
+ self._stream.write(line + "\n")
45
+ self._stream.flush()
@@ -0,0 +1,70 @@
1
+ """The local-directory watched source: watch a folder on the self-host tier.
2
+
3
+ The genuinely useful self-host connector (and the lifecycle machinery's test
4
+ double with real semantics): source_ref = the file's relative path, revision
5
+ = mtime+size, deletion = the file is gone. Cloud connectors (Drive) implement
6
+ the same port behind their provider adapters.
7
+ """
8
+
9
+ from collections.abc import Mapping
10
+ from datetime import datetime
11
+ from datetime import UTC
12
+ from pathlib import Path
13
+
14
+ from rememberstack.model import SourceItem
15
+
16
+ _MIME_BY_SUFFIX = {".md": "text/markdown", ".txt": "text/plain", ".html": "text/html"}
17
+
18
+
19
+ class LocalDirectoryWatcher:
20
+ """Watch one directory tree of text documents."""
21
+
22
+ def __init__(self, *, root: Path) -> None:
23
+ """Bind the watcher to its directory (created if absent)."""
24
+ self._root = root
25
+ self._root.mkdir(parents=True, exist_ok=True)
26
+
27
+ def poll(self, *, known: Mapping[str, str]) -> tuple[SourceItem, ...]:
28
+ """Every current file, plus deletion markers for vanished refs.
29
+
30
+ Paths that resolve outside the root (symlinks pointing elsewhere)
31
+ are skipped: a writable watched directory must not become a lever
32
+ for ingesting arbitrary readable files into memory.
33
+ """
34
+ base = self._root.resolve()
35
+ items: list[SourceItem] = []
36
+ seen: set[str] = set()
37
+ for path in sorted(self._root.rglob("*")):
38
+ if not path.is_file() or path.suffix not in _MIME_BY_SUFFIX:
39
+ continue
40
+ if not path.resolve().is_relative_to(base):
41
+ continue # a symlink escaping the root is not a watched file
42
+ ref = path.relative_to(self._root).as_posix()
43
+ seen.add(ref)
44
+ stat = path.stat()
45
+ items.append(
46
+ SourceItem(
47
+ source_ref=ref,
48
+ revision=f"{stat.st_mtime_ns}:{stat.st_size}",
49
+ modified_at=datetime.fromtimestamp(stat.st_mtime, tz=UTC),
50
+ filename=path.name,
51
+ mime=_MIME_BY_SUFFIX[path.suffix],
52
+ )
53
+ )
54
+ for ref in sorted(set(known) - seen):
55
+ items.append(
56
+ SourceItem(
57
+ source_ref=ref,
58
+ revision="deleted",
59
+ modified_at=datetime.now(tz=UTC),
60
+ deleted=True,
61
+ )
62
+ )
63
+ return tuple(items)
64
+
65
+ def fetch(self, *, source_ref: str) -> bytes:
66
+ """The file's current bytes; refs escaping the root are refused."""
67
+ path = (self._root / source_ref).resolve()
68
+ if not path.is_relative_to(self._root.resolve()):
69
+ raise PermissionError(f"{source_ref!r} escapes the watched root")
70
+ return path.read_bytes()
@@ -0,0 +1,15 @@
1
+ """Test-tier adapters: in-memory doubles outside the two-maintained-adapter set."""
2
+
3
+ from rememberstack.adapters.testing.cost_meter import NoopCostMeter
4
+ from rememberstack.adapters.testing.model_provider import FakeModelProvider
5
+ from rememberstack.adapters.testing.queue import RecordedAnnouncement
6
+ from rememberstack.adapters.testing.queue import RecordingTaskQueue
7
+ from rememberstack.adapters.testing.telemetry import RecordingTelemetry
8
+
9
+ __all__ = (
10
+ "FakeModelProvider",
11
+ "NoopCostMeter",
12
+ "RecordedAnnouncement",
13
+ "RecordingTaskQueue",
14
+ "RecordingTelemetry",
15
+ )
@@ -0,0 +1,13 @@
1
+ """A no-op cost meter for tests that invoke stage handlers directly."""
2
+
3
+ from rememberstack.model import ProviderCallUsage
4
+
5
+
6
+ class NoopCostMeter:
7
+ """Accept provider accounting without persisting it."""
8
+
9
+ def record(
10
+ self, *, call_key: str, tier: str | None, usage: ProviderCallUsage
11
+ ) -> None:
12
+ """Discard one test-only call record."""
13
+ del call_key, tier, usage
@@ -0,0 +1,83 @@
1
+ """A deterministic in-memory model provider for behavior tests (no network)."""
2
+
3
+ from decimal import Decimal
4
+ import hashlib
5
+
6
+ from rememberstack.model import EmbeddingRequest
7
+ from rememberstack.model import EmbeddingResponse
8
+ from rememberstack.model import GeneratedResponse
9
+ from rememberstack.model import ModelRequest
10
+ from rememberstack.model import ProviderCallUsage
11
+ from rememberstack.model import StructuredResponseModel
12
+
13
+ _EMBEDDING_DIMENSION = 8
14
+
15
+
16
+ class FakeModelProvider:
17
+ """Deterministic embeddings and canned structured generations for proofs."""
18
+
19
+ def __init__(
20
+ self,
21
+ *,
22
+ generate_payload: dict[str, object] | None = None,
23
+ generate_payloads: dict[str, dict[str, object]] | None = None,
24
+ generate_router: object | None = None,
25
+ ) -> None:
26
+ """Bind canned payloads: one default, one per response-type name, or a
27
+ router callable (prompt, type_name) -> payload for per-call behavior."""
28
+ self._generate_payload = generate_payload
29
+ self._generate_payloads = generate_payloads or {}
30
+ self._generate_router = generate_router
31
+ self.embedded_texts: list[str] = []
32
+ self.generated_prompts: list[str] = []
33
+
34
+ def generate[ResponseT: StructuredResponseModel](
35
+ self, *, request: ModelRequest, response_type: type[ResponseT]
36
+ ) -> GeneratedResponse[ResponseT]:
37
+ """Return the canned payload validated as the caller's declared type."""
38
+ self.generated_prompts.append(request.prompt)
39
+ if callable(self._generate_router):
40
+ output = response_type.model_validate(
41
+ self._generate_router(request.prompt, response_type.__name__)
42
+ )
43
+ else:
44
+ payload = self._generate_payloads.get(
45
+ response_type.__name__, self._generate_payload
46
+ )
47
+ if payload is None:
48
+ raise AssertionError(f"no canned payload for {response_type.__name__}")
49
+ output = response_type.model_validate(payload)
50
+ return GeneratedResponse(
51
+ output=output,
52
+ usage=_fake_usage(
53
+ model_name=request.model, tokens_in=len(request.prompt.split())
54
+ ),
55
+ )
56
+
57
+ def embed(self, *, request: EmbeddingRequest) -> EmbeddingResponse:
58
+ """Return one deterministic content-derived vector per input text."""
59
+ self.embedded_texts.extend(request.texts)
60
+ return EmbeddingResponse(
61
+ vectors=tuple(_vector_for(text=text) for text in request.texts),
62
+ usage=_fake_usage(
63
+ model_name=request.model,
64
+ tokens_in=sum(len(text.split()) for text in request.texts),
65
+ ),
66
+ )
67
+
68
+
69
+ def _fake_usage(*, model_name: str, tokens_in: int) -> ProviderCallUsage:
70
+ """Return deterministic zero-cost accounting for one in-memory provider call."""
71
+ return ProviderCallUsage(
72
+ model_name=model_name,
73
+ tokens_in=tokens_in,
74
+ tokens_out=0,
75
+ cost_usd=Decimal(0),
76
+ latency_ms=0,
77
+ )
78
+
79
+
80
+ def _vector_for(*, text: str) -> tuple[float, ...]:
81
+ """Derive a stable pseudo-vector from the text content."""
82
+ digest = hashlib.sha256(text.encode("utf-8")).digest()
83
+ return tuple(byte / 255.0 for byte in digest[:_EMBEDDING_DIMENSION])
@@ -0,0 +1,43 @@
1
+ """The test-tier task queue: records announcements in memory, delivers nothing."""
2
+
3
+ from uuid import UUID
4
+
5
+ from pydantic import BaseModel
6
+ from pydantic import ConfigDict
7
+
8
+ from rememberstack.model import QueueRoute
9
+ from rememberstack.model import UTCDateTime
10
+
11
+
12
+ class RecordedAnnouncement(BaseModel):
13
+ """One announce call as the port received it, for assertions."""
14
+
15
+ model_config = ConfigDict(frozen=True, extra="forbid")
16
+
17
+ processing_id: UUID
18
+ route_snapshot: QueueRoute
19
+ not_before_snapshot: UTCDateTime
20
+
21
+
22
+ class RecordingTaskQueue:
23
+ """Satisfies the task-queue port; tests assert on the recorded announcements."""
24
+
25
+ def __init__(self) -> None:
26
+ """Start with an empty announcement log."""
27
+ self.announcements: list[RecordedAnnouncement] = []
28
+
29
+ def announce(
30
+ self,
31
+ *,
32
+ processing_id: UUID,
33
+ route_snapshot: QueueRoute,
34
+ not_before_snapshot: UTCDateTime,
35
+ ) -> None:
36
+ """Record the announcement; delivery is the test's job, if any."""
37
+ self.announcements.append(
38
+ RecordedAnnouncement(
39
+ processing_id=processing_id,
40
+ route_snapshot=route_snapshot,
41
+ not_before_snapshot=not_before_snapshot,
42
+ )
43
+ )
@@ -0,0 +1,22 @@
1
+ """In-memory telemetry exporter preserving exception identity for tests."""
2
+
3
+ from rememberstack.model import TelemetryEvent
4
+
5
+
6
+ class RecordingTelemetry:
7
+ """Record structured events and original exception objects in call order."""
8
+
9
+ def __init__(self) -> None:
10
+ """Start with no exports."""
11
+ self.events: list[TelemetryEvent] = []
12
+ self.exceptions: list[tuple[TelemetryEvent, BaseException]] = []
13
+
14
+ def export_event(self, *, event: TelemetryEvent) -> None:
15
+ """Record a non-exception event."""
16
+ self.events.append(event)
17
+
18
+ def export_exception(
19
+ self, *, event: TelemetryEvent, exception: BaseException
20
+ ) -> None:
21
+ """Record the exact exception instance supplied by the worker."""
22
+ self.exceptions.append((event, exception))
@@ -0,0 +1,19 @@
1
+ """Public dependency-light client API."""
2
+
3
+ from rememberstack.model.client import ConnectorCreate
4
+ from rememberstack.model.client import ConnectorDescriptor
5
+ from rememberstack.model.client import ConnectorNotFoundError
6
+ from rememberstack.model.client import ToolDescriptor
7
+ from rememberstack.surfaces.sdk import ClientSettings
8
+ from rememberstack.surfaces.sdk import MemoryApiError
9
+ from rememberstack.surfaces.sdk import MemoryClient
10
+
11
+ __all__ = (
12
+ "ClientSettings",
13
+ "ConnectorCreate",
14
+ "ConnectorDescriptor",
15
+ "ConnectorNotFoundError",
16
+ "MemoryApiError",
17
+ "MemoryClient",
18
+ "ToolDescriptor",
19
+ )
@@ -0,0 +1,127 @@
1
+ """Pure domain logic package."""
2
+
3
+ from rememberstack.core.blockizer import block_hash
4
+ from rememberstack.core.blockizer import blockize
5
+ from rememberstack.core.blockizer import BLOCKIZER_VERSION
6
+ from rememberstack.core.blockizer import normalized_block_text
7
+ from rememberstack.core.chunker import chunk_content_hash
8
+ from rememberstack.core.chunker import CHUNKER_VERSION
9
+ from rememberstack.core.chunker import chunker_version
10
+ from rememberstack.core.chunker import ChunkerParams
11
+ from rememberstack.core.chunker import count_tokens
12
+ from rememberstack.core.chunker import extraction_input_hash
13
+ from rememberstack.core.chunker import is_anchor
14
+ from rememberstack.core.chunker import pack_blocks
15
+ from rememberstack.core.consumption_skill import CONSUMPTION_SKILL_VERSION
16
+ from rememberstack.core.consumption_skill import render_consumption_skill
17
+ from rememberstack.core.conversion import ConversionRouter
18
+ from rememberstack.core.conversion import Converter
19
+ from rememberstack.core.conversion import MarkdownPassthroughConverter
20
+ from rememberstack.core.conversion import PASSTHROUGH_CONVERTER_VERSION
21
+ from rememberstack.core.core_manifest import CORE_MANIFEST
22
+ from rememberstack.core.core_manifest import CoreManifest
23
+ from rememberstack.core.core_manifest import EntityTypeDefinition
24
+ from rememberstack.core.core_manifest import PredicateDefinition
25
+ from rememberstack.core.core_manifest import PredicateSignatureDefinition
26
+ from rememberstack.core.extension_packs import ExtensionPack
27
+ from rememberstack.core.extension_packs import PackEntityType
28
+ from rememberstack.core.extension_packs import PackPredicate
29
+ from rememberstack.core.extension_packs import WORK_PACK
30
+ from rememberstack.core.forget import source_identity_hash
31
+ from rememberstack.core.knowledge_authored import authored_declaration_is_empty
32
+ from rememberstack.core.knowledge_authored import knowledge_citation_reference
33
+ from rememberstack.core.knowledge_authored import KnowledgeAuthoredDeclarationError
34
+ from rememberstack.core.knowledge_authored import parse_knowledge_authored_frontmatter
35
+ from rememberstack.core.knowledge_compile import knowledge_compile_order
36
+ from rememberstack.core.knowledge_compile import KnowledgeCompileGraphError
37
+ from rememberstack.core.knowledge_compile import KnowledgePageValidationError
38
+ from rememberstack.core.knowledge_compile import validate_knowledge_page_output
39
+ from rememberstack.core.knowledge_fact_sheet import compose_knowledge_page
40
+ from rememberstack.core.knowledge_fact_sheet import KnowledgeFactLifecycle
41
+ from rememberstack.core.knowledge_fact_sheet import render_knowledge_fact_sheet
42
+ from rememberstack.core.knowledge_hashing import knowledge_content_hash
43
+ from rememberstack.core.knowledge_hashing import knowledge_inputs_hash
44
+ from rememberstack.core.knowledge_hashing import knowledge_summary_hash
45
+ from rememberstack.core.knowledge_planner import knowledge_planning_input_hash
46
+ from rememberstack.core.knowledge_planner import primary_knowledge_plan_trigger
47
+ from rememberstack.core.knowledge_planner import route_knowledge_plan
48
+ from rememberstack.core.knowledge_writer import cap_knowledge_writer_bundle
49
+ from rememberstack.core.knowledge_writer import knowledge_writer_coverage
50
+ from rememberstack.core.knowledge_writer import render_knowledge_writer_bundle
51
+ from rememberstack.core.ranking import DEFAULT_EVIDENCE_COUNT_WEIGHT
52
+ from rememberstack.core.ranking import DEFAULT_GRAPH_DISTANCE_WEIGHT
53
+ from rememberstack.core.ranking import DEFAULT_RRF_K
54
+ from rememberstack.core.ranking import reciprocal_rank_fusion
55
+ from rememberstack.core.ranking import rerank_by_signal
56
+ from rememberstack.core.ranking import rerank_by_weighted_signals
57
+ from rememberstack.core.recipe_linter import KNOWN_OPS
58
+ from rememberstack.core.recipe_linter import lint_recipe
59
+ from rememberstack.core.recipe_linter import RecipeLintError
60
+ from rememberstack.core.section_snap import SECTION_ROLES
61
+ from rememberstack.core.section_snap import snap_sections
62
+ from rememberstack.core.storage_routing import HOT_MIME_PREFIXES
63
+ from rememberstack.core.storage_routing import storage_class_for
64
+
65
+ __all__ = (
66
+ "BLOCKIZER_VERSION",
67
+ "CHUNKER_VERSION",
68
+ "ChunkerParams",
69
+ "chunk_content_hash",
70
+ "chunker_version",
71
+ "count_tokens",
72
+ "extraction_input_hash",
73
+ "is_anchor",
74
+ "pack_blocks",
75
+ "CORE_MANIFEST",
76
+ "ExtensionPack",
77
+ "PackEntityType",
78
+ "PackPredicate",
79
+ "WORK_PACK",
80
+ "ConversionRouter",
81
+ "CONSUMPTION_SKILL_VERSION",
82
+ "Converter",
83
+ "CoreManifest",
84
+ "EntityTypeDefinition",
85
+ "MarkdownPassthroughConverter",
86
+ "PASSTHROUGH_CONVERTER_VERSION",
87
+ "PredicateDefinition",
88
+ "PredicateSignatureDefinition",
89
+ "block_hash",
90
+ "blockize",
91
+ "normalized_block_text",
92
+ "HOT_MIME_PREFIXES",
93
+ "storage_class_for",
94
+ "source_identity_hash",
95
+ "SECTION_ROLES",
96
+ "snap_sections",
97
+ "DEFAULT_EVIDENCE_COUNT_WEIGHT",
98
+ "DEFAULT_GRAPH_DISTANCE_WEIGHT",
99
+ "DEFAULT_RRF_K",
100
+ "KNOWN_OPS",
101
+ "KnowledgeCompileGraphError",
102
+ "KnowledgeAuthoredDeclarationError",
103
+ "KnowledgeFactLifecycle",
104
+ "KnowledgePageValidationError",
105
+ "knowledge_content_hash",
106
+ "knowledge_citation_reference",
107
+ "knowledge_compile_order",
108
+ "knowledge_inputs_hash",
109
+ "knowledge_planning_input_hash",
110
+ "knowledge_summary_hash",
111
+ "primary_knowledge_plan_trigger",
112
+ "parse_knowledge_authored_frontmatter",
113
+ "compose_knowledge_page",
114
+ "RecipeLintError",
115
+ "lint_recipe",
116
+ "reciprocal_rank_fusion",
117
+ "render_consumption_skill",
118
+ "render_knowledge_fact_sheet",
119
+ "cap_knowledge_writer_bundle",
120
+ "knowledge_writer_coverage",
121
+ "render_knowledge_writer_bundle",
122
+ "route_knowledge_plan",
123
+ "rerank_by_signal",
124
+ "rerank_by_weighted_signals",
125
+ "validate_knowledge_page_output",
126
+ "authored_declaration_is_empty",
127
+ )