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.
- thinkstack_core/__init__.py +158 -0
- thinkstack_core/aggphi_textual.py +275 -0
- thinkstack_core/alerts/__init__.py +23 -0
- thinkstack_core/alerts/base.py +46 -0
- thinkstack_core/alerts/config.py +60 -0
- thinkstack_core/alerts/dispatcher.py +110 -0
- thinkstack_core/alerts/jira.py +96 -0
- thinkstack_core/alerts/linear.py +72 -0
- thinkstack_core/alerts/pagerduty.py +66 -0
- thinkstack_core/alerts/slack.py +81 -0
- thinkstack_core/alerts/teams.py +70 -0
- thinkstack_core/audit/__init__.py +43 -0
- thinkstack_core/audit/exporter.py +297 -0
- thinkstack_core/audit/privacy.py +101 -0
- thinkstack_core/audit/scrubber.py +149 -0
- thinkstack_core/audit/service.py +67 -0
- thinkstack_core/audit/signing.py +127 -0
- thinkstack_core/broadcast/__init__.py +4 -0
- thinkstack_core/broadcast/broadcaster.py +100 -0
- thinkstack_core/broadcast/watcher.py +71 -0
- thinkstack_core/capability.py +639 -0
- thinkstack_core/cloud/__init__.py +1 -0
- thinkstack_core/cloud/client_config.py +472 -0
- thinkstack_core/cloud/client_configs/.claude-opencode-fallback.json +8 -0
- thinkstack_core/cloud/client_configs/.claude-stdio.json +13 -0
- thinkstack_core/cloud/client_configs/.cursor-mcp.json +13 -0
- thinkstack_core/cloud/client_configs/.opencode-bridge.json +13 -0
- thinkstack_core/cloud/client_configs/.opencode.json +15 -0
- thinkstack_core/cloud/client_configs/.vscode-mcp.json +13 -0
- thinkstack_core/cloud/mcp_client.py +229 -0
- thinkstack_core/cloud/setup.py +144 -0
- thinkstack_core/cloud/sync.py +143 -0
- thinkstack_core/cloud/sync_bundle.py +639 -0
- thinkstack_core/cloud/sync_conflicts.py +183 -0
- thinkstack_core/cloud/sync_state.py +159 -0
- thinkstack_core/cloud/team_sync.py +337 -0
- thinkstack_core/cloud/thinkstack-mcp-bridge.js +357 -0
- thinkstack_core/codex/__init__.py +9 -0
- thinkstack_core/codex/__main__.py +97 -0
- thinkstack_core/codex/capture.py +208 -0
- thinkstack_core/codex/proxy.py +412 -0
- thinkstack_core/compat.py +103 -0
- thinkstack_core/concept_catalog.py +209 -0
- thinkstack_core/consolidation/__init__.py +3 -0
- thinkstack_core/consolidation/synthesizer.py +87 -0
- thinkstack_core/consolidation/workflow.py +175 -0
- thinkstack_core/daemon/__init__.py +27 -0
- thinkstack_core/daemon/supervisor.py +293 -0
- thinkstack_core/daemon/watcher.py +244 -0
- thinkstack_core/dashboard_api.py +2012 -0
- thinkstack_core/deltaf.py +97 -0
- thinkstack_core/disclosure.py +50 -0
- thinkstack_core/divergence/__init__.py +3 -0
- thinkstack_core/divergence/detector.py +166 -0
- thinkstack_core/gateway/__init__.py +32 -0
- thinkstack_core/gateway/key_manager.py +124 -0
- thinkstack_core/gateway/metrics_webhook.py +252 -0
- thinkstack_core/gateway/policy.py +262 -0
- thinkstack_core/gateway/server.py +727 -0
- thinkstack_core/gateway/sso.py +233 -0
- thinkstack_core/gcc.py +1246 -0
- thinkstack_core/github/__init__.py +35 -0
- thinkstack_core/github/app.py +240 -0
- thinkstack_core/github/comment_builder.py +113 -0
- thinkstack_core/github/pat.py +76 -0
- thinkstack_core/github/pr_parser.py +82 -0
- thinkstack_core/github/pr_reporter.py +555 -0
- thinkstack_core/gitlab/__init__.py +177 -0
- thinkstack_core/hitl/__init__.py +4 -0
- thinkstack_core/hitl/channels.py +129 -0
- thinkstack_core/hitl/orchestrator.py +95 -0
- thinkstack_core/hooks/__init__.py +17 -0
- thinkstack_core/hooks/claude_code.py +228 -0
- thinkstack_core/hooks/git_capture.py +341 -0
- thinkstack_core/hooks/git_commit.py +182 -0
- thinkstack_core/hooks/installer.py +850 -0
- thinkstack_core/hooks/pre_commit.py +157 -0
- thinkstack_core/hooks/runner.py +386 -0
- thinkstack_core/identity/__init__.py +4 -0
- thinkstack_core/identity/agent.py +86 -0
- thinkstack_core/identity/providers.py +85 -0
- thinkstack_core/invariants.py +182 -0
- thinkstack_core/mcp/__init__.py +10 -0
- thinkstack_core/mcp/auth.py +177 -0
- thinkstack_core/mcp/server.py +1215 -0
- thinkstack_core/metrics/__init__.py +35 -0
- thinkstack_core/metrics/aggregate.py +215 -0
- thinkstack_core/metrics/calibrate.py +198 -0
- thinkstack_core/metrics/calibration.py +125 -0
- thinkstack_core/metrics/credibility.py +288 -0
- thinkstack_core/metrics/delivery_time.py +70 -0
- thinkstack_core/metrics/dhs.py +126 -0
- thinkstack_core/metrics/mcs.py +96 -0
- thinkstack_core/metrics/roi.py +88 -0
- thinkstack_core/metrics/session_writer.py +81 -0
- thinkstack_core/metrics/shadow_ai.py +117 -0
- thinkstack_core/metrics/sprint_writer.py +243 -0
- thinkstack_core/observability/__init__.py +78 -0
- thinkstack_core/observability/datadog.py +157 -0
- thinkstack_core/observability/formatter.py +119 -0
- thinkstack_core/observability/report.py +264 -0
- thinkstack_core/observability/servicenow.py +147 -0
- thinkstack_core/observability/splunk.py +218 -0
- thinkstack_core/observability/webhook.py +227 -0
- thinkstack_core/parser/__init__.py +30 -0
- thinkstack_core/parser/blocks.py +216 -0
- thinkstack_core/parser/inference.py +159 -0
- thinkstack_core/parser/thinking.py +112 -0
- thinkstack_core/projects.py +169 -0
- thinkstack_core/prompt_artifact.py +76 -0
- thinkstack_core/proxy/__init__.py +9 -0
- thinkstack_core/proxy/routes/__init__.py +1 -0
- thinkstack_core/proxy/routes/anthropic.py +264 -0
- thinkstack_core/proxy/routes/azure_openai.py +336 -0
- thinkstack_core/proxy/routes/gemini.py +331 -0
- thinkstack_core/proxy/routes/groq.py +284 -0
- thinkstack_core/proxy/routes/ollama.py +279 -0
- thinkstack_core/proxy/routes/openai.py +287 -0
- thinkstack_core/proxy/server.py +356 -0
- thinkstack_core/query/__init__.py +15 -0
- thinkstack_core/query/grep.py +181 -0
- thinkstack_core/query/hybrid.py +86 -0
- thinkstack_core/query/semantic.py +157 -0
- thinkstack_core/rdp.py +105 -0
- thinkstack_core/reasoning/__init__.py +4 -0
- thinkstack_core/reasoning/entry.py +31 -0
- thinkstack_core/reasoning/store.py +122 -0
- thinkstack_core/reasoning_plus/__init__.py +70 -0
- thinkstack_core/reasoning_plus/augmenter.py +337 -0
- thinkstack_core/reasoning_plus/capture.py +51 -0
- thinkstack_core/reasoning_plus/config.py +313 -0
- thinkstack_core/reasoning_plus/context.py +262 -0
- thinkstack_core/reasoning_plus/learning/__init__.py +125 -0
- thinkstack_core/reasoning_plus/learning/analytics.py +141 -0
- thinkstack_core/reasoning_plus/learning/api.py +784 -0
- thinkstack_core/reasoning_plus/learning/chain.py +285 -0
- thinkstack_core/reasoning_plus/learning/composer.py +141 -0
- thinkstack_core/reasoning_plus/learning/conflicts.py +184 -0
- thinkstack_core/reasoning_plus/learning/context_collector.py +194 -0
- thinkstack_core/reasoning_plus/learning/cross_project.py +234 -0
- thinkstack_core/reasoning_plus/learning/deny_list.py +108 -0
- thinkstack_core/reasoning_plus/learning/embeddings.py +209 -0
- thinkstack_core/reasoning_plus/learning/evolution.py +119 -0
- thinkstack_core/reasoning_plus/learning/extractor.py +271 -0
- thinkstack_core/reasoning_plus/learning/filter_five_layer.py +95 -0
- thinkstack_core/reasoning_plus/learning/models.py +149 -0
- thinkstack_core/reasoning_plus/learning/org_store.py +156 -0
- thinkstack_core/reasoning_plus/learning/pii.py +142 -0
- thinkstack_core/reasoning_plus/learning/promotion.py +58 -0
- thinkstack_core/reasoning_plus/learning/provenance.py +126 -0
- thinkstack_core/reasoning_plus/learning/recorder.py +81 -0
- thinkstack_core/reasoning_plus/learning/relevance.py +122 -0
- thinkstack_core/reasoning_plus/learning/state.py +86 -0
- thinkstack_core/reasoning_plus/learning/store.py +178 -0
- thinkstack_core/reasoning_plus/learning/theta_learning_bridge.py +94 -0
- thinkstack_core/reasoning_plus/prompt.py +90 -0
- thinkstack_core/rep.py +134 -0
- thinkstack_core/rep_network/__init__.py +25 -0
- thinkstack_core/rep_network/merge.py +70 -0
- thinkstack_core/rep_network/node.py +137 -0
- thinkstack_core/rep_network/server.py +140 -0
- thinkstack_core/rep_network/sync.py +207 -0
- thinkstack_core/sensitivity.py +182 -0
- thinkstack_core/serve.py +258 -0
- thinkstack_core/session/__init__.py +39 -0
- thinkstack_core/session/disagreement.py +188 -0
- thinkstack_core/session/models.py +114 -0
- thinkstack_core/session/orchestrator.py +182 -0
- thinkstack_core/session/planner.py +169 -0
- thinkstack_core/session/simulator.py +132 -0
- thinkstack_core/signing.py +290 -0
- thinkstack_core/sis.py +197 -0
- thinkstack_core/skills/pr-reviewer/SKILL.md +204 -0
- thinkstack_core/skills/thinkstack-auto-sync/SKILL.md +175 -0
- thinkstack_core/skills/thinkstack-session-start/SKILL.md +136 -0
- thinkstack_core/storage.py +308 -0
- thinkstack_core/templates/__init__.py +6 -0
- thinkstack_core/templates/engine.py +122 -0
- thinkstack_core/templates/go.py +18 -0
- thinkstack_core/templates/infra.py +19 -0
- thinkstack_core/templates/library/__init__.py +18 -0
- thinkstack_core/templates/library/api_design.md +27 -0
- thinkstack_core/templates/library/bug_fix.md +27 -0
- thinkstack_core/templates/library/decision_record.md +27 -0
- thinkstack_core/templates/library/engine.py +228 -0
- thinkstack_core/templates/library/security_review.md +30 -0
- thinkstack_core/templates/python.py +19 -0
- thinkstack_core/templates/react.py +18 -0
- thinkstack_core/templates/typescript.py +18 -0
- thinkstack_core/theta.py +221 -0
- thinkstack_core/theta_synthesis.py +268 -0
- thinkstack_core/topics.py +320 -0
- thinkstack_core/variance.py +219 -0
- thinkstack_core/wrapper/__init__.py +52 -0
- thinkstack_core/wrapper/anthropic.py +487 -0
- thinkstack_core/wrapper/base.py +562 -0
- thinkstack_core/wrapper/bedrock.py +342 -0
- thinkstack_core/wrapper/gemini.py +422 -0
- thinkstack_core/wrapper/ollama.py +527 -0
- thinkstack_core/wrapper/openai.py +461 -0
- thinkstack_core-4.0.0.dist-info/METADATA +868 -0
- thinkstack_core-4.0.0.dist-info/RECORD +205 -0
- thinkstack_core-4.0.0.dist-info/WHEEL +5 -0
- thinkstack_core-4.0.0.dist-info/entry_points.txt +2 -0
- thinkstack_core-4.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""
|
|
2
|
+
thinkstack_core.reasoning_plus.context
|
|
3
|
+
====================================
|
|
4
|
+
Generic smart workspace context retrieval.
|
|
5
|
+
|
|
6
|
+
Lifts and generalizes the helpers originally in benchmarks/swe_bench/context_utils.py
|
|
7
|
+
so they can be used by any ThinkStack wrapper, not just the SWE-Bench runner.
|
|
8
|
+
|
|
9
|
+
The functions are defensive: if `git` is missing, the directory is not a repo, or a
|
|
10
|
+
subprocess call fails, they return empty results rather than raising.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
import subprocess
|
|
16
|
+
from collections import Counter
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Iterable
|
|
19
|
+
|
|
20
|
+
# Common English words to ignore when extracting keywords from user prompt text.
|
|
21
|
+
_STOPWORDS = {
|
|
22
|
+
"a", "an", "the", "and", "or", "but", "is", "are", "was", "were",
|
|
23
|
+
"be", "been", "being", "to", "of", "in", "for", "on", "at", "by",
|
|
24
|
+
"with", "from", "as", "it", "its", "this", "that", "these", "those",
|
|
25
|
+
"i", "you", "he", "she", "we", "they", "my", "your", "his", "her",
|
|
26
|
+
"our", "their", "will", "would", "could", "should", "may", "might",
|
|
27
|
+
"can", "must", "shall", "have", "has", "had", "do", "does", "did",
|
|
28
|
+
"not", "no", "yes", "if", "then", "else", "when", "where", "why",
|
|
29
|
+
"how", "what", "which", "who", "whom", "whose", "all", "any", "both",
|
|
30
|
+
"each", "every", "few", "more", "most", "other", "some", "such",
|
|
31
|
+
"only", "own", "same", "so", "than", "too", "very", "just", "also",
|
|
32
|
+
"now", "here", "there", "up", "out", "down", "over", "under", "again",
|
|
33
|
+
"further", "once", "about", "before", "after", "above", "below",
|
|
34
|
+
"between", "into", "through", "during", "before", "after", "until",
|
|
35
|
+
"while", "because", "although", "though", "unless", "since", "although",
|
|
36
|
+
"however", "therefore", "thus", "hence", "moreover", "furthermore",
|
|
37
|
+
"nevertheless", "nonetheless", "otherwise", "instead", "meanwhile",
|
|
38
|
+
"otherwise", "nevertheless", "nonetheless", "additionally", "consequently",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Default file patterns to search. Overrideable by callers.
|
|
42
|
+
DEFAULT_INCLUDE_GLOBS = ("*.py", "*.js", "*.ts", "*.tsx", "*.java", "*.kt", "*.go",
|
|
43
|
+
"*.rs", "*.cpp", "*.c", "*.h", "*.hpp", "*.md", "*.yaml",
|
|
44
|
+
"*.yml", "*.json", "*.toml")
|
|
45
|
+
|
|
46
|
+
# Default exclude globs. Overrideable by callers.
|
|
47
|
+
DEFAULT_EXCLUDE_GLOBS = ("*test*", "*tests*", "*__pycache__*", "*.min.js",
|
|
48
|
+
"*.bundle.js", "node_modules/*", "dist/*", "build/*",
|
|
49
|
+
"*.lock", "*.svg", "*.png", "*.jpg")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def extract_keywords(text: str, max_keywords: int = 15) -> list[str]:
|
|
53
|
+
"""
|
|
54
|
+
Extract technical keywords from prompt text.
|
|
55
|
+
|
|
56
|
+
Strategy:
|
|
57
|
+
- Keep code-like identifiers: snake_case, camelCase, dotted.module.paths.
|
|
58
|
+
- Keep words that contain digits or non-alphabetic characters.
|
|
59
|
+
- Drop common English stopwords and very short words.
|
|
60
|
+
- Return the most frequent technical terms, deduplicated.
|
|
61
|
+
"""
|
|
62
|
+
if not text:
|
|
63
|
+
return []
|
|
64
|
+
|
|
65
|
+
tokens = re.findall(r"[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*", text)
|
|
66
|
+
|
|
67
|
+
candidates = []
|
|
68
|
+
for tok in tokens:
|
|
69
|
+
t = tok.lower()
|
|
70
|
+
if len(t) < 2:
|
|
71
|
+
continue
|
|
72
|
+
if t in _STOPWORDS:
|
|
73
|
+
continue
|
|
74
|
+
if (
|
|
75
|
+
"_" in t
|
|
76
|
+
or any(c.isdigit() for c in t)
|
|
77
|
+
or any(c.isupper() for c in tok)
|
|
78
|
+
or "." in t
|
|
79
|
+
):
|
|
80
|
+
candidates.append(t)
|
|
81
|
+
elif len(t) > 4:
|
|
82
|
+
candidates.append(t)
|
|
83
|
+
|
|
84
|
+
counts = Counter(candidates)
|
|
85
|
+
ranked = [t for t, _ in counts.most_common(max_keywords * 2)]
|
|
86
|
+
seen: set[str] = set()
|
|
87
|
+
out: list[str] = []
|
|
88
|
+
for t in ranked:
|
|
89
|
+
if t not in seen:
|
|
90
|
+
seen.add(t)
|
|
91
|
+
out.append(t)
|
|
92
|
+
if len(out) >= max_keywords:
|
|
93
|
+
break
|
|
94
|
+
return out
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def search_repo(
|
|
98
|
+
repo_path: Path,
|
|
99
|
+
keywords: list[str],
|
|
100
|
+
base_commit: str | None = None,
|
|
101
|
+
top_n: int = 3,
|
|
102
|
+
max_matches_per_keyword: int = 20,
|
|
103
|
+
include_globs: Iterable[str] = (),
|
|
104
|
+
exclude_globs: Iterable[str] = (),
|
|
105
|
+
) -> list[str]:
|
|
106
|
+
"""
|
|
107
|
+
Search a git repo for files related to the given keywords.
|
|
108
|
+
|
|
109
|
+
Returns up to `top_n` file paths ranked by keyword hit count.
|
|
110
|
+
Returns empty list if the path is not a git repo or `git` is unavailable.
|
|
111
|
+
"""
|
|
112
|
+
if not keywords or not repo_path.exists():
|
|
113
|
+
return []
|
|
114
|
+
|
|
115
|
+
include_globs = tuple(include_globs) or DEFAULT_INCLUDE_GLOBS
|
|
116
|
+
exclude_globs = tuple(exclude_globs) or DEFAULT_EXCLUDE_GLOBS
|
|
117
|
+
|
|
118
|
+
file_scores: Counter[str] = Counter()
|
|
119
|
+
|
|
120
|
+
for kw in keywords:
|
|
121
|
+
try:
|
|
122
|
+
pathspecs = ["--", *include_globs]
|
|
123
|
+
cmd = [
|
|
124
|
+
"git", "grep", "-l", "-i", "-E", re.escape(kw),
|
|
125
|
+
*pathspecs,
|
|
126
|
+
]
|
|
127
|
+
if base_commit:
|
|
128
|
+
cmd = ["git", "grep", "-l", "-i", "-E", re.escape(kw), base_commit, "--", *include_globs]
|
|
129
|
+
|
|
130
|
+
result = subprocess.run(
|
|
131
|
+
cmd,
|
|
132
|
+
cwd=repo_path,
|
|
133
|
+
capture_output=True,
|
|
134
|
+
text=True,
|
|
135
|
+
errors="ignore",
|
|
136
|
+
timeout=30,
|
|
137
|
+
)
|
|
138
|
+
if result.returncode != 0:
|
|
139
|
+
continue
|
|
140
|
+
files = result.stdout.strip().splitlines()
|
|
141
|
+
for fp in files[:max_matches_per_keyword]:
|
|
142
|
+
if _is_excluded(fp, exclude_globs):
|
|
143
|
+
continue
|
|
144
|
+
file_scores[fp] += 1
|
|
145
|
+
except Exception:
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
ranked = [fp for fp, _ in file_scores.most_common()]
|
|
149
|
+
return ranked[:top_n]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def read_files_at_commit(
|
|
153
|
+
repo_path: Path,
|
|
154
|
+
file_paths: list[str],
|
|
155
|
+
commit: str | None = None,
|
|
156
|
+
max_lines: int = 150,
|
|
157
|
+
) -> str:
|
|
158
|
+
"""
|
|
159
|
+
Read file contents at a specific commit, capped per file.
|
|
160
|
+
|
|
161
|
+
If `commit` is None, reads from the working tree.
|
|
162
|
+
"""
|
|
163
|
+
chunks = []
|
|
164
|
+
for fp in file_paths:
|
|
165
|
+
try:
|
|
166
|
+
if commit:
|
|
167
|
+
result = subprocess.run(
|
|
168
|
+
["git", "show", f"{commit}:{fp}"],
|
|
169
|
+
cwd=repo_path,
|
|
170
|
+
capture_output=True,
|
|
171
|
+
text=True,
|
|
172
|
+
errors="ignore",
|
|
173
|
+
timeout=10,
|
|
174
|
+
)
|
|
175
|
+
if result.returncode != 0:
|
|
176
|
+
continue
|
|
177
|
+
text = result.stdout
|
|
178
|
+
else:
|
|
179
|
+
target = repo_path / fp
|
|
180
|
+
if not target.exists():
|
|
181
|
+
continue
|
|
182
|
+
text = target.read_text(encoding="utf-8", errors="ignore")
|
|
183
|
+
except Exception:
|
|
184
|
+
continue
|
|
185
|
+
|
|
186
|
+
lines = text.splitlines()
|
|
187
|
+
if len(lines) > max_lines:
|
|
188
|
+
text = "\n".join(lines[:max_lines]) + f"\n\n... [{len(lines) - max_lines} lines truncated]\n"
|
|
189
|
+
chunks.append(f"### File: {fp}\n```\n{text}\n```\n")
|
|
190
|
+
return "\n".join(chunks)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def gather_smart_context(
|
|
194
|
+
repo_path: Path | str,
|
|
195
|
+
prompt_text: str,
|
|
196
|
+
*,
|
|
197
|
+
base_commit: str | None = None,
|
|
198
|
+
top_n: int = 3,
|
|
199
|
+
max_lines: int = 150,
|
|
200
|
+
max_keywords: int = 15,
|
|
201
|
+
include_globs: Iterable[str] = (),
|
|
202
|
+
exclude_globs: Iterable[str] = (),
|
|
203
|
+
) -> tuple[list[str], str]:
|
|
204
|
+
"""
|
|
205
|
+
End-to-end smart context: extract keywords, search repo, read files.
|
|
206
|
+
|
|
207
|
+
Returns a tuple of (file_paths, contents_markdown).
|
|
208
|
+
"""
|
|
209
|
+
repo_path = Path(repo_path)
|
|
210
|
+
if not _is_git_repo(repo_path):
|
|
211
|
+
return [], ""
|
|
212
|
+
|
|
213
|
+
keywords = extract_keywords(prompt_text, max_keywords=max_keywords)
|
|
214
|
+
if not keywords:
|
|
215
|
+
return [], ""
|
|
216
|
+
|
|
217
|
+
extra_paths = search_repo(
|
|
218
|
+
repo_path,
|
|
219
|
+
keywords,
|
|
220
|
+
base_commit=base_commit,
|
|
221
|
+
top_n=top_n,
|
|
222
|
+
include_globs=include_globs,
|
|
223
|
+
exclude_globs=exclude_globs,
|
|
224
|
+
)
|
|
225
|
+
if not extra_paths:
|
|
226
|
+
return [], ""
|
|
227
|
+
|
|
228
|
+
contents = read_files_at_commit(
|
|
229
|
+
repo_path,
|
|
230
|
+
extra_paths,
|
|
231
|
+
commit=base_commit,
|
|
232
|
+
max_lines=max_lines,
|
|
233
|
+
)
|
|
234
|
+
return extra_paths, contents
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _is_git_repo(path: Path) -> bool:
|
|
238
|
+
"""Return True if `path` is inside a git repository."""
|
|
239
|
+
try:
|
|
240
|
+
result = subprocess.run(
|
|
241
|
+
["git", "rev-parse", "--git-dir"],
|
|
242
|
+
cwd=path,
|
|
243
|
+
capture_output=True,
|
|
244
|
+
text=True,
|
|
245
|
+
errors="ignore",
|
|
246
|
+
timeout=5,
|
|
247
|
+
)
|
|
248
|
+
return result.returncode == 0 and result.stdout.strip() != ""
|
|
249
|
+
except Exception:
|
|
250
|
+
return False
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _is_excluded(file_path: str, exclude_globs: Iterable[str]) -> bool:
|
|
254
|
+
"""Simple glob check; any glob that is a substring/endswith match."""
|
|
255
|
+
for g in exclude_globs:
|
|
256
|
+
if g.endswith("*"):
|
|
257
|
+
prefix = g[:-1]
|
|
258
|
+
if file_path.startswith(prefix) or f"/{prefix}" in file_path:
|
|
259
|
+
return True
|
|
260
|
+
if g in file_path:
|
|
261
|
+
return True
|
|
262
|
+
return False
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""
|
|
2
|
+
thinkstack_core.reasoning_plus.learning
|
|
3
|
+
=====================================
|
|
4
|
+
Reasoning Plus Learning (DRPL): capture, distill, and reuse reasoning
|
|
5
|
+
across LLM, tool, and memory calls.
|
|
6
|
+
|
|
7
|
+
Public API:
|
|
8
|
+
ReasoningPlusLearning — high-level facade
|
|
9
|
+
ReasoningCall — dataclass for a recorded call
|
|
10
|
+
Learning — dataclass for a distilled learning
|
|
11
|
+
CallRecorder — persist calls to .GCC/
|
|
12
|
+
LearningStore — persist learnings to .GCC/
|
|
13
|
+
LearningExtractor — derive learnings from calls
|
|
14
|
+
RelevanceEngine — rank learnings by relevance
|
|
15
|
+
PromptComposer — format learnings for prompts
|
|
16
|
+
workspace_state_hash — compute a state hash for staleness
|
|
17
|
+
cross_project_suggest — suggest learnings from sibling governed projects
|
|
18
|
+
register_project — register a project for cross-project learning
|
|
19
|
+
list_projects — list registered governed projects
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from thinkstack_core.reasoning_plus.learning.api import ReasoningPlusLearning
|
|
24
|
+
from thinkstack_core.reasoning_plus.learning.composer import PromptComposer
|
|
25
|
+
from thinkstack_core.reasoning_plus.learning.cross_project import (
|
|
26
|
+
CrossProjectRegistry,
|
|
27
|
+
GovernedProject,
|
|
28
|
+
cross_project_suggest,
|
|
29
|
+
list_projects,
|
|
30
|
+
register_project,
|
|
31
|
+
)
|
|
32
|
+
from thinkstack_core.reasoning_plus.learning.extractor import LearningExtractor
|
|
33
|
+
from thinkstack_core.reasoning_plus.learning.models import Learning, ReasoningCall
|
|
34
|
+
from thinkstack_core.reasoning_plus.learning.recorder import CallRecorder
|
|
35
|
+
from thinkstack_core.reasoning_plus.learning.relevance import RelevanceEngine
|
|
36
|
+
from thinkstack_core.reasoning_plus.learning.state import workspace_state_hash
|
|
37
|
+
from thinkstack_core.reasoning_plus.learning.store import LearningStore
|
|
38
|
+
from thinkstack_core.reasoning_plus.learning.analytics import (
|
|
39
|
+
concept_stats,
|
|
40
|
+
danger_zones,
|
|
41
|
+
concept_health_section,
|
|
42
|
+
)
|
|
43
|
+
from thinkstack_core.reasoning_plus.learning.theta_learning_bridge import (
|
|
44
|
+
sync_learnings_to_theta,
|
|
45
|
+
)
|
|
46
|
+
from thinkstack_core.reasoning_plus.learning.chain import (
|
|
47
|
+
build_chain,
|
|
48
|
+
format_chains,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
from thinkstack_core.reasoning_plus.learning.context_collector import (
|
|
52
|
+
ExtractionContext,
|
|
53
|
+
collect_extraction_context,
|
|
54
|
+
)
|
|
55
|
+
from thinkstack_core.reasoning_plus.learning.conflicts import (
|
|
56
|
+
Conflict,
|
|
57
|
+
ConflictDetector,
|
|
58
|
+
ConflictResolution,
|
|
59
|
+
resolve_conflict,
|
|
60
|
+
)
|
|
61
|
+
from thinkstack_core.reasoning_plus.learning.evolution import (
|
|
62
|
+
apply_confidence_decay,
|
|
63
|
+
goal_changed_since,
|
|
64
|
+
)
|
|
65
|
+
from thinkstack_core.reasoning_plus.learning.pii import (
|
|
66
|
+
filter_learnings_for_sync,
|
|
67
|
+
sanitize_for_extraction,
|
|
68
|
+
scan_for_secrets,
|
|
69
|
+
)
|
|
70
|
+
from thinkstack_core.reasoning_plus.learning.org_store import (
|
|
71
|
+
OrgLearningAggregate,
|
|
72
|
+
aggregate_org_learnings,
|
|
73
|
+
)
|
|
74
|
+
from thinkstack_core.reasoning_plus.learning.filter_five_layer import (
|
|
75
|
+
five_layer_filter,
|
|
76
|
+
)
|
|
77
|
+
from thinkstack_core.reasoning_plus.learning.deny_list import (
|
|
78
|
+
DenyList,
|
|
79
|
+
load_deny_list,
|
|
80
|
+
save_deny_list,
|
|
81
|
+
)
|
|
82
|
+
from thinkstack_core.reasoning_plus.learning.promotion import (
|
|
83
|
+
promote_learning,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
__all__ = [
|
|
87
|
+
"ReasoningPlusLearning",
|
|
88
|
+
"ReasoningCall",
|
|
89
|
+
"Learning",
|
|
90
|
+
"CallRecorder",
|
|
91
|
+
"LearningStore",
|
|
92
|
+
"LearningExtractor",
|
|
93
|
+
"RelevanceEngine",
|
|
94
|
+
"PromptComposer",
|
|
95
|
+
"workspace_state_hash",
|
|
96
|
+
"cross_project_suggest",
|
|
97
|
+
"register_project",
|
|
98
|
+
"list_projects",
|
|
99
|
+
"CrossProjectRegistry",
|
|
100
|
+
"GovernedProject",
|
|
101
|
+
"concept_stats",
|
|
102
|
+
"danger_zones",
|
|
103
|
+
"concept_health_section",
|
|
104
|
+
"sync_learnings_to_theta",
|
|
105
|
+
"build_chain",
|
|
106
|
+
"format_chains",
|
|
107
|
+
"ExtractionContext",
|
|
108
|
+
"collect_extraction_context",
|
|
109
|
+
"Conflict",
|
|
110
|
+
"ConflictDetector",
|
|
111
|
+
"ConflictResolution",
|
|
112
|
+
"resolve_conflict",
|
|
113
|
+
"apply_confidence_decay",
|
|
114
|
+
"goal_changed_since",
|
|
115
|
+
"filter_learnings_for_sync",
|
|
116
|
+
"sanitize_for_extraction",
|
|
117
|
+
"scan_for_secrets",
|
|
118
|
+
"OrgLearningAggregate",
|
|
119
|
+
"aggregate_org_learnings",
|
|
120
|
+
"five_layer_filter",
|
|
121
|
+
"DenyList",
|
|
122
|
+
"load_deny_list",
|
|
123
|
+
"save_deny_list",
|
|
124
|
+
"promote_learning",
|
|
125
|
+
]
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""
|
|
2
|
+
thinkstack_core.reasoning_plus.learning.analytics
|
|
3
|
+
================================================
|
|
4
|
+
Per-concept learning analytics: stats, danger-zone detection, health dashboard.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from collections import Counter
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from thinkstack_core.reasoning_plus.learning.store import LearningStore
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("thinkstack.reasoning_plus.learning")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def concept_stats(store: LearningStore) -> dict[str, dict[str, Any]]:
|
|
18
|
+
"""Return per-concept statistics aggregated from all stored learnings.
|
|
19
|
+
|
|
20
|
+
Returns a dict keyed by lowercased concept name with:
|
|
21
|
+
- total: total learnings tagged with this concept
|
|
22
|
+
- active: learnings with validity="active"
|
|
23
|
+
- stale: learnings with validity="stale"
|
|
24
|
+
- deprecated: learnings with validity="deprecated"
|
|
25
|
+
- by_type: Counter of learning types (insight, correction, pattern, avoid, confirm)
|
|
26
|
+
- by_outcome: Counter of outcomes among source calls (success, failure, partial, unknown)
|
|
27
|
+
- avg_confidence: mean confidence across all learnings for this concept
|
|
28
|
+
- success_rate: fraction of (non-unknown) outcomes that are "success"
|
|
29
|
+
"""
|
|
30
|
+
all_learnings = store.list()
|
|
31
|
+
concept_map: dict[str, dict[str, Any]] = {}
|
|
32
|
+
|
|
33
|
+
for learning in all_learnings:
|
|
34
|
+
concepts = learning.trigger_concepts
|
|
35
|
+
if not concepts:
|
|
36
|
+
concepts = ["__untyped__"]
|
|
37
|
+
for concept in concepts:
|
|
38
|
+
cname = concept.lower()
|
|
39
|
+
if cname not in concept_map:
|
|
40
|
+
concept_map[cname] = {
|
|
41
|
+
"total": 0,
|
|
42
|
+
"active": 0,
|
|
43
|
+
"stale": 0,
|
|
44
|
+
"deprecated": 0,
|
|
45
|
+
"by_type": Counter(),
|
|
46
|
+
"by_outcome": Counter(),
|
|
47
|
+
"confidences": [],
|
|
48
|
+
}
|
|
49
|
+
concept_map[cname]["total"] += 1
|
|
50
|
+
concept_map[cname][learning.validity] += 1
|
|
51
|
+
concept_map[cname]["by_type"][learning.type] += 1
|
|
52
|
+
concept_map[cname]["confidences"].append(learning.confidence)
|
|
53
|
+
|
|
54
|
+
for cname, data in concept_map.items():
|
|
55
|
+
confs = data.pop("confidences")
|
|
56
|
+
data["avg_confidence"] = sum(confs) / len(confs) if confs else 0.0
|
|
57
|
+
data["success_rate"] = _compute_success_rate(store, cname)
|
|
58
|
+
|
|
59
|
+
return dict(sorted(concept_map.items()))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _compute_success_rate(store: LearningStore, concept: str) -> float:
|
|
63
|
+
"""Query call records for the given concept and compute success fraction."""
|
|
64
|
+
from thinkstack_core.reasoning_plus.learning.models import ReasoningCall
|
|
65
|
+
from thinkstack_core.reasoning_plus.learning.recorder import CallRecorder
|
|
66
|
+
|
|
67
|
+
recorder = CallRecorder(store._gcc_dir)
|
|
68
|
+
calls = recorder.list()
|
|
69
|
+
matched = [c for c in calls if concept in {co.lower() for co in c.concepts}]
|
|
70
|
+
non_unknown = [c for c in matched if c.outcome not in ("unknown", "")]
|
|
71
|
+
if not non_unknown:
|
|
72
|
+
return 0.0
|
|
73
|
+
successes = sum(1 for c in non_unknown if c.outcome == "success")
|
|
74
|
+
return successes / len(non_unknown)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def danger_zones(
|
|
78
|
+
store: LearningStore,
|
|
79
|
+
threshold: float = 0.4,
|
|
80
|
+
min_samples: int = 3,
|
|
81
|
+
) -> list[dict[str, Any]]:
|
|
82
|
+
"""Return concepts with success rate below *threshold*.
|
|
83
|
+
|
|
84
|
+
Each entry contains:
|
|
85
|
+
- concept: concept name
|
|
86
|
+
- success_rate: computed success fraction
|
|
87
|
+
- total_learnings: total learnings for this concept
|
|
88
|
+
- avg_confidence: mean learning confidence
|
|
89
|
+
Only concepts with at least *min_samples* total learnings are considered.
|
|
90
|
+
"""
|
|
91
|
+
stats = concept_stats(store)
|
|
92
|
+
zones: list[dict[str, Any]] = []
|
|
93
|
+
for cname, data in stats.items():
|
|
94
|
+
if cname == "__untyped__":
|
|
95
|
+
continue
|
|
96
|
+
if data["total"] < min_samples:
|
|
97
|
+
continue
|
|
98
|
+
if data["success_rate"] < threshold:
|
|
99
|
+
zones.append({
|
|
100
|
+
"concept": cname,
|
|
101
|
+
"success_rate": data["success_rate"],
|
|
102
|
+
"total_learnings": data["total"],
|
|
103
|
+
"avg_confidence": data["avg_confidence"],
|
|
104
|
+
})
|
|
105
|
+
zones.sort(key=lambda x: x["success_rate"])
|
|
106
|
+
return zones
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def concept_health_section(store: LearningStore, threshold: float = 0.4) -> str:
|
|
110
|
+
"""Return a formatted concept health block for the ``thinkstack today`` dashboard."""
|
|
111
|
+
stats = concept_stats(store)
|
|
112
|
+
if not stats:
|
|
113
|
+
return ""
|
|
114
|
+
|
|
115
|
+
lines = ["─── Concept health ───"]
|
|
116
|
+
|
|
117
|
+
zones = danger_zones(store, threshold=threshold)
|
|
118
|
+
if zones:
|
|
119
|
+
lines.append(f" \u26a0 DANGER ZONES (success rate < {threshold:.0%}):")
|
|
120
|
+
for z in zones[:5]:
|
|
121
|
+
lines.append(
|
|
122
|
+
f" {z['concept']}: success {z['success_rate']:.0%}, "
|
|
123
|
+
f"{z['total_learnings']} learning(s), "
|
|
124
|
+
f"avg confidence {z['avg_confidence']:.2f}"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
healthy = [(c, d) for c, d in stats.items()
|
|
128
|
+
if c != "__untyped__" and d["success_rate"] >= threshold]
|
|
129
|
+
if healthy:
|
|
130
|
+
lines.append(f" Healthy concepts ({len(healthy)}):")
|
|
131
|
+
for c, d in sorted(healthy, key=lambda x: x[1]["success_rate"], reverse=True)[:8]:
|
|
132
|
+
lines.append(
|
|
133
|
+
f" {c}: {d['active']} active, "
|
|
134
|
+
f"{d['success_rate']:.0%} success"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
untyped = stats.get("__untyped__")
|
|
138
|
+
if untyped and untyped["total"] > 0:
|
|
139
|
+
lines.append(f" Untagged learnings: {untyped['total']}")
|
|
140
|
+
|
|
141
|
+
return "\n".join(lines)
|