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,132 @@
1
+ """This module defines explicit and implicit expansion stage helpers for read policy."""
2
+
3
+ from typing import Any
4
+
5
+ from app.boot.read_policy import resolve_read_payload_defaults
6
+ from app.core.interfaces.repos import IReadPolicyRepo, ISemanticRetrievalRepo
7
+
8
+
9
+ def expand_candidates(
10
+ direct_candidates: list[dict[str, Any]],
11
+ payload: dict[str, Any],
12
+ *,
13
+ read_policy: IReadPolicyRepo,
14
+ semantic_retrieval: ISemanticRetrievalRepo,
15
+ ) -> dict[str, list[dict[str, Any]]]:
16
+ """This function expands direct candidates via explicit links and semantic neighbors."""
17
+
18
+ payload = resolve_read_payload_defaults(payload)
19
+ explicit: list[dict[str, Any]] = []
20
+ implicit: list[dict[str, Any]] = []
21
+ expand = payload["expand"]
22
+ repo_id = payload["repo_id"]
23
+ include_global = bool(payload["include_global"])
24
+ kinds = payload.get("kinds")
25
+ min_strength = float(expand["min_association_strength"])
26
+ semantic_hops = int(expand["semantic_hops"])
27
+ max_association_depth = int(expand["max_association_depth"])
28
+
29
+ for direct_candidate in direct_candidates:
30
+ anchor_memory_id = direct_candidate["memory_id"]
31
+ anchor_score = float(direct_candidate.get("rrf_score", direct_candidate.get("score", 0.0)))
32
+
33
+ if expand["include_problem_links"]:
34
+ for neighbor in read_policy.list_problem_attempt_neighbors(
35
+ repo_id=repo_id,
36
+ include_global=include_global,
37
+ anchor_memory_id=anchor_memory_id,
38
+ kinds=kinds,
39
+ ):
40
+ explicit.append(
41
+ {
42
+ "memory_id": neighbor["memory_id"],
43
+ "anchor_memory_id": anchor_memory_id,
44
+ "anchor_score": anchor_score,
45
+ "depth": 1,
46
+ "expansion_type": neighbor["expansion_type"],
47
+ }
48
+ )
49
+
50
+ if expand["include_fact_update_links"]:
51
+ for neighbor in read_policy.list_fact_update_neighbors(
52
+ repo_id=repo_id,
53
+ include_global=include_global,
54
+ anchor_memory_id=anchor_memory_id,
55
+ kinds=kinds,
56
+ ):
57
+ explicit.append(
58
+ {
59
+ "memory_id": neighbor["memory_id"],
60
+ "anchor_memory_id": anchor_memory_id,
61
+ "anchor_score": anchor_score,
62
+ "depth": 1,
63
+ "expansion_type": neighbor["expansion_type"],
64
+ }
65
+ )
66
+
67
+ if expand["include_association_links"] and max_association_depth > 0:
68
+ seen_association_memory_ids = {str(anchor_memory_id)}
69
+ association_frontier = [str(anchor_memory_id)]
70
+ for depth in range(1, max_association_depth + 1):
71
+ next_association_frontier: list[str] = []
72
+ for frontier_memory_id in association_frontier:
73
+ for neighbor in read_policy.list_association_neighbors(
74
+ repo_id=repo_id,
75
+ include_global=include_global,
76
+ anchor_memory_id=frontier_memory_id,
77
+ kinds=kinds,
78
+ min_strength=min_strength,
79
+ ):
80
+ neighbor_memory_id = str(neighbor["memory_id"])
81
+ if neighbor_memory_id in seen_association_memory_ids:
82
+ continue
83
+ seen_association_memory_ids.add(neighbor_memory_id)
84
+ next_association_frontier.append(neighbor_memory_id)
85
+ explicit.append(
86
+ {
87
+ "memory_id": neighbor_memory_id,
88
+ "anchor_memory_id": anchor_memory_id,
89
+ "anchor_score": anchor_score,
90
+ "depth": depth,
91
+ "expansion_type": neighbor["expansion_type"],
92
+ "relation_strength": float(neighbor["strength"]),
93
+ "relation_type": neighbor["relation_type"],
94
+ }
95
+ )
96
+ association_frontier = next_association_frontier
97
+ if not association_frontier:
98
+ break
99
+
100
+ if semantic_hops > 0:
101
+ seen_memory_ids = {str(anchor_memory_id)}
102
+ frontier = [str(anchor_memory_id)]
103
+ for hop in range(1, semantic_hops + 1):
104
+ next_frontier: list[str] = []
105
+ for frontier_memory_id in frontier:
106
+ for neighbor in semantic_retrieval.list_semantic_neighbors(
107
+ repo_id=repo_id,
108
+ include_global=include_global,
109
+ anchor_memory_id=frontier_memory_id,
110
+ kinds=kinds,
111
+ limit=payload.get("limit"),
112
+ ):
113
+ neighbor_memory_id = str(neighbor["memory_id"])
114
+ if neighbor_memory_id in seen_memory_ids:
115
+ continue
116
+ seen_memory_ids.add(neighbor_memory_id)
117
+ next_frontier.append(neighbor_memory_id)
118
+ implicit.append(
119
+ {
120
+ "memory_id": neighbor_memory_id,
121
+ "anchor_memory_id": anchor_memory_id,
122
+ "anchor_score": anchor_score,
123
+ "hop": hop,
124
+ "expansion_type": "semantic_neighbor",
125
+ "neighbor_similarity": float(neighbor["score"]),
126
+ }
127
+ )
128
+ frontier = next_frontier
129
+ if not frontier:
130
+ break
131
+
132
+ return {"explicit": explicit, "implicit": implicit}
@@ -0,0 +1,34 @@
1
+ """This module defines reciprocal-rank fusion helpers for direct seed ranking."""
2
+
3
+ from typing import Any
4
+
5
+ from app.boot.retrieval import get_retrieval_defaults
6
+
7
+
8
+ def fuse_with_rrf(semantic: list[dict[str, Any]], keyword: list[dict[str, Any]]) -> list[dict[str, Any]]:
9
+ """This function merges lane candidates using reciprocal-rank fusion."""
10
+
11
+ defaults = get_retrieval_defaults()
12
+ k_rrf = defaults["k_rrf"]
13
+ lane_weights = {
14
+ "semantic": defaults["semantic_weight"],
15
+ "keyword": defaults["keyword_weight"],
16
+ }
17
+ fused: dict[str, dict[str, Any]] = {}
18
+
19
+ for lane_name, candidates in (("semantic", semantic), ("keyword", keyword)):
20
+ for rank, candidate in enumerate(candidates, start=1):
21
+ memory_id = candidate["memory_id"]
22
+ entry = fused.setdefault(
23
+ memory_id,
24
+ {
25
+ "memory_id": memory_id,
26
+ "rrf_score": 0.0,
27
+ "rank_semantic": None,
28
+ "rank_keyword": None,
29
+ },
30
+ )
31
+ entry[f"rank_{lane_name}"] = rank
32
+ entry["rrf_score"] += lane_weights[lane_name] / (k_rrf + rank)
33
+
34
+ return sorted(fused.values(), key=lambda item: (-float(item["rrf_score"]), str(item["memory_id"])))
@@ -0,0 +1,101 @@
1
+ """Helpers for constructing normalized lexical queries and document terms."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass
7
+
8
+
9
+ _TOKEN_PATTERN = re.compile(r"[a-z0-9]+")
10
+ _STOPWORDS = frozenset(
11
+ {
12
+ "a",
13
+ "an",
14
+ "and",
15
+ "are",
16
+ "as",
17
+ "at",
18
+ "be",
19
+ "before",
20
+ "by",
21
+ "for",
22
+ "from",
23
+ "in",
24
+ "into",
25
+ "is",
26
+ "it",
27
+ "of",
28
+ "on",
29
+ "or",
30
+ "that",
31
+ "the",
32
+ "these",
33
+ "this",
34
+ "those",
35
+ "to",
36
+ "with",
37
+ "without",
38
+ }
39
+ )
40
+
41
+
42
+ @dataclass(frozen=True, slots=True)
43
+ class LexicalQuery:
44
+ """Normalized lexical query terms used by the keyword retrieval lane."""
45
+
46
+ raw_terms: tuple[str, ...]
47
+ informative_terms: tuple[str, ...]
48
+ terms: tuple[str, ...]
49
+ uses_stopword_fallback: bool
50
+
51
+
52
+ @dataclass(frozen=True, slots=True)
53
+ class NormalizedLexicalText:
54
+ """Normalized raw and informative token views for one text payload."""
55
+
56
+ raw_terms: tuple[str, ...]
57
+ informative_terms: tuple[str, ...]
58
+
59
+ def terms_for(self, query: LexicalQuery) -> tuple[str, ...]:
60
+ """Return the document term view aligned to the lexical query mode."""
61
+
62
+ if query.uses_stopword_fallback:
63
+ return self.raw_terms
64
+ return self.informative_terms
65
+
66
+
67
+ def build_lexical_query(query_text: str) -> LexicalQuery:
68
+ """Build a normalized lexical query with strict informative-term semantics."""
69
+
70
+ normalized = normalize_lexical_text(query_text)
71
+ raw_terms = _unique_terms(normalized.raw_terms)
72
+ informative_terms = _unique_terms(normalized.informative_terms)
73
+ uses_stopword_fallback = not informative_terms and bool(raw_terms)
74
+ terms = raw_terms if uses_stopword_fallback else informative_terms
75
+ return LexicalQuery(
76
+ raw_terms=raw_terms,
77
+ informative_terms=informative_terms,
78
+ terms=terms,
79
+ uses_stopword_fallback=uses_stopword_fallback,
80
+ )
81
+
82
+
83
+ def normalize_lexical_text(text: str) -> NormalizedLexicalText:
84
+ """Normalize text into raw and informative lexical token sequences."""
85
+
86
+ raw_terms = tuple(_TOKEN_PATTERN.findall(text.lower()))
87
+ informative_terms = tuple(term for term in raw_terms if term not in _STOPWORDS)
88
+ return NormalizedLexicalText(raw_terms=raw_terms, informative_terms=informative_terms)
89
+
90
+
91
+ def _unique_terms(terms: tuple[str, ...]) -> tuple[str, ...]:
92
+ """Deduplicate terms while preserving original order."""
93
+
94
+ seen: set[str] = set()
95
+ ordered: list[str] = []
96
+ for term in terms:
97
+ if term in seen:
98
+ continue
99
+ seen.add(term)
100
+ ordered.append(term)
101
+ return tuple(ordered)
@@ -0,0 +1,93 @@
1
+ """This module defines read-policy pipeline orchestration for context-pack generation."""
2
+
3
+ from typing import Any
4
+
5
+ from app.boot.read_policy import resolve_read_payload_defaults
6
+ from app.core.interfaces.repos import IKeywordRetrievalRepo, IMemoriesRepo, IReadPolicyRepo, ISemanticRetrievalRepo
7
+ from app.core.interfaces.retrieval import IVectorSearch
8
+ from app.core.policies.read_policy.context_pack_builder import assemble_context_pack
9
+ from app.core.policies.read_policy.expansion import expand_candidates
10
+ from app.core.policies.read_policy.fusion_rrf import fuse_with_rrf
11
+ from app.core.policies.read_policy.scenario_lift import derive_scenarios
12
+ from app.core.policies.read_policy.scoring import score_candidates
13
+ from app.core.policies.read_policy.seed_retrieval import retrieve_seeds
14
+ from app.core.policies.read_policy.utility_prior import apply_utility_prior
15
+
16
+
17
+ def build_context_pack(
18
+ payload: dict[str, Any],
19
+ *,
20
+ keyword_retrieval: IKeywordRetrievalRepo,
21
+ memories: IMemoriesRepo,
22
+ semantic_retrieval: ISemanticRetrievalRepo,
23
+ read_policy: IReadPolicyRepo,
24
+ vector_search: IVectorSearch | None,
25
+ ) -> dict[str, Any]:
26
+ """This function orchestrates ratified read-policy stages into a final pack."""
27
+
28
+ payload = _resolve_read_defaults(payload)
29
+ seeds = retrieve_seeds(
30
+ payload,
31
+ semantic_retrieval=semantic_retrieval,
32
+ keyword_retrieval=keyword_retrieval,
33
+ vector_search=vector_search,
34
+ )
35
+ direct_candidates = fuse_with_rrf(seeds["semantic"], seeds["keyword"])
36
+ expanded_candidates = expand_candidates(
37
+ direct_candidates,
38
+ payload,
39
+ read_policy=read_policy,
40
+ semantic_retrieval=semantic_retrieval,
41
+ )
42
+ bucketed_candidates = {
43
+ "direct": direct_candidates,
44
+ "explicit": expanded_candidates["explicit"],
45
+ "implicit": expanded_candidates["implicit"],
46
+ }
47
+ scored_candidates = score_candidates(bucketed_candidates, payload)
48
+ adjusted_candidates = {
49
+ bucket_name: apply_utility_prior(candidates, payload)
50
+ for bucket_name, candidates in scored_candidates.items()
51
+ }
52
+ pack = assemble_context_pack(adjusted_candidates, payload)
53
+ hydrated_pack = _hydrate_pack_items(pack, memories)
54
+ return derive_scenarios(hydrated_pack, payload)["pack"]
55
+
56
+
57
+ def _resolve_read_defaults(payload: dict[str, Any]) -> dict[str, Any]:
58
+ """Resolve mode-based read defaults for callers that omit a limit."""
59
+
60
+ return resolve_read_payload_defaults(payload)
61
+
62
+
63
+ def _hydrate_pack_items(pack: dict[str, Any], memories: IMemoriesRepo) -> dict[str, Any]:
64
+ """Fill any missing display fields on selected pack items from the memories repository."""
65
+
66
+ missing_memory_ids: list[str] = []
67
+ seen_memory_ids: set[str] = set()
68
+ for section_name in ("direct", "explicit_related", "implicit_related"):
69
+ for item in pack[section_name]:
70
+ memory_id = str(item["memory_id"])
71
+ if ("kind" not in item or "text" not in item) and memory_id not in seen_memory_ids:
72
+ seen_memory_ids.add(memory_id)
73
+ missing_memory_ids.append(memory_id)
74
+
75
+ if missing_memory_ids:
76
+ hydrated_memories = {
77
+ memory.id: memory
78
+ for memory in memories.list_by_ids(missing_memory_ids)
79
+ }
80
+ for section_name in ("direct", "explicit_related", "implicit_related"):
81
+ for item in pack[section_name]:
82
+ if "kind" in item and "text" in item:
83
+ continue
84
+ memory_id = str(item["memory_id"])
85
+ memory = hydrated_memories.get(memory_id)
86
+ if memory is None:
87
+ raise ValueError(f"Missing hydrated memory for context-pack item: {memory_id}")
88
+ item.setdefault("kind", memory.kind.value)
89
+ item.setdefault("text", memory.text)
90
+ if "kind" not in item or "text" not in item:
91
+ raise ValueError(f"Incomplete hydrated memory for context-pack item: {memory_id}")
92
+
93
+ return pack
@@ -0,0 +1,11 @@
1
+ """This module defines scenario-lift stubs for deriving scenario abstractions from matches."""
2
+
3
+ from typing import Any
4
+
5
+
6
+ def derive_scenarios(pack: dict[str, Any], payload: dict[str, Any]) -> dict[str, Any]:
7
+ """This function derives and ranks scenario projections from selected members."""
8
+
9
+ # TODO: Implement scenario projection schema once ratified fields are finalized.
10
+ _ = payload
11
+ return {"scenarios": [], "pack": pack}
@@ -0,0 +1,61 @@
1
+ """This module defines scoring helpers for explicit and implicit read candidates."""
2
+
3
+ from typing import Any
4
+
5
+
6
+ def score_candidates(candidates: dict[str, list[dict[str, Any]]], payload: dict[str, Any]) -> dict[str, list[dict[str, Any]]]:
7
+ """This function computes base scores used for bucket ranking and spillover."""
8
+
9
+ _ = payload
10
+ return {
11
+ "direct": _score_direct_candidates(candidates.get("direct", [])),
12
+ "explicit": _score_explicit_candidates(candidates.get("explicit", [])),
13
+ "implicit": _score_implicit_candidates(candidates.get("implicit", [])),
14
+ }
15
+
16
+
17
+ def _score_direct_candidates(candidates: list[dict[str, Any]]) -> list[dict[str, Any]]:
18
+ """Assign direct-bucket scores from reciprocal-rank fusion output."""
19
+
20
+ scored = []
21
+ for candidate in candidates:
22
+ item = dict(candidate)
23
+ item["score"] = float(candidate.get("rrf_score", candidate.get("score", 0.0)))
24
+ scored.append(item)
25
+ return _sort_scored_candidates(scored)
26
+
27
+
28
+ def _score_explicit_candidates(candidates: list[dict[str, Any]]) -> list[dict[str, Any]]:
29
+ """Assign explicit-bucket scores from anchor relevance and explicit-link metadata."""
30
+
31
+ scored = []
32
+ for candidate in candidates:
33
+ item = dict(candidate)
34
+ anchor_score = float(candidate.get("anchor_score", candidate.get("score", 0.0)))
35
+ depth = max(1, int(candidate.get("depth", 1)))
36
+ relation_strength = 1.0
37
+ if candidate.get("expansion_type") == "association":
38
+ relation_strength = float(candidate.get("relation_strength", 1.0))
39
+ item["score"] = anchor_score * relation_strength / depth
40
+ scored.append(item)
41
+ return _sort_scored_candidates(scored)
42
+
43
+
44
+ def _score_implicit_candidates(candidates: list[dict[str, Any]]) -> list[dict[str, Any]]:
45
+ """Assign implicit-bucket scores from anchor relevance, similarity, and hop count."""
46
+
47
+ scored = []
48
+ for candidate in candidates:
49
+ item = dict(candidate)
50
+ anchor_score = float(candidate.get("anchor_score", candidate.get("score", 0.0)))
51
+ hop = max(1, int(candidate.get("hop", 1)))
52
+ neighbor_similarity = float(candidate.get("neighbor_similarity", 1.0))
53
+ item["score"] = anchor_score * neighbor_similarity / hop
54
+ scored.append(item)
55
+ return _sort_scored_candidates(scored)
56
+
57
+
58
+ def _sort_scored_candidates(candidates: list[dict[str, Any]]) -> list[dict[str, Any]]:
59
+ """Return candidates in deterministic descending score order."""
60
+
61
+ return sorted(candidates, key=lambda item: (-float(item["score"]), str(item["memory_id"])))
@@ -0,0 +1,54 @@
1
+ """This module defines semantic and keyword seed retrieval stage helpers."""
2
+
3
+ from typing import Any
4
+
5
+ from app.boot.thresholds import get_threshold_settings
6
+ from app.core.interfaces.repos import IKeywordRetrievalRepo, ISemanticRetrievalRepo
7
+ from app.core.interfaces.retrieval import IVectorSearch
8
+
9
+
10
+ def retrieve_seeds(
11
+ payload: dict[str, Any],
12
+ *,
13
+ semantic_retrieval: ISemanticRetrievalRepo,
14
+ keyword_retrieval: IKeywordRetrievalRepo,
15
+ vector_search: IVectorSearch | None,
16
+ ) -> dict[str, list[dict[str, Any]]]:
17
+ """This function retrieves initial semantic and keyword candidate seeds."""
18
+
19
+ repo_id = payload["repo_id"]
20
+ include_global = bool(payload["include_global"])
21
+ kinds = payload.get("kinds")
22
+ limit = int(payload["limit"])
23
+ query_text = payload["query"]
24
+ query_vector = (
25
+ list(vector_search.embed_query(query_text))
26
+ if vector_search is not None
27
+ else []
28
+ )
29
+ thresholds = get_threshold_settings()
30
+
31
+ semantic = [
32
+ candidate
33
+ for candidate in semantic_retrieval.query_semantic(
34
+ repo_id=repo_id,
35
+ include_global=include_global,
36
+ query_vector=query_vector,
37
+ kinds=kinds,
38
+ limit=limit,
39
+ )
40
+ if float(candidate["score"]) >= thresholds["semantic_threshold"]
41
+ ]
42
+ keyword = [
43
+ candidate
44
+ for candidate in keyword_retrieval.query_keyword(
45
+ repo_id=repo_id,
46
+ mode=payload["mode"],
47
+ include_global=include_global,
48
+ query_text=query_text,
49
+ kinds=kinds,
50
+ limit=limit,
51
+ )
52
+ if float(candidate["score"]) >= thresholds["keyword_threshold"]
53
+ ]
54
+ return {"semantic": semantic, "keyword": keyword}
@@ -0,0 +1,11 @@
1
+ """This module defines weak late-stage utility-prior adjustments for near-tie ordering."""
2
+
3
+ from typing import Any
4
+
5
+
6
+ def apply_utility_prior(candidates: list[dict[str, Any]], payload: dict[str, Any]) -> list[dict[str, Any]]:
7
+ """This function applies bounded utility adjustments only for near-tie candidates."""
8
+
9
+ # TODO: Implement u_shrunk and alpha_utility near-tie nudging.
10
+ _ = payload
11
+ return candidates
@@ -0,0 +1 @@
1
+ """This package defines deterministic update-policy planning and execution."""
@@ -0,0 +1,80 @@
1
+ """This module defines update-policy planning and execution helpers."""
2
+
3
+ from uuid import uuid4
4
+ from typing import Any
5
+
6
+ from app.core.entities.associations import AssociationSourceMode, AssociationState
7
+ from app.core.interfaces.unit_of_work import IUnitOfWork
8
+ from app.core.policies._shared.executor import apply_side_effects
9
+ from app.core.policies._shared.side_effects import make_side_effect
10
+
11
+
12
+ def build_update_plan(payload: dict[str, Any]) -> list[dict[str, Any]]:
13
+ """This function converts a validated update payload into deterministic side effects."""
14
+
15
+ update = payload["update"]
16
+ update_type = update["type"]
17
+ memory_id = payload["memory_id"]
18
+ repo_id = payload["repo_id"]
19
+
20
+ if update_type == "archive_state":
21
+ return [make_side_effect("memory.archive_state", {"memory_id": memory_id, "archived": update["archived"]})]
22
+
23
+ if update_type == "utility_vote":
24
+ return [
25
+ make_side_effect(
26
+ "utility_observation.append",
27
+ {
28
+ "id": str(uuid4()),
29
+ "memory_id": memory_id,
30
+ "problem_id": update["problem_id"],
31
+ "vote": update["vote"],
32
+ "rationale": update.get("rationale"),
33
+ },
34
+ )
35
+ ]
36
+
37
+ if update_type == "fact_update_link":
38
+ return [
39
+ make_side_effect(
40
+ "fact_update.create",
41
+ {
42
+ "id": str(uuid4()),
43
+ "old_fact_id": update["old_fact_id"],
44
+ "change_id": memory_id,
45
+ "new_fact_id": update["new_fact_id"],
46
+ },
47
+ )
48
+ ]
49
+
50
+ if update_type == "association_link":
51
+ confidence = update.get("confidence")
52
+ salience = update.get("salience")
53
+ return [
54
+ make_side_effect(
55
+ "association.upsert_and_observe",
56
+ {
57
+ "repo_id": repo_id,
58
+ "edge_id": str(uuid4()),
59
+ "from_memory_id": memory_id,
60
+ "to_memory_id": update["to_memory_id"],
61
+ "relation_type": update["relation_type"],
62
+ "source_mode": AssociationSourceMode.AGENT.value,
63
+ "state": AssociationState.TENTATIVE.value,
64
+ "strength": confidence if confidence is not None else 0.5,
65
+ "observation_id": str(uuid4()),
66
+ "observation_source": "agent_explicit",
67
+ "valence": confidence if confidence is not None else 0.5,
68
+ "salience": salience if salience is not None else 0.5,
69
+ "evidence_refs": list(update["evidence_refs"]),
70
+ },
71
+ )
72
+ ]
73
+
74
+ raise ValueError(f"Unsupported update type for plan build: {update_type}")
75
+
76
+
77
+ def apply_update_plan(plan: list[dict[str, Any]], uow: IUnitOfWork) -> None:
78
+ """This function executes a deterministic update plan inside one transaction."""
79
+
80
+ apply_side_effects(plan, uow)
@@ -0,0 +1 @@
1
+ """This package defines create, read, and update use-case entry points."""
@@ -0,0 +1,85 @@
1
+ """Core guidance rules derived from trusted session state and telemetry."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timedelta, timezone
6
+
7
+ from app.core.entities.guidance import GuidanceDecision
8
+ from app.core.entities.identity import CallerIdentity, IdentityTrustLevel
9
+ from app.core.entities.session_state import SessionState
10
+ from app.core.interfaces.repos import ITelemetryRepo
11
+
12
+
13
+ GUIDANCE_REMINDER_INTERVAL = timedelta(minutes=30)
14
+
15
+
16
+ def should_emit_guidance_reminder(
17
+ *,
18
+ guidance: GuidanceDecision,
19
+ last_guidance_problem_id: str | None,
20
+ last_guidance_at: str | None,
21
+ now_iso: str,
22
+ ) -> bool:
23
+ """Return whether one reminder should be emitted for the current active problem."""
24
+
25
+ if guidance.problem_id is None:
26
+ return True
27
+ if last_guidance_problem_id != guidance.problem_id or last_guidance_at is None:
28
+ return True
29
+ last_seen = _parse_iso(last_guidance_at)
30
+ now = _parse_iso(now_iso)
31
+ return now - last_seen >= GUIDANCE_REMINDER_INTERVAL
32
+
33
+
34
+ def build_pending_utility_guidance(
35
+ *,
36
+ repo_id: str,
37
+ caller_identity: CallerIdentity | None,
38
+ session_state: SessionState | None,
39
+ telemetry: ITelemetryRepo,
40
+ now_iso: str,
41
+ strong: bool = False,
42
+ ) -> list[GuidanceDecision]:
43
+ """Build pending utility-vote guidance for one trusted caller and active problem."""
44
+
45
+ if caller_identity is None or caller_identity.trust_level != IdentityTrustLevel.TRUSTED:
46
+ return []
47
+ if session_state is None or session_state.current_problem_id is None:
48
+ return []
49
+
50
+ candidates = telemetry.list_pending_utility_candidates(
51
+ repo_id=repo_id,
52
+ caller_id=caller_identity.canonical_id or "",
53
+ problem_id=session_state.current_problem_id,
54
+ since_iso=session_state.session_started_at,
55
+ )
56
+ if not candidates:
57
+ return []
58
+
59
+ decision = GuidanceDecision(
60
+ code="pending_utility_votes",
61
+ severity="info",
62
+ message=f"{len(candidates)} retrieved memories still need utility votes for the active problem.",
63
+ problem_id=session_state.current_problem_id,
64
+ memory_ids=[candidate.memory_id for candidate in candidates],
65
+ vote_scale_hint={"helpful": 1.0, "neutral": 0.0, "misleading": -1.0},
66
+ )
67
+ if strong:
68
+ return [decision]
69
+ if should_emit_guidance_reminder(
70
+ guidance=decision,
71
+ last_guidance_problem_id=session_state.last_guidance_problem_id,
72
+ last_guidance_at=session_state.last_guidance_at,
73
+ now_iso=now_iso,
74
+ ):
75
+ return [decision]
76
+ return []
77
+
78
+
79
+ def _parse_iso(value: str) -> datetime:
80
+ """Parse one ISO timestamp into a timezone-aware datetime."""
81
+
82
+ parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
83
+ if parsed.tzinfo is None:
84
+ return parsed.replace(tzinfo=timezone.utc)
85
+ return parsed.astimezone(timezone.utc)