shellbrain 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 (165) hide show
  1. app/__init__.py +1 -0
  2. app/__main__.py +7 -0
  3. app/boot/__init__.py +1 -0
  4. app/boot/admin_db.py +88 -0
  5. app/boot/config.py +14 -0
  6. app/boot/create_policy.py +52 -0
  7. app/boot/db.py +70 -0
  8. app/boot/embeddings.py +55 -0
  9. app/boot/home.py +45 -0
  10. app/boot/migrations.py +61 -0
  11. app/boot/read_policy.py +179 -0
  12. app/boot/repos.py +15 -0
  13. app/boot/retrieval.py +3 -0
  14. app/boot/thresholds.py +19 -0
  15. app/boot/update_policy.py +34 -0
  16. app/boot/use_cases.py +22 -0
  17. app/config/__init__.py +1 -0
  18. app/config/defaults/create_policy.yaml +7 -0
  19. app/config/defaults/read_policy.yaml +25 -0
  20. app/config/defaults/runtime.yaml +10 -0
  21. app/config/defaults/thresholds.yaml +3 -0
  22. app/config/defaults/update_policy.yaml +5 -0
  23. app/config/loader.py +58 -0
  24. app/core/__init__.py +1 -0
  25. app/core/contracts/__init__.py +1 -0
  26. app/core/contracts/errors.py +29 -0
  27. app/core/contracts/requests.py +211 -0
  28. app/core/contracts/responses.py +15 -0
  29. app/core/entities/__init__.py +1 -0
  30. app/core/entities/associations.py +58 -0
  31. app/core/entities/episodes.py +66 -0
  32. app/core/entities/evidence.py +29 -0
  33. app/core/entities/facts.py +30 -0
  34. app/core/entities/guidance.py +47 -0
  35. app/core/entities/identity.py +48 -0
  36. app/core/entities/memory.py +34 -0
  37. app/core/entities/runtime_context.py +19 -0
  38. app/core/entities/session_state.py +31 -0
  39. app/core/entities/telemetry.py +152 -0
  40. app/core/entities/utility.py +14 -0
  41. app/core/interfaces/__init__.py +1 -0
  42. app/core/interfaces/clock.py +12 -0
  43. app/core/interfaces/config.py +28 -0
  44. app/core/interfaces/embeddings.py +12 -0
  45. app/core/interfaces/idgen.py +11 -0
  46. app/core/interfaces/repos.py +279 -0
  47. app/core/interfaces/retrieval.py +20 -0
  48. app/core/interfaces/session_state_store.py +33 -0
  49. app/core/interfaces/unit_of_work.py +50 -0
  50. app/core/policies/__init__.py +1 -0
  51. app/core/policies/_shared/__init__.py +1 -0
  52. app/core/policies/_shared/executor.py +132 -0
  53. app/core/policies/_shared/side_effects.py +9 -0
  54. app/core/policies/create_policy/__init__.py +1 -0
  55. app/core/policies/create_policy/pipeline.py +96 -0
  56. app/core/policies/read_policy/__init__.py +1 -0
  57. app/core/policies/read_policy/bm25.py +114 -0
  58. app/core/policies/read_policy/context_pack_builder.py +140 -0
  59. app/core/policies/read_policy/expansion.py +132 -0
  60. app/core/policies/read_policy/fusion_rrf.py +34 -0
  61. app/core/policies/read_policy/lexical_query.py +101 -0
  62. app/core/policies/read_policy/pipeline.py +93 -0
  63. app/core/policies/read_policy/scenario_lift.py +11 -0
  64. app/core/policies/read_policy/scoring.py +61 -0
  65. app/core/policies/read_policy/seed_retrieval.py +54 -0
  66. app/core/policies/read_policy/utility_prior.py +11 -0
  67. app/core/policies/update_policy/__init__.py +1 -0
  68. app/core/policies/update_policy/pipeline.py +80 -0
  69. app/core/use_cases/__init__.py +1 -0
  70. app/core/use_cases/build_guidance.py +85 -0
  71. app/core/use_cases/create_memory.py +26 -0
  72. app/core/use_cases/manage_session_state.py +159 -0
  73. app/core/use_cases/read_memory.py +21 -0
  74. app/core/use_cases/record_episode_sync_telemetry.py +19 -0
  75. app/core/use_cases/record_operation_telemetry.py +32 -0
  76. app/core/use_cases/sync_episode.py +162 -0
  77. app/core/use_cases/update_memory.py +40 -0
  78. app/migrations/__init__.py +1 -0
  79. app/migrations/env.py +65 -0
  80. app/migrations/versions/20260226_0001_initial_schema.py +232 -0
  81. app/migrations/versions/20260312_0002_add_hard_invariants.py +60 -0
  82. app/migrations/versions/20260312_0003_drop_create_confidence.py +40 -0
  83. app/migrations/versions/20260313_0004_episode_sync_hardening.py +71 -0
  84. app/migrations/versions/20260313_0005_evidence_episode_event_refs.py +45 -0
  85. app/migrations/versions/20260318_0006_usage_telemetry_schema.py +175 -0
  86. app/migrations/versions/20260319_0007_identity_session_guidance.py +49 -0
  87. app/migrations/versions/20260320_0008_instance_metadata_and_backup_safety.py +31 -0
  88. app/migrations/versions/__init__.py +1 -0
  89. app/periphery/__init__.py +1 -0
  90. app/periphery/admin/__init__.py +1 -0
  91. app/periphery/admin/backup.py +360 -0
  92. app/periphery/admin/destructive_guard.py +32 -0
  93. app/periphery/admin/doctor.py +192 -0
  94. app/periphery/admin/init.py +996 -0
  95. app/periphery/admin/instance_guard.py +211 -0
  96. app/periphery/admin/machine_state.py +354 -0
  97. app/periphery/admin/privileges.py +42 -0
  98. app/periphery/admin/repo_state.py +266 -0
  99. app/periphery/admin/restore.py +30 -0
  100. app/periphery/cli/__init__.py +1 -0
  101. app/periphery/cli/handlers.py +830 -0
  102. app/periphery/cli/hydration.py +119 -0
  103. app/periphery/cli/main.py +710 -0
  104. app/periphery/cli/presenter_json.py +10 -0
  105. app/periphery/cli/schema_validation.py +201 -0
  106. app/periphery/db/__init__.py +1 -0
  107. app/periphery/db/engine.py +10 -0
  108. app/periphery/db/models/__init__.py +1 -0
  109. app/periphery/db/models/associations.py +55 -0
  110. app/periphery/db/models/episodes.py +55 -0
  111. app/periphery/db/models/evidence.py +19 -0
  112. app/periphery/db/models/experiences.py +33 -0
  113. app/periphery/db/models/instance_metadata.py +17 -0
  114. app/periphery/db/models/memories.py +39 -0
  115. app/periphery/db/models/metadata.py +6 -0
  116. app/periphery/db/models/registry.py +18 -0
  117. app/periphery/db/models/telemetry.py +174 -0
  118. app/periphery/db/models/utility.py +19 -0
  119. app/periphery/db/models/views.py +154 -0
  120. app/periphery/db/repos/__init__.py +1 -0
  121. app/periphery/db/repos/relational/__init__.py +1 -0
  122. app/periphery/db/repos/relational/associations_repo.py +117 -0
  123. app/periphery/db/repos/relational/episodes_repo.py +188 -0
  124. app/periphery/db/repos/relational/evidence_repo.py +82 -0
  125. app/periphery/db/repos/relational/experiences_repo.py +41 -0
  126. app/periphery/db/repos/relational/memories_repo.py +99 -0
  127. app/periphery/db/repos/relational/read_policy_repo.py +202 -0
  128. app/periphery/db/repos/relational/telemetry_repo.py +161 -0
  129. app/periphery/db/repos/relational/utility_repo.py +30 -0
  130. app/periphery/db/repos/semantic/__init__.py +1 -0
  131. app/periphery/db/repos/semantic/keyword_retrieval_repo.py +63 -0
  132. app/periphery/db/repos/semantic/semantic_retrieval_repo.py +111 -0
  133. app/periphery/db/session.py +10 -0
  134. app/periphery/db/uow.py +75 -0
  135. app/periphery/embeddings/__init__.py +1 -0
  136. app/periphery/embeddings/local_provider.py +35 -0
  137. app/periphery/embeddings/query_vector_search.py +18 -0
  138. app/periphery/episodes/__init__.py +1 -0
  139. app/periphery/episodes/claude_code.py +387 -0
  140. app/periphery/episodes/codex.py +423 -0
  141. app/periphery/episodes/launcher.py +66 -0
  142. app/periphery/episodes/normalization.py +31 -0
  143. app/periphery/episodes/poller.py +299 -0
  144. app/periphery/episodes/source_discovery.py +66 -0
  145. app/periphery/episodes/tool_filter.py +165 -0
  146. app/periphery/identity/__init__.py +1 -0
  147. app/periphery/identity/claude_hook_install.py +67 -0
  148. app/periphery/identity/claude_runtime.py +83 -0
  149. app/periphery/identity/codex_runtime.py +32 -0
  150. app/periphery/identity/compatibility.py +38 -0
  151. app/periphery/identity/resolver.py +163 -0
  152. app/periphery/session_state/__init__.py +1 -0
  153. app/periphery/session_state/file_store.py +100 -0
  154. app/periphery/telemetry/__init__.py +33 -0
  155. app/periphery/telemetry/operation_summary.py +299 -0
  156. app/periphery/telemetry/session_selection.py +156 -0
  157. app/periphery/telemetry/sync_summary.py +65 -0
  158. app/periphery/validation/__init__.py +1 -0
  159. app/periphery/validation/integrity_validation.py +253 -0
  160. app/periphery/validation/semantic_validation.py +94 -0
  161. shellbrain-0.1.0.dist-info/METADATA +130 -0
  162. shellbrain-0.1.0.dist-info/RECORD +165 -0
  163. shellbrain-0.1.0.dist-info/WHEEL +5 -0
  164. shellbrain-0.1.0.dist-info/entry_points.txt +2 -0
  165. shellbrain-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,266 @@
1
+ """Repo-local registration and identity helpers for Shellbrain."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from datetime import datetime, timezone
7
+ import hashlib
8
+ import json
9
+ from pathlib import Path
10
+ import re
11
+ import subprocess
12
+ import tomllib
13
+ from typing import Any
14
+
15
+
16
+ REPO_STATE_VERSION = 1
17
+ IDENTITY_STRENGTH_EXPLICIT = "explicit"
18
+ IDENTITY_STRENGTH_GIT_REMOTE = "git_remote"
19
+ IDENTITY_STRENGTH_WEAK_LOCAL = "weak_local"
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class RepoRegistration:
24
+ """Repo-local registration metadata bound to one machine instance."""
25
+
26
+ repo_state_version: int
27
+ repo_id: str
28
+ identity_strength: str
29
+ git_root: str | None
30
+ source_remote: str | None
31
+ registered_at: str
32
+ machine_instance_id: str
33
+ claude_status: str
34
+ claude_settings_path: str | None = None
35
+ claude_note: str | None = None
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class RepoIdentity:
40
+ """Resolved repo identity before registration."""
41
+
42
+ repo_id: str
43
+ identity_strength: str
44
+ git_root: str | None
45
+ source_remote: str | None
46
+
47
+
48
+ def repo_runtime_dir(repo_root: Path) -> Path:
49
+ """Return the repo-local runtime directory."""
50
+
51
+ return Path(repo_root).resolve() / ".shellbrain"
52
+
53
+
54
+ def repo_registration_path(repo_root: Path) -> Path:
55
+ """Return the repo-local registration file path."""
56
+
57
+ return repo_runtime_dir(repo_root) / "repo_registration.toml"
58
+
59
+
60
+ def load_repo_registration(repo_root: Path) -> RepoRegistration | None:
61
+ """Load one repo registration when present."""
62
+
63
+ path = repo_registration_path(repo_root)
64
+ try:
65
+ payload = tomllib.loads(path.read_text(encoding="utf-8"))
66
+ except FileNotFoundError:
67
+ return None
68
+ if not isinstance(payload, dict):
69
+ raise ValueError("Repo registration must be a TOML table.")
70
+ return RepoRegistration(
71
+ repo_state_version=int(payload.get("repo_state_version") or 0),
72
+ repo_id=_required_str(payload, "repo_id"),
73
+ identity_strength=_required_str(payload, "identity_strength"),
74
+ git_root=_optional_str(payload.get("git_root")),
75
+ source_remote=_optional_str(payload.get("source_remote")),
76
+ registered_at=_required_str(payload, "registered_at"),
77
+ machine_instance_id=_required_str(payload, "machine_instance_id"),
78
+ claude_status=_required_str(payload, "claude_status"),
79
+ claude_settings_path=_optional_str(payload.get("claude_settings_path")),
80
+ claude_note=_optional_str(payload.get("claude_note")),
81
+ )
82
+
83
+
84
+ def save_repo_registration(registration: RepoRegistration, repo_root: Path) -> Path:
85
+ """Persist one repo registration."""
86
+
87
+ path = repo_registration_path(repo_root)
88
+ path.parent.mkdir(parents=True, exist_ok=True)
89
+ lines = [
90
+ f"repo_state_version = {registration.repo_state_version}",
91
+ f"repo_id = {json.dumps(registration.repo_id)}",
92
+ f"identity_strength = {json.dumps(registration.identity_strength)}",
93
+ f"git_root = {json.dumps(registration.git_root or '')}",
94
+ f"source_remote = {json.dumps(registration.source_remote or '')}",
95
+ f"registered_at = {json.dumps(registration.registered_at)}",
96
+ f"machine_instance_id = {json.dumps(registration.machine_instance_id)}",
97
+ f"claude_status = {json.dumps(registration.claude_status)}",
98
+ f"claude_settings_path = {json.dumps(registration.claude_settings_path or '')}",
99
+ f"claude_note = {json.dumps(registration.claude_note or '')}",
100
+ "",
101
+ ]
102
+ path.write_text("\n".join(lines), encoding="utf-8")
103
+ return path
104
+
105
+
106
+ def register_repo(
107
+ *,
108
+ repo_root: Path,
109
+ machine_instance_id: str,
110
+ explicit_repo_id: str | None = None,
111
+ claude_status: str = "not_checked",
112
+ claude_settings_path: str | None = None,
113
+ claude_note: str | None = None,
114
+ ) -> RepoRegistration:
115
+ """Resolve and persist one repo registration."""
116
+
117
+ identity = resolve_repo_identity(repo_root=repo_root, explicit_repo_id=explicit_repo_id)
118
+ registration = RepoRegistration(
119
+ repo_state_version=REPO_STATE_VERSION,
120
+ repo_id=identity.repo_id,
121
+ identity_strength=identity.identity_strength,
122
+ git_root=identity.git_root,
123
+ source_remote=identity.source_remote,
124
+ registered_at=datetime.now(timezone.utc).isoformat(),
125
+ machine_instance_id=machine_instance_id,
126
+ claude_status=claude_status,
127
+ claude_settings_path=claude_settings_path,
128
+ claude_note=claude_note,
129
+ )
130
+ save_repo_registration(registration, repo_root)
131
+ return registration
132
+
133
+
134
+ def resolve_repo_identity(*, repo_root: Path, explicit_repo_id: str | None = None) -> RepoIdentity:
135
+ """Resolve repo identity using explicit override, git remotes, or weak local fallback."""
136
+
137
+ target = Path(repo_root).resolve()
138
+ if explicit_repo_id:
139
+ git_root = resolve_git_root(target)
140
+ return RepoIdentity(
141
+ repo_id=explicit_repo_id,
142
+ identity_strength=IDENTITY_STRENGTH_EXPLICIT,
143
+ git_root=str(git_root) if git_root is not None else None,
144
+ source_remote=None,
145
+ )
146
+ git_root = resolve_git_root(target)
147
+ if git_root is not None:
148
+ remotes = list_git_remotes(git_root)
149
+ if "origin" in remotes:
150
+ return RepoIdentity(
151
+ repo_id=normalize_git_remote(remotes["origin"]),
152
+ identity_strength=IDENTITY_STRENGTH_GIT_REMOTE,
153
+ git_root=str(git_root),
154
+ source_remote="origin",
155
+ )
156
+ if len(remotes) == 1:
157
+ remote_name, remote_url = next(iter(remotes.items()))
158
+ return RepoIdentity(
159
+ repo_id=normalize_git_remote(remote_url),
160
+ identity_strength=IDENTITY_STRENGTH_GIT_REMOTE,
161
+ git_root=str(git_root),
162
+ source_remote=remote_name,
163
+ )
164
+ if len(remotes) > 1:
165
+ raise ValueError(
166
+ "Multiple git remotes are configured and none is named origin. Rerun with --repo-id to choose a durable Shellbrain repo identity."
167
+ )
168
+ return RepoIdentity(
169
+ repo_id=f"{target.name}::{_weak_local_hash(target)}",
170
+ identity_strength=IDENTITY_STRENGTH_WEAK_LOCAL,
171
+ git_root=str(git_root) if git_root is not None else None,
172
+ source_remote=None,
173
+ )
174
+
175
+
176
+ def resolve_git_root(repo_root: Path) -> Path | None:
177
+ """Return the git root when the target directory is inside one repository."""
178
+
179
+ try:
180
+ completed = subprocess.run(
181
+ ["git", "rev-parse", "--show-toplevel"],
182
+ cwd=repo_root,
183
+ capture_output=True,
184
+ text=True,
185
+ check=False,
186
+ )
187
+ except FileNotFoundError:
188
+ return None
189
+ if completed.returncode != 0:
190
+ return None
191
+ output = completed.stdout.strip()
192
+ if not output:
193
+ return None
194
+ return Path(output).expanduser().resolve()
195
+
196
+
197
+ def list_git_remotes(repo_root: Path) -> dict[str, str]:
198
+ """Return fetch remotes keyed by remote name."""
199
+
200
+ try:
201
+ completed = subprocess.run(
202
+ ["git", "remote", "-v"],
203
+ cwd=repo_root,
204
+ capture_output=True,
205
+ text=True,
206
+ check=False,
207
+ )
208
+ except FileNotFoundError:
209
+ return {}
210
+ if completed.returncode != 0:
211
+ return {}
212
+ remotes: dict[str, str] = {}
213
+ for line in completed.stdout.splitlines():
214
+ parts = line.split()
215
+ if len(parts) < 3 or parts[2] != "(fetch)":
216
+ continue
217
+ remotes.setdefault(parts[0], parts[1])
218
+ return remotes
219
+
220
+
221
+ def normalize_git_remote(url: str) -> str:
222
+ """Normalize one git remote into a stable host/owner/repo identity."""
223
+
224
+ value = url.strip()
225
+ if not value:
226
+ raise ValueError("Git remote URL must not be empty.")
227
+ scp_match = re.match(r"^(?:[^@]+@)?([^:]+):(.+)$", value)
228
+ if scp_match and "://" not in value:
229
+ host = scp_match.group(1).lower()
230
+ path = scp_match.group(2)
231
+ else:
232
+ from urllib.parse import urlparse
233
+
234
+ parsed = urlparse(value)
235
+ host = (parsed.hostname or "").lower()
236
+ path = parsed.path.lstrip("/")
237
+ normalized_path = path.removesuffix(".git").strip("/")
238
+ if not host or not normalized_path:
239
+ raise ValueError(f"Unsupported git remote URL: {url}")
240
+ return f"{host}/{normalized_path}"
241
+
242
+
243
+ def _required_str(payload: dict[str, Any], key: str) -> str:
244
+ """Return a required string field."""
245
+
246
+ value = payload.get(key)
247
+ if not isinstance(value, str) or not value:
248
+ raise ValueError(f"Repo registration field {key!r} must be a non-empty string.")
249
+ return value
250
+
251
+
252
+ def _optional_str(value: Any) -> str | None:
253
+ """Return an optional string field."""
254
+
255
+ if value is None:
256
+ return None
257
+ if not isinstance(value, str):
258
+ raise ValueError("Optional repo registration fields must be strings when present.")
259
+ return value or None
260
+
261
+
262
+ def _weak_local_hash(path: Path) -> str:
263
+ """Return one short weak-local identity suffix."""
264
+
265
+ digest = hashlib.sha256(str(path.resolve()).encode("utf-8")).hexdigest()
266
+ return digest[:12]
@@ -0,0 +1,30 @@
1
+ """Restore helpers for Shellbrain logical backups."""
2
+
3
+ from pathlib import Path
4
+
5
+ from app.periphery.admin.backup import BackupManifest, restore_backup as _restore_backup
6
+
7
+
8
+ def restore_backup(
9
+ *,
10
+ admin_dsn: str,
11
+ backup_root: Path,
12
+ target_db: str,
13
+ app_dsn: str | None = None,
14
+ backup_id: str | None = None,
15
+ container_name: str | None = None,
16
+ container_admin_user: str | None = None,
17
+ container_admin_password: str | None = None,
18
+ ) -> BackupManifest:
19
+ """Restore one backup into a fresh scratch database."""
20
+
21
+ return _restore_backup(
22
+ admin_dsn=admin_dsn,
23
+ backup_root=backup_root,
24
+ target_db=target_db,
25
+ app_dsn=app_dsn,
26
+ backup_id=backup_id,
27
+ container_name=container_name,
28
+ container_admin_user=container_admin_user,
29
+ container_admin_password=container_admin_password,
30
+ )
@@ -0,0 +1 @@
1
+ """This package contains CLI entry points and payload adaptation helpers."""