thinkstack-core 4.0.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 (205) hide show
  1. thinkstack_core/__init__.py +158 -0
  2. thinkstack_core/aggphi_textual.py +275 -0
  3. thinkstack_core/alerts/__init__.py +23 -0
  4. thinkstack_core/alerts/base.py +46 -0
  5. thinkstack_core/alerts/config.py +60 -0
  6. thinkstack_core/alerts/dispatcher.py +110 -0
  7. thinkstack_core/alerts/jira.py +96 -0
  8. thinkstack_core/alerts/linear.py +72 -0
  9. thinkstack_core/alerts/pagerduty.py +66 -0
  10. thinkstack_core/alerts/slack.py +81 -0
  11. thinkstack_core/alerts/teams.py +70 -0
  12. thinkstack_core/audit/__init__.py +43 -0
  13. thinkstack_core/audit/exporter.py +297 -0
  14. thinkstack_core/audit/privacy.py +101 -0
  15. thinkstack_core/audit/scrubber.py +149 -0
  16. thinkstack_core/audit/service.py +67 -0
  17. thinkstack_core/audit/signing.py +127 -0
  18. thinkstack_core/broadcast/__init__.py +4 -0
  19. thinkstack_core/broadcast/broadcaster.py +100 -0
  20. thinkstack_core/broadcast/watcher.py +71 -0
  21. thinkstack_core/capability.py +639 -0
  22. thinkstack_core/cloud/__init__.py +1 -0
  23. thinkstack_core/cloud/client_config.py +472 -0
  24. thinkstack_core/cloud/client_configs/.claude-opencode-fallback.json +8 -0
  25. thinkstack_core/cloud/client_configs/.claude-stdio.json +13 -0
  26. thinkstack_core/cloud/client_configs/.cursor-mcp.json +13 -0
  27. thinkstack_core/cloud/client_configs/.opencode-bridge.json +13 -0
  28. thinkstack_core/cloud/client_configs/.opencode.json +15 -0
  29. thinkstack_core/cloud/client_configs/.vscode-mcp.json +13 -0
  30. thinkstack_core/cloud/mcp_client.py +229 -0
  31. thinkstack_core/cloud/setup.py +144 -0
  32. thinkstack_core/cloud/sync.py +143 -0
  33. thinkstack_core/cloud/sync_bundle.py +639 -0
  34. thinkstack_core/cloud/sync_conflicts.py +183 -0
  35. thinkstack_core/cloud/sync_state.py +159 -0
  36. thinkstack_core/cloud/team_sync.py +337 -0
  37. thinkstack_core/cloud/thinkstack-mcp-bridge.js +357 -0
  38. thinkstack_core/codex/__init__.py +9 -0
  39. thinkstack_core/codex/__main__.py +97 -0
  40. thinkstack_core/codex/capture.py +208 -0
  41. thinkstack_core/codex/proxy.py +412 -0
  42. thinkstack_core/compat.py +103 -0
  43. thinkstack_core/concept_catalog.py +209 -0
  44. thinkstack_core/consolidation/__init__.py +3 -0
  45. thinkstack_core/consolidation/synthesizer.py +87 -0
  46. thinkstack_core/consolidation/workflow.py +175 -0
  47. thinkstack_core/daemon/__init__.py +27 -0
  48. thinkstack_core/daemon/supervisor.py +293 -0
  49. thinkstack_core/daemon/watcher.py +244 -0
  50. thinkstack_core/dashboard_api.py +2012 -0
  51. thinkstack_core/deltaf.py +97 -0
  52. thinkstack_core/disclosure.py +50 -0
  53. thinkstack_core/divergence/__init__.py +3 -0
  54. thinkstack_core/divergence/detector.py +166 -0
  55. thinkstack_core/gateway/__init__.py +32 -0
  56. thinkstack_core/gateway/key_manager.py +124 -0
  57. thinkstack_core/gateway/metrics_webhook.py +252 -0
  58. thinkstack_core/gateway/policy.py +262 -0
  59. thinkstack_core/gateway/server.py +727 -0
  60. thinkstack_core/gateway/sso.py +233 -0
  61. thinkstack_core/gcc.py +1246 -0
  62. thinkstack_core/github/__init__.py +35 -0
  63. thinkstack_core/github/app.py +240 -0
  64. thinkstack_core/github/comment_builder.py +113 -0
  65. thinkstack_core/github/pat.py +76 -0
  66. thinkstack_core/github/pr_parser.py +82 -0
  67. thinkstack_core/github/pr_reporter.py +555 -0
  68. thinkstack_core/gitlab/__init__.py +177 -0
  69. thinkstack_core/hitl/__init__.py +4 -0
  70. thinkstack_core/hitl/channels.py +129 -0
  71. thinkstack_core/hitl/orchestrator.py +95 -0
  72. thinkstack_core/hooks/__init__.py +17 -0
  73. thinkstack_core/hooks/claude_code.py +228 -0
  74. thinkstack_core/hooks/git_capture.py +341 -0
  75. thinkstack_core/hooks/git_commit.py +182 -0
  76. thinkstack_core/hooks/installer.py +850 -0
  77. thinkstack_core/hooks/pre_commit.py +157 -0
  78. thinkstack_core/hooks/runner.py +386 -0
  79. thinkstack_core/identity/__init__.py +4 -0
  80. thinkstack_core/identity/agent.py +86 -0
  81. thinkstack_core/identity/providers.py +85 -0
  82. thinkstack_core/invariants.py +182 -0
  83. thinkstack_core/mcp/__init__.py +10 -0
  84. thinkstack_core/mcp/auth.py +177 -0
  85. thinkstack_core/mcp/server.py +1215 -0
  86. thinkstack_core/metrics/__init__.py +35 -0
  87. thinkstack_core/metrics/aggregate.py +215 -0
  88. thinkstack_core/metrics/calibrate.py +198 -0
  89. thinkstack_core/metrics/calibration.py +125 -0
  90. thinkstack_core/metrics/credibility.py +288 -0
  91. thinkstack_core/metrics/delivery_time.py +70 -0
  92. thinkstack_core/metrics/dhs.py +126 -0
  93. thinkstack_core/metrics/mcs.py +96 -0
  94. thinkstack_core/metrics/roi.py +88 -0
  95. thinkstack_core/metrics/session_writer.py +81 -0
  96. thinkstack_core/metrics/shadow_ai.py +117 -0
  97. thinkstack_core/metrics/sprint_writer.py +243 -0
  98. thinkstack_core/observability/__init__.py +78 -0
  99. thinkstack_core/observability/datadog.py +157 -0
  100. thinkstack_core/observability/formatter.py +119 -0
  101. thinkstack_core/observability/report.py +264 -0
  102. thinkstack_core/observability/servicenow.py +147 -0
  103. thinkstack_core/observability/splunk.py +218 -0
  104. thinkstack_core/observability/webhook.py +227 -0
  105. thinkstack_core/parser/__init__.py +30 -0
  106. thinkstack_core/parser/blocks.py +216 -0
  107. thinkstack_core/parser/inference.py +159 -0
  108. thinkstack_core/parser/thinking.py +112 -0
  109. thinkstack_core/projects.py +169 -0
  110. thinkstack_core/prompt_artifact.py +76 -0
  111. thinkstack_core/proxy/__init__.py +9 -0
  112. thinkstack_core/proxy/routes/__init__.py +1 -0
  113. thinkstack_core/proxy/routes/anthropic.py +264 -0
  114. thinkstack_core/proxy/routes/azure_openai.py +336 -0
  115. thinkstack_core/proxy/routes/gemini.py +331 -0
  116. thinkstack_core/proxy/routes/groq.py +284 -0
  117. thinkstack_core/proxy/routes/ollama.py +279 -0
  118. thinkstack_core/proxy/routes/openai.py +287 -0
  119. thinkstack_core/proxy/server.py +356 -0
  120. thinkstack_core/query/__init__.py +15 -0
  121. thinkstack_core/query/grep.py +181 -0
  122. thinkstack_core/query/hybrid.py +86 -0
  123. thinkstack_core/query/semantic.py +157 -0
  124. thinkstack_core/rdp.py +105 -0
  125. thinkstack_core/reasoning/__init__.py +4 -0
  126. thinkstack_core/reasoning/entry.py +31 -0
  127. thinkstack_core/reasoning/store.py +122 -0
  128. thinkstack_core/reasoning_plus/__init__.py +70 -0
  129. thinkstack_core/reasoning_plus/augmenter.py +337 -0
  130. thinkstack_core/reasoning_plus/capture.py +51 -0
  131. thinkstack_core/reasoning_plus/config.py +313 -0
  132. thinkstack_core/reasoning_plus/context.py +262 -0
  133. thinkstack_core/reasoning_plus/learning/__init__.py +125 -0
  134. thinkstack_core/reasoning_plus/learning/analytics.py +141 -0
  135. thinkstack_core/reasoning_plus/learning/api.py +784 -0
  136. thinkstack_core/reasoning_plus/learning/chain.py +285 -0
  137. thinkstack_core/reasoning_plus/learning/composer.py +141 -0
  138. thinkstack_core/reasoning_plus/learning/conflicts.py +184 -0
  139. thinkstack_core/reasoning_plus/learning/context_collector.py +194 -0
  140. thinkstack_core/reasoning_plus/learning/cross_project.py +234 -0
  141. thinkstack_core/reasoning_plus/learning/deny_list.py +108 -0
  142. thinkstack_core/reasoning_plus/learning/embeddings.py +209 -0
  143. thinkstack_core/reasoning_plus/learning/evolution.py +119 -0
  144. thinkstack_core/reasoning_plus/learning/extractor.py +271 -0
  145. thinkstack_core/reasoning_plus/learning/filter_five_layer.py +95 -0
  146. thinkstack_core/reasoning_plus/learning/models.py +149 -0
  147. thinkstack_core/reasoning_plus/learning/org_store.py +156 -0
  148. thinkstack_core/reasoning_plus/learning/pii.py +142 -0
  149. thinkstack_core/reasoning_plus/learning/promotion.py +58 -0
  150. thinkstack_core/reasoning_plus/learning/provenance.py +126 -0
  151. thinkstack_core/reasoning_plus/learning/recorder.py +81 -0
  152. thinkstack_core/reasoning_plus/learning/relevance.py +122 -0
  153. thinkstack_core/reasoning_plus/learning/state.py +86 -0
  154. thinkstack_core/reasoning_plus/learning/store.py +178 -0
  155. thinkstack_core/reasoning_plus/learning/theta_learning_bridge.py +94 -0
  156. thinkstack_core/reasoning_plus/prompt.py +90 -0
  157. thinkstack_core/rep.py +134 -0
  158. thinkstack_core/rep_network/__init__.py +25 -0
  159. thinkstack_core/rep_network/merge.py +70 -0
  160. thinkstack_core/rep_network/node.py +137 -0
  161. thinkstack_core/rep_network/server.py +140 -0
  162. thinkstack_core/rep_network/sync.py +207 -0
  163. thinkstack_core/sensitivity.py +182 -0
  164. thinkstack_core/serve.py +258 -0
  165. thinkstack_core/session/__init__.py +39 -0
  166. thinkstack_core/session/disagreement.py +188 -0
  167. thinkstack_core/session/models.py +114 -0
  168. thinkstack_core/session/orchestrator.py +182 -0
  169. thinkstack_core/session/planner.py +169 -0
  170. thinkstack_core/session/simulator.py +132 -0
  171. thinkstack_core/signing.py +290 -0
  172. thinkstack_core/sis.py +197 -0
  173. thinkstack_core/skills/pr-reviewer/SKILL.md +204 -0
  174. thinkstack_core/skills/thinkstack-auto-sync/SKILL.md +175 -0
  175. thinkstack_core/skills/thinkstack-session-start/SKILL.md +136 -0
  176. thinkstack_core/storage.py +308 -0
  177. thinkstack_core/templates/__init__.py +6 -0
  178. thinkstack_core/templates/engine.py +122 -0
  179. thinkstack_core/templates/go.py +18 -0
  180. thinkstack_core/templates/infra.py +19 -0
  181. thinkstack_core/templates/library/__init__.py +18 -0
  182. thinkstack_core/templates/library/api_design.md +27 -0
  183. thinkstack_core/templates/library/bug_fix.md +27 -0
  184. thinkstack_core/templates/library/decision_record.md +27 -0
  185. thinkstack_core/templates/library/engine.py +228 -0
  186. thinkstack_core/templates/library/security_review.md +30 -0
  187. thinkstack_core/templates/python.py +19 -0
  188. thinkstack_core/templates/react.py +18 -0
  189. thinkstack_core/templates/typescript.py +18 -0
  190. thinkstack_core/theta.py +221 -0
  191. thinkstack_core/theta_synthesis.py +268 -0
  192. thinkstack_core/topics.py +320 -0
  193. thinkstack_core/variance.py +219 -0
  194. thinkstack_core/wrapper/__init__.py +52 -0
  195. thinkstack_core/wrapper/anthropic.py +487 -0
  196. thinkstack_core/wrapper/base.py +562 -0
  197. thinkstack_core/wrapper/bedrock.py +342 -0
  198. thinkstack_core/wrapper/gemini.py +422 -0
  199. thinkstack_core/wrapper/ollama.py +527 -0
  200. thinkstack_core/wrapper/openai.py +461 -0
  201. thinkstack_core-4.0.0.dist-info/METADATA +868 -0
  202. thinkstack_core-4.0.0.dist-info/RECORD +205 -0
  203. thinkstack_core-4.0.0.dist-info/WHEEL +5 -0
  204. thinkstack_core-4.0.0.dist-info/entry_points.txt +2 -0
  205. thinkstack_core-4.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,81 @@
1
+ """
2
+ thinkstack_core.reasoning_plus.learning.recorder
3
+ ==============================================
4
+ Record reasoning calls (LLM, tool, memory, action) to .GCC/.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import logging
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from thinkstack_core.reasoning_plus.learning.models import ReasoningCall
14
+
15
+ logger = logging.getLogger("thinkstack.reasoning_plus.learning")
16
+
17
+
18
+ class CallRecorder:
19
+ """Persist ReasoningCall records under .GCC/reasoning_learnings/calls/."""
20
+
21
+ def __init__(self, gcc_dir: Path | str) -> None:
22
+ self._gcc_dir = Path(gcc_dir)
23
+ self._calls_dir = self._gcc_dir / "reasoning_learnings" / "calls"
24
+
25
+ def record(self, call: ReasoningCall) -> Path:
26
+ """Write a call to disk and return the path."""
27
+ self._calls_dir.mkdir(parents=True, exist_ok=True)
28
+ path = self._calls_dir / f"{call.id}.json"
29
+ try:
30
+ _atomic_json_write(path, call.to_dict())
31
+ except Exception as exc:
32
+ logger.warning("thinkstack: failed to record reasoning call — %s", exc)
33
+ return path
34
+
35
+ def get(self, call_id: str) -> ReasoningCall | None:
36
+ path = self._calls_dir / f"{call_id}.json"
37
+ if not path.exists():
38
+ return None
39
+ try:
40
+ data = json.loads(path.read_text(encoding="utf-8"))
41
+ return ReasoningCall.from_dict(data)
42
+ except Exception as exc:
43
+ logger.warning("thinkstack: failed to read reasoning call %s — %s", call_id, exc)
44
+ return None
45
+
46
+ def list(self, session_id: str | None = None, call_type: str | None = None) -> list[ReasoningCall]:
47
+ """List recorded calls, optionally filtered by session or type."""
48
+ out: list[ReasoningCall] = []
49
+ if not self._calls_dir.exists():
50
+ return out
51
+ for path in sorted(self._calls_dir.glob("*.json")):
52
+ try:
53
+ data = json.loads(path.read_text(encoding="utf-8"))
54
+ call = ReasoningCall.from_dict(data)
55
+ if session_id and call.session_id != session_id:
56
+ continue
57
+ if call_type and call.call_type != call_type:
58
+ continue
59
+ out.append(call)
60
+ except Exception as exc:
61
+ logger.debug("thinkstack: skipping malformed call record %s — %s", path, exc)
62
+ return out
63
+
64
+ def count(self) -> int:
65
+ if not self._calls_dir.exists():
66
+ return 0
67
+ return len(list(self._calls_dir.glob("*.json")))
68
+
69
+
70
+ def _atomic_json_write(path: Path, data: dict[str, Any]) -> None:
71
+ tmp = path.with_suffix(path.suffix + ".tmp")
72
+ try:
73
+ with open(tmp, "w", encoding="utf-8") as f:
74
+ json.dump(data, f, indent=2)
75
+ tmp.replace(path)
76
+ finally:
77
+ if tmp.exists():
78
+ try:
79
+ tmp.unlink()
80
+ except Exception:
81
+ pass
@@ -0,0 +1,122 @@
1
+ """
2
+ thinkstack_core.reasoning_plus.learning.relevance
3
+ =================================================
4
+ Rank learnings by relevance to the current context and state.
5
+
6
+ Scoring factors (two-dimensional):
7
+ 1. Semantic similarity between context and learning content (embedding)
8
+ 2. Concept overlap between context keywords and learning trigger_concepts
9
+ 3. Confidence
10
+ 4. State freshness (matching state_hash boosts score)
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import hashlib
15
+ import time
16
+ from typing import Any
17
+
18
+ from thinkstack_core.reasoning_plus.context import extract_keywords
19
+ from thinkstack_core.reasoning_plus.learning.embeddings import (
20
+ EmbeddingBackend,
21
+ KeywordEmbeddingBackend,
22
+ )
23
+ from thinkstack_core.reasoning_plus.learning.models import Learning
24
+
25
+
26
+ class RelevanceEngine:
27
+ """Score and rank learnings for a given context using an embedding backend."""
28
+
29
+ def __init__(self, backend: EmbeddingBackend | None = None, cache_ttl: float = 0) -> None:
30
+ self._backend = backend or KeywordEmbeddingBackend()
31
+ self._cache_ttl = cache_ttl
32
+ self._cache: dict[str, tuple[list[Learning], float]] = {}
33
+
34
+ def rank(
35
+ self,
36
+ context: str,
37
+ learnings: list[Learning],
38
+ current_state_hash: str | None = None,
39
+ top_n: int = 3,
40
+ ) -> list[Learning]:
41
+ """
42
+ Return the top-N active learnings ranked by relevance.
43
+
44
+ Scoring factors:
45
+ - semantic similarity between context and learning content
46
+ - concept overlap between context keywords and learning trigger_concepts
47
+ - confidence
48
+ - state freshness (matching state_hash boosts score)
49
+
50
+ Results are cached in memory when ``cache_ttl`` > 0 so repeated
51
+ similar prompts in the same session avoid recomputing embeddings.
52
+ """
53
+ cache_key = self._cache_key(context, learnings, current_state_hash, top_n)
54
+ if self._cache_ttl and cache_key:
55
+ cached = self._cache.get(cache_key)
56
+ if cached and (time.monotonic() - cached[1]) < self._cache_ttl:
57
+ return cached[0]
58
+
59
+ context_embedding = self._backend.encode(context)
60
+ context_keywords = set(extract_keywords(context, max_keywords=30))
61
+ scored: list[tuple[float, Learning]] = []
62
+
63
+ for learning in learnings:
64
+ if learning.validity != "active":
65
+ continue
66
+ score = self._score(
67
+ learning, context_embedding, current_state_hash, context_keywords,
68
+ )
69
+ scored.append((score, learning))
70
+
71
+ scored.sort(key=lambda x: x[0], reverse=True)
72
+ result = [learning for _, learning in scored[:top_n]]
73
+
74
+ if self._cache_ttl and cache_key:
75
+ self._cache[cache_key] = (result, time.monotonic())
76
+ return result
77
+
78
+ def _cache_key(
79
+ self,
80
+ context: str,
81
+ learnings: list[Learning],
82
+ current_state_hash: str | None,
83
+ top_n: int,
84
+ ) -> str:
85
+ """Stable key for the relevance cache."""
86
+ context_hash = hashlib.sha256(context.encode("utf-8")).hexdigest()[:16]
87
+ learning_ids = sorted({l.id for l in learnings})
88
+ learning_hash = hashlib.sha256(
89
+ "|".join(learning_ids).encode("utf-8")
90
+ ).hexdigest()[:16]
91
+ return f"{context_hash}:{current_state_hash or ''}:{learning_hash}:{top_n}"
92
+
93
+ def _score(
94
+ self,
95
+ learning: Learning,
96
+ context_embedding: Any,
97
+ current_state_hash: str | None,
98
+ context_keywords: set[str] | None = None,
99
+ ) -> float:
100
+ learning_embedding = self._backend.encode(learning.content)
101
+ semantic_score = self._backend.similarity(context_embedding, learning_embedding)
102
+
103
+ confidence_weight = learning.confidence
104
+
105
+ state_bonus = 0.0
106
+ if current_state_hash and learning.state_hash == current_state_hash:
107
+ state_bonus = 0.2
108
+
109
+ # Concept overlap: how much the learning's trigger_concepts overlap
110
+ # with keywords extracted from the current prompt context.
111
+ concept_bonus = 0.0
112
+ if context_keywords and learning.trigger_concepts:
113
+ learning_concepts = {c.lower() for c in learning.trigger_concepts}
114
+ overlap = len(context_keywords & learning_concepts)
115
+ if overlap > 0:
116
+ concept_bonus = min(0.3, overlap * 0.1)
117
+
118
+ return semantic_score + confidence_weight * 0.5 + state_bonus + concept_bonus
119
+
120
+ def invalidate_cache(self) -> None:
121
+ """Clear the relevance cache."""
122
+ self._cache.clear()
@@ -0,0 +1,86 @@
1
+ """
2
+ thinkstack_core.reasoning_plus.learning.state
3
+ ===========================================
4
+ Workspace state hashing and staleness detection for learnings.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ import subprocess
10
+ from pathlib import Path
11
+ from typing import Iterable
12
+
13
+ from thinkstack_core.reasoning_plus.learning.models import hash_state
14
+
15
+ logger = logging.getLogger("thinkstack.reasoning_plus.learning")
16
+
17
+
18
+ def workspace_state_hash(repo_path: Path | str, file_paths: Iterable[str] | None = None) -> str:
19
+ """
20
+ Return a short hash representing the current state of the workspace.
21
+
22
+ Strategy:
23
+ - If inside a git repo, include the current HEAD commit hash.
24
+ - For any provided file_paths, include the current file content hash.
25
+ - If not a git repo and no file paths, fall back to a hash of the directory path.
26
+ """
27
+ repo_path = Path(repo_path)
28
+ tokens: list[str] = []
29
+
30
+ head = _git_head(repo_path)
31
+ if head:
32
+ tokens.append(f"head:{head}")
33
+
34
+ for fp in file_paths or []:
35
+ content_hash = _file_content_hash(repo_path / fp)
36
+ if content_hash:
37
+ tokens.append(f"{fp}:{content_hash}")
38
+
39
+ if not tokens:
40
+ tokens.append(str(repo_path.resolve()))
41
+
42
+ return hash_state(*tokens)
43
+
44
+
45
+ def _git_head(repo_path: Path) -> str | None:
46
+ try:
47
+ result = subprocess.run(
48
+ ["git", "rev-parse", "HEAD"],
49
+ cwd=repo_path,
50
+ capture_output=True,
51
+ text=True,
52
+ errors="ignore",
53
+ timeout=5,
54
+ )
55
+ if result.returncode == 0:
56
+ return result.stdout.strip()
57
+ except Exception as exc:
58
+ logger.debug("thinkstack: could not read git HEAD — %s", exc)
59
+ return None
60
+
61
+
62
+ def _file_content_hash(path: Path) -> str | None:
63
+ if not path.exists():
64
+ return None
65
+ try:
66
+ return hash_state(path.read_text(encoding="utf-8", errors="ignore"))
67
+ except Exception as exc:
68
+ logger.debug("thinkstack: could not hash %s — %s", path, exc)
69
+ return None
70
+
71
+
72
+ def affected_by_state_change(repo_path: Path | str, learning_file_paths: Iterable[str], changed_paths: Iterable[str]) -> bool:
73
+ """
74
+ Return True if a learning should be re-evaluated because the workspace changed.
75
+
76
+ A learning is affected when any of the paths it references overlap with a changed path.
77
+ """
78
+ changed = set(changed_paths)
79
+ for fp in learning_file_paths:
80
+ if fp in changed:
81
+ return True
82
+ # Also invalidate if a parent directory was changed
83
+ for changed_path in changed:
84
+ if changed_path.startswith(fp) or fp.startswith(changed_path):
85
+ return True
86
+ return False
@@ -0,0 +1,178 @@
1
+ """
2
+ thinkstack_core.reasoning_plus.learning.store
3
+ ===========================================
4
+ Persistence layer for distilled learnings.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import logging
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from thinkstack_core.reasoning_plus.context import extract_keywords
14
+ from thinkstack_core.reasoning_plus.learning.models import Learning
15
+
16
+ logger = logging.getLogger("thinkstack.reasoning_plus.learning")
17
+
18
+
19
+ class LearningStore:
20
+ """Persist Learning records under .GCC/reasoning_learnings/learnings/."""
21
+
22
+ def __init__(self, gcc_dir: Path | str) -> None:
23
+ self._gcc_dir = Path(gcc_dir)
24
+ self._learnings_dir = self._gcc_dir / "reasoning_learnings" / "learnings"
25
+
26
+ def save(self, learning: Learning, deduplicate: bool = True) -> Path:
27
+ self._learnings_dir.mkdir(parents=True, exist_ok=True)
28
+ if deduplicate and learning.validity == "active":
29
+ duplicate = self._find_duplicate(learning)
30
+ if duplicate:
31
+ merged = self._merge(duplicate, learning)
32
+ path = self._learnings_dir / f"{merged.id}.json"
33
+ try:
34
+ _atomic_json_write(path, merged.to_dict())
35
+ except Exception as exc:
36
+ logger.warning("thinkstack: failed to save merged learning — %s", exc)
37
+ return path
38
+ path = self._learnings_dir / f"{learning.id}.json"
39
+ try:
40
+ _atomic_json_write(path, learning.to_dict())
41
+ except Exception as exc:
42
+ logger.warning("thinkstack: failed to save learning — %s", exc)
43
+ return path
44
+
45
+ def get(self, learning_id: str) -> Learning | None:
46
+ path = self._learnings_dir / f"{learning_id}.json"
47
+ if not path.exists():
48
+ return None
49
+ try:
50
+ data = json.loads(path.read_text(encoding="utf-8"))
51
+ return Learning.from_dict(data)
52
+ except Exception as exc:
53
+ logger.warning("thinkstack: failed to read learning %s — %s", learning_id, exc)
54
+ return None
55
+
56
+ def list(
57
+ self,
58
+ validity: str | None = None,
59
+ concept: str | None = None,
60
+ scope: str | None = None,
61
+ ) -> list[Learning]:
62
+ out: list[Learning] = []
63
+ if not self._learnings_dir.exists():
64
+ return out
65
+ for path in sorted(self._learnings_dir.glob("*.json")):
66
+ try:
67
+ data = json.loads(path.read_text(encoding="utf-8"))
68
+ learning = Learning.from_dict(data)
69
+ if validity and learning.validity != validity:
70
+ continue
71
+ if concept and concept.lower() not in {c.lower() for c in learning.trigger_concepts}:
72
+ continue
73
+ if scope and learning.scope != scope:
74
+ continue
75
+ out.append(learning)
76
+ except Exception as exc:
77
+ logger.debug("thinkstack: skipping malformed learning %s — %s", path, exc)
78
+ return out
79
+
80
+ def invalidate(self, learning_id: str, reason: str = "stale") -> Learning | None:
81
+ learning = self.get(learning_id)
82
+ if not learning:
83
+ return None
84
+ learning.validity = "stale" if reason == "stale" else "deprecated"
85
+ learning.updated_at = _now_iso()
86
+ self.save(learning)
87
+ return learning
88
+
89
+ def count(self) -> int:
90
+ if not self._learnings_dir.exists():
91
+ return 0
92
+ return len(list(self._learnings_dir.glob("*.json")))
93
+
94
+ # ------------------------------------------------------------------
95
+ # Deduplication
96
+ # ------------------------------------------------------------------
97
+
98
+ def _find_duplicate(self, learning: Learning, threshold: float = 0.8) -> Learning | None:
99
+ """Return the most similar *other* active learning if it reaches the threshold."""
100
+ best: Learning | None = None
101
+ best_score = threshold
102
+ for existing in self.list(validity="active"):
103
+ if existing.id == learning.id:
104
+ continue
105
+ score = self._similarity(existing, learning)
106
+ if score >= best_score:
107
+ best_score = score
108
+ best = existing
109
+ return best
110
+
111
+ def _similarity(self, a: Learning, b: Learning) -> float:
112
+ """Combined concept and content similarity in [0, 1].
113
+
114
+ Concept overlap is weighted more heavily because two learnings that share
115
+ the same trigger concepts are likely talking about the same situation.
116
+ """
117
+ a_concepts = {c.lower() for c in a.trigger_concepts}
118
+ b_concepts = {c.lower() for c in b.trigger_concepts}
119
+ concept_union = len(a_concepts | b_concepts)
120
+ concept_score = len(a_concepts & b_concepts) / concept_union if concept_union else 0.0
121
+
122
+ a_keywords = set(extract_keywords(a.content, max_keywords=15))
123
+ b_keywords = set(extract_keywords(b.content, max_keywords=15))
124
+ content_union = len(a_keywords | b_keywords)
125
+ content_score = len(a_keywords & b_keywords) / content_union if content_union else 0.0
126
+
127
+ return 0.8 * concept_score + 0.2 * content_score
128
+
129
+ def _merge(self, existing: Learning, new: Learning) -> Learning:
130
+ """Merge *new* into *existing*, keeping the existing stable ID."""
131
+ merged = Learning.from_dict(existing.to_dict())
132
+ merged.content = new.content
133
+ merged.trigger_concepts = sorted(
134
+ set(existing.trigger_concepts) | set(new.trigger_concepts)
135
+ )
136
+ merged.confidence = max(existing.confidence, new.confidence)
137
+ merged.source_reasoning_ids = list(
138
+ set(existing.source_reasoning_ids) | set(new.source_reasoning_ids)
139
+ )
140
+ merged.validity = _more_restrictive_validity(existing.validity, new.validity)
141
+ # Preserve evolution fields (S19) — union lists, prefer non-empty values.
142
+ merged.supersedes = sorted(set(existing.supersedes) | set(new.supersedes))
143
+ merged.superseded_by = new.superseded_by or existing.superseded_by
144
+ merged.evolution_chain = new.evolution_chain or existing.evolution_chain
145
+ merged.scope = _more_restrictive_scope(existing.scope, new.scope)
146
+ merged.updated_at = _now_iso()
147
+ return merged
148
+
149
+
150
+ def _atomic_json_write(path: Path, data: dict[str, Any]) -> None:
151
+ tmp = path.with_suffix(path.suffix + ".tmp")
152
+ try:
153
+ with open(tmp, "w", encoding="utf-8") as f:
154
+ json.dump(data, f, indent=2)
155
+ tmp.replace(path)
156
+ finally:
157
+ if tmp.exists():
158
+ try:
159
+ tmp.unlink()
160
+ except Exception:
161
+ pass
162
+
163
+
164
+ def _more_restrictive_validity(a: str, b: str) -> str:
165
+ """Return the more restrictive validity status."""
166
+ order = {"deprecated": 2, "stale": 1, "active": 0}
167
+ return a if order.get(a, 0) >= order.get(b, 0) else b
168
+
169
+
170
+ def _more_restrictive_scope(a: str, b: str) -> str:
171
+ """Return the more restrictive (narrower visibility) scope."""
172
+ order = {"branch": 2, "project": 1, "org": 0}
173
+ return a if order.get(a, 1) >= order.get(b, 1) else b
174
+
175
+
176
+ def _now_iso() -> str:
177
+ from datetime import datetime, timezone
178
+ return datetime.now(timezone.utc).isoformat()
@@ -0,0 +1,94 @@
1
+ """
2
+ thinkstack_core.reasoning_plus.learning.theta_learning_bridge
3
+ ============================================================
4
+ Bridge between DRPL learning outcomes and the coordination vector Θ.
5
+
6
+ Feeds per-concept success rates into ThetaStore.ripple() so that
7
+ learning outcomes influence the shared coordination vector.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from thinkstack_core.reasoning_plus.learning.store import LearningStore
16
+ from thinkstack_core.theta import make_theta_store
17
+
18
+ logger = logging.getLogger("thinkstack.reasoning_plus.learning")
19
+
20
+
21
+ def sync_learnings_to_theta(
22
+ gcc_dir: Path | str,
23
+ min_samples: int = 3,
24
+ disclosure: str = "PUBLIC",
25
+ ) -> dict[str, Any]:
26
+ """Compute per-concept success rates from learnings and feed them into Θ.
27
+
28
+ For each concept with at least *min_samples* learnings, creates a
29
+ sensitivity event with confidence = success_rate and feeds it through
30
+ ThetaStore.ripple(). This means concepts with high success rates
31
+ produce high-confidence events, and failing concepts produce low-confidence
32
+ signals in the coordination vector.
33
+
34
+ Args:
35
+ gcc_dir: Path to the local .GCC/ directory.
36
+ min_samples: Minimum learnings per concept to include.
37
+ disclosure: Disclosure level for generated events.
38
+
39
+ Returns:
40
+ A dict with keys:
41
+ - concepts_synced: number of concepts fed into theta
42
+ - events_generated: total events generated
43
+ - theta_concepts_after: concept count in theta after sync
44
+ """
45
+ from thinkstack_core.reasoning_plus.learning.analytics import concept_stats
46
+
47
+ store = LearningStore(gcc_dir)
48
+ stats = concept_stats(store)
49
+
50
+ events: list[dict] = []
51
+ concepts_synced = 0
52
+
53
+ for cname, data in stats.items():
54
+ if cname == "__untyped__":
55
+ continue
56
+ if data["total"] < min_samples:
57
+ continue
58
+ events.append({
59
+ "target_concept": cname,
60
+ "confidence": data["success_rate"],
61
+ "disclosure_level": disclosure,
62
+ "created_at": _now_iso(),
63
+ "source": "learning-analytics",
64
+ })
65
+ concepts_synced += 1
66
+
67
+ if not events:
68
+ return {
69
+ "concepts_synced": 0,
70
+ "events_generated": 0,
71
+ "theta_concepts_after": len(
72
+ make_theta_store(Path(gcc_dir)).load().get("coordination_vector", {})
73
+ ),
74
+ }
75
+
76
+ theta_store = make_theta_store(Path(gcc_dir))
77
+ theta_store.ripple(events)
78
+ updated = theta_store.load()
79
+ theta_count = len(updated.get("coordination_vector", {}))
80
+
81
+ logger.info(
82
+ "thinkstack: synced %d concept(s) into Θ (%d events) — theta now has %d concept(s)",
83
+ concepts_synced, len(events), theta_count,
84
+ )
85
+ return {
86
+ "concepts_synced": concepts_synced,
87
+ "events_generated": len(events),
88
+ "theta_concepts_after": theta_count,
89
+ }
90
+
91
+
92
+ def _now_iso() -> str:
93
+ from datetime import datetime, timezone
94
+ return datetime.now(timezone.utc).isoformat()
@@ -0,0 +1,90 @@
1
+ """
2
+ thinkstack_core.reasoning_plus.prompt
3
+ ===================================
4
+ Reasoning Plus prompt templates.
5
+
6
+ The default template is task-agnostic: it asks the model to reason step-by-step
7
+ inside <thinking> tags before producing the final answer. The SWE-Bench runner
8
+ uses a patch-specific override via the `task_prompt` parameter.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ DEFAULT_REASONING_DIRECTIVE = """You are a careful reasoning assistant.
13
+
14
+ Before giving your final answer, think step by step inside <thinking>...</thinking> tags.
15
+ Explain your reasoning, the relevant facts, and any trade-offs you considered.
16
+ Then provide the final answer outside the <thinking> block.
17
+
18
+ You MUST output the <thinking> block before the final answer.
19
+ """
20
+
21
+
22
+ PATCH_REASONING_DIRECTIVE = """You are an expert software engineer fixing a GitHub issue in a Python repository.
23
+
24
+ Your job is to produce a single, correct patch in unified diff format that the repository maintainers could apply with `git apply`.
25
+
26
+ Rules:
27
+ - Edit only the files needed to fix the issue.
28
+ - Use exact `git diff` style hunks: `--- a/<path>` and `+++ b/<path>` headers, `@@ -start,len +start,len @@` context lines.
29
+ - Context lines must match the original file exactly (indentation, spacing, content).
30
+ - Do not add line numbers, explanations, or markdown inside the patch.
31
+ - Do not output any text after `</patch>`.
32
+
33
+ Before writing the patch, you MUST think step by step inside <thinking>...</thinking> tags.
34
+ The thinking block should explain your analysis of the issue, the files that need to change, and the fix strategy.
35
+ Then write the patch between <patch>...</patch> tags.
36
+
37
+ You MUST output the <thinking> block before the <patch> block.
38
+
39
+ Example of the required output format:
40
+
41
+ <thinking>
42
+ 1. Root cause: the function X does not handle Y because Z.
43
+ 2. Files to edit: src/example.py
44
+ 3. Fix strategy: add a guard before the call to X.
45
+ </thinking>
46
+
47
+ <patch>
48
+ --- a/src/example.py
49
+ +++ b/src/example.py
50
+ @@ -10,7 +10,7 @@
51
+ def old_function():
52
+ x = 1
53
+ - return x
54
+ + return x + 1
55
+
56
+ def other_function():
57
+ pass
58
+ </patch>
59
+ """
60
+
61
+
62
+ def build_reasoning_plus_system_prompt(
63
+ base_prompt: str,
64
+ smart_context: str = "",
65
+ *,
66
+ require_thinking: bool = True,
67
+ task_prompt: str = "",
68
+ ) -> str:
69
+ """
70
+ Build a Reasoning Plus system prompt.
71
+
72
+ Args:
73
+ base_prompt: the caller's existing system prompt.
74
+ smart_context: optional markdown snippet of related files.
75
+ require_thinking: whether to append the thinking directive.
76
+ task_prompt: optional task-specific directive (e.g. the SWE-Bench patch prompt).
77
+ If empty, the generic task-agnostic directive is used.
78
+ """
79
+ parts = [base_prompt.strip()]
80
+
81
+ if smart_context:
82
+ parts.append("[ThinkStack smart context]")
83
+ parts.append(smart_context.strip())
84
+
85
+ if require_thinking:
86
+ directive = task_prompt.strip() if task_prompt else DEFAULT_REASONING_DIRECTIVE.strip()
87
+ parts.append("[ThinkStack reasoning directive]")
88
+ parts.append(directive)
89
+
90
+ return "\n\n".join(parts)