memor-cli 0.5.0__tar.gz → 0.6.0__tar.gz
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.
- {memor_cli-0.5.0/memor_cli.egg-info → memor_cli-0.6.0}/PKG-INFO +26 -6
- {memor_cli-0.5.0 → memor_cli-0.6.0}/README.md +25 -5
- memor_cli-0.6.0/memor/__init__.py +1 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/daemon.py +10 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/dashboard/server.py +26 -5
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/dashboard/static/index.html +93 -6
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/feedback.py +55 -9
- memor_cli-0.6.0/memor/global_memories.py +102 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/store/sqlite_store.py +33 -6
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/types.py +4 -1
- {memor_cli-0.5.0 → memor_cli-0.6.0/memor_cli.egg-info}/PKG-INFO +26 -6
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor_cli.egg-info/SOURCES.txt +4 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/pyproject.toml +1 -1
- memor_cli-0.6.0/tests/test_dashboard_quality.py +93 -0
- memor_cli-0.6.0/tests/test_global_memories.py +178 -0
- memor_cli-0.6.0/tests/test_negative_feedback.py +208 -0
- memor_cli-0.5.0/memor/__init__.py +0 -1
- {memor_cli-0.5.0 → memor_cli-0.6.0}/LICENSE +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/cli.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/dashboard/__init__.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/distill/__init__.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/distill/distiller.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/distill/extractive.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/embed/__init__.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/embed/api.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/embed/fake.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/embed/local.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/eval/__init__.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/eval/baselines/__init__.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/eval/baselines/base.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/eval/baselines/claude_mem.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/eval/baselines/graphiti.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/eval/counterfactual.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/eval/dataset.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/eval/embed_benchmark.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/eval/judge.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/eval/metrics.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/eval/runner.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/hook_cli.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/hook_server.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/ingest/__init__.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/ingest/claude_code.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/ingest/documents.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/interfaces.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/llm/__init__.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/llm/anthropic.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/llm/base.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/llm/openai_compat.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/project.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/query_complexity.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/recall.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/redact.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/retrieve/__init__.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/retrieve/retriever.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/service.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/session_context.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/store/__init__.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/tokencount.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor/turn_metrics.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor_cli.egg-info/dependency_links.txt +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor_cli.egg-info/entry_points.txt +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor_cli.egg-info/requires.txt +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/memor_cli.egg-info/top_level.txt +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/setup.cfg +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/tests/test_cli_smoke.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/tests/test_counterfactual.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/tests/test_daemon.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/tests/test_dashboard.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/tests/test_dataset_builder.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/tests/test_dimension_safety.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/tests/test_distiller.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/tests/test_embed.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/tests/test_embed_benchmark.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/tests/test_eval_ablation.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/tests/test_eval_runner.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/tests/test_external_baselines.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/tests/test_extractive.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/tests/test_feedback.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/tests/test_hook.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/tests/test_hook_server.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/tests/test_hybrid_retrieval.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/tests/test_ingest_claude_code.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/tests/test_ingest_documents.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/tests/test_install_hook.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/tests/test_interfaces.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/tests/test_judge.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/tests/test_metrics.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/tests/test_noise_filter.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/tests/test_project_resolver.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/tests/test_quality_gate.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/tests/test_query_complexity.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/tests/test_recall_core.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/tests/test_redact.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/tests/test_retriever.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/tests/test_roi_trend.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/tests/test_semantic_feedback.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/tests/test_service.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/tests/test_session_context.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/tests/test_skill_recall.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/tests/test_store.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/tests/test_supersession.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/tests/test_tokencount.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/tests/test_turn_metrics.py +0 -0
- {memor_cli-0.5.0 → memor_cli-0.6.0}/tests/test_types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memor-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Measured memory for coding agents. Fire and forget — no API keys needed.
|
|
5
5
|
Author-email: Nimit Bhandari <nimitbhandari17@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -46,7 +46,7 @@ Dynamic: license-file
|
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
[](LICENSE)
|
|
49
|
-
[]()
|
|
50
50
|
[]()
|
|
51
51
|
[](https://pypi.org/project/memor-cli/)
|
|
52
52
|
|
|
@@ -113,7 +113,7 @@ memor dashboard
|
|
|
113
113
|
|
|
114
114
|
**Two background processes:**
|
|
115
115
|
|
|
116
|
-
1. **Daemon** — polls `~/.claude/projects/` for transcripts, embeds chunks, runs distillation, analyzes feedback, compacts duplicates, tracks session-level token usage. All local.
|
|
116
|
+
1. **Daemon** — polls `~/.claude/projects/` for transcripts, embeds chunks, runs distillation, analyzes feedback (positive and negative), promotes cross-project patterns to global scope, compacts duplicates, tracks session-level token usage. All local.
|
|
117
117
|
2. **Hook** — fires on every prompt, recalls relevant memories, injects them as context. Sub-15ms.
|
|
118
118
|
|
|
119
119
|
**No API keys required.** Embeddings run locally via [model2vec](https://github.com/MinishLab/model2vec) (potion-base-8M, 256-dim). Vectors stored in [sqlite-vec](https://github.com/asg017/sqlite-vec). Everything runs on your machine.
|
|
@@ -146,7 +146,12 @@ This means a relevant decision from yesterday beats a vaguely-related chunk from
|
|
|
146
146
|
|
|
147
147
|
### Feedback Loop
|
|
148
148
|
|
|
149
|
-
Memor tracks whether recalled memories actually get used by the agent. After each session, the daemon analyzes the transcript
|
|
149
|
+
Memor tracks whether recalled memories actually get used by the agent — and whether they actively hurt. After each session, the daemon analyzes the transcript in both directions:
|
|
150
|
+
|
|
151
|
+
- **Positive signal** — n-gram overlap or semantic similarity between recalled content and the agent's response. Memories that consistently prove useful get quality boosts.
|
|
152
|
+
- **Negative signal** — user rejection ("no that's wrong", "we switched to X") or assistant contradiction ("however, looking at the current code, we actually use Y"). Memories that get corrected receive a quality penalty, making them less likely to be recalled next time.
|
|
153
|
+
|
|
154
|
+
The quality formula is Bayesian: `(uses - negatives + 1) / (recalls + 2)`. One correction weighs as much as one positive use, so harmful memories drop fast. Memories never recalled in 30+ days get automatically deactivated. Near-duplicate memories are compacted into one.
|
|
150
155
|
|
|
151
156
|
---
|
|
152
157
|
|
|
@@ -161,6 +166,20 @@ Memories are automatically classified as `decision`, `bugfix`, `lesson`, `snippe
|
|
|
161
166
|
|
|
162
167
|
---
|
|
163
168
|
|
|
169
|
+
## Global Memories
|
|
170
|
+
|
|
171
|
+
Some patterns aren't project-specific — they're yours. "Always use type hints." "Structure FastAPI apps with a `routes/` directory." "Prefer composition over inheritance."
|
|
172
|
+
|
|
173
|
+
Memor detects these automatically. When the same pattern appears in **3 or more projects** (measured by embedding similarity), the daemon promotes it to a `_global` scope:
|
|
174
|
+
|
|
175
|
+
- **Global memories are recalled everywhere** — they show up in every project's search results alongside project-specific memories.
|
|
176
|
+
- **Source duplicates are deactivated** — the per-project copies get superseded by the single global version, reducing clutter.
|
|
177
|
+
- **No manual tagging** — promotion is fully automatic, based on cross-project clustering.
|
|
178
|
+
|
|
179
|
+
This means your coding habits and preferences follow you into new projects from the first prompt, without you having to re-explain anything.
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
164
183
|
## Dashboard
|
|
165
184
|
|
|
166
185
|
```bash
|
|
@@ -217,7 +236,8 @@ memor/
|
|
|
217
236
|
+-- project.py Git-root project resolver (filesystem-aware)
|
|
218
237
|
+-- recall.py Shared recall core (used by hook + skill)
|
|
219
238
|
+-- redact.py Secret detection and redaction at ingest
|
|
220
|
-
+-- feedback.py
|
|
239
|
+
+-- feedback.py Feedback analyzer (positive usage + negative signals)
|
|
240
|
+
+-- global_memories.py Cross-project promotion to _global scope
|
|
221
241
|
|
|
|
222
242
|
+-- retrieve/
|
|
223
243
|
| +-- retriever.py Hybrid retrieval (dense + BM25, RRF) + relevance gate + scoring
|
|
@@ -291,7 +311,7 @@ cd memor-ai
|
|
|
291
311
|
python3 -m venv .venv && source .venv/bin/activate
|
|
292
312
|
pip install -e ".[dev]"
|
|
293
313
|
|
|
294
|
-
pytest #
|
|
314
|
+
pytest # 251 tests
|
|
295
315
|
```
|
|
296
316
|
|
|
297
317
|
---
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
[](LICENSE)
|
|
12
|
-
[]()
|
|
13
13
|
[]()
|
|
14
14
|
[](https://pypi.org/project/memor-cli/)
|
|
15
15
|
|
|
@@ -76,7 +76,7 @@ memor dashboard
|
|
|
76
76
|
|
|
77
77
|
**Two background processes:**
|
|
78
78
|
|
|
79
|
-
1. **Daemon** — polls `~/.claude/projects/` for transcripts, embeds chunks, runs distillation, analyzes feedback, compacts duplicates, tracks session-level token usage. All local.
|
|
79
|
+
1. **Daemon** — polls `~/.claude/projects/` for transcripts, embeds chunks, runs distillation, analyzes feedback (positive and negative), promotes cross-project patterns to global scope, compacts duplicates, tracks session-level token usage. All local.
|
|
80
80
|
2. **Hook** — fires on every prompt, recalls relevant memories, injects them as context. Sub-15ms.
|
|
81
81
|
|
|
82
82
|
**No API keys required.** Embeddings run locally via [model2vec](https://github.com/MinishLab/model2vec) (potion-base-8M, 256-dim). Vectors stored in [sqlite-vec](https://github.com/asg017/sqlite-vec). Everything runs on your machine.
|
|
@@ -109,7 +109,12 @@ This means a relevant decision from yesterday beats a vaguely-related chunk from
|
|
|
109
109
|
|
|
110
110
|
### Feedback Loop
|
|
111
111
|
|
|
112
|
-
Memor tracks whether recalled memories actually get used by the agent. After each session, the daemon analyzes the transcript
|
|
112
|
+
Memor tracks whether recalled memories actually get used by the agent — and whether they actively hurt. After each session, the daemon analyzes the transcript in both directions:
|
|
113
|
+
|
|
114
|
+
- **Positive signal** — n-gram overlap or semantic similarity between recalled content and the agent's response. Memories that consistently prove useful get quality boosts.
|
|
115
|
+
- **Negative signal** — user rejection ("no that's wrong", "we switched to X") or assistant contradiction ("however, looking at the current code, we actually use Y"). Memories that get corrected receive a quality penalty, making them less likely to be recalled next time.
|
|
116
|
+
|
|
117
|
+
The quality formula is Bayesian: `(uses - negatives + 1) / (recalls + 2)`. One correction weighs as much as one positive use, so harmful memories drop fast. Memories never recalled in 30+ days get automatically deactivated. Near-duplicate memories are compacted into one.
|
|
113
118
|
|
|
114
119
|
---
|
|
115
120
|
|
|
@@ -124,6 +129,20 @@ Memories are automatically classified as `decision`, `bugfix`, `lesson`, `snippe
|
|
|
124
129
|
|
|
125
130
|
---
|
|
126
131
|
|
|
132
|
+
## Global Memories
|
|
133
|
+
|
|
134
|
+
Some patterns aren't project-specific — they're yours. "Always use type hints." "Structure FastAPI apps with a `routes/` directory." "Prefer composition over inheritance."
|
|
135
|
+
|
|
136
|
+
Memor detects these automatically. When the same pattern appears in **3 or more projects** (measured by embedding similarity), the daemon promotes it to a `_global` scope:
|
|
137
|
+
|
|
138
|
+
- **Global memories are recalled everywhere** — they show up in every project's search results alongside project-specific memories.
|
|
139
|
+
- **Source duplicates are deactivated** — the per-project copies get superseded by the single global version, reducing clutter.
|
|
140
|
+
- **No manual tagging** — promotion is fully automatic, based on cross-project clustering.
|
|
141
|
+
|
|
142
|
+
This means your coding habits and preferences follow you into new projects from the first prompt, without you having to re-explain anything.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
127
146
|
## Dashboard
|
|
128
147
|
|
|
129
148
|
```bash
|
|
@@ -180,7 +199,8 @@ memor/
|
|
|
180
199
|
+-- project.py Git-root project resolver (filesystem-aware)
|
|
181
200
|
+-- recall.py Shared recall core (used by hook + skill)
|
|
182
201
|
+-- redact.py Secret detection and redaction at ingest
|
|
183
|
-
+-- feedback.py
|
|
202
|
+
+-- feedback.py Feedback analyzer (positive usage + negative signals)
|
|
203
|
+
+-- global_memories.py Cross-project promotion to _global scope
|
|
184
204
|
|
|
|
185
205
|
+-- retrieve/
|
|
186
206
|
| +-- retriever.py Hybrid retrieval (dense + BM25, RRF) + relevance gate + scoring
|
|
@@ -254,7 +274,7 @@ cd memor-ai
|
|
|
254
274
|
python3 -m venv .venv && source .venv/bin/activate
|
|
255
275
|
pip install -e ".[dev]"
|
|
256
276
|
|
|
257
|
-
pytest #
|
|
277
|
+
pytest # 251 tests
|
|
258
278
|
```
|
|
259
279
|
|
|
260
280
|
---
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.6.0"
|
|
@@ -267,6 +267,16 @@ def run_poll_cycle(
|
|
|
267
267
|
except Exception:
|
|
268
268
|
pass
|
|
269
269
|
|
|
270
|
+
# Promote cross-project patterns to global scope
|
|
271
|
+
if new_ingested:
|
|
272
|
+
try:
|
|
273
|
+
from memor.global_memories import run_promotion
|
|
274
|
+
promoted = run_promotion(store, embedder, min_projects=3)
|
|
275
|
+
if promoted > 0:
|
|
276
|
+
print(f" promoted {promoted} memories to global scope")
|
|
277
|
+
except Exception:
|
|
278
|
+
pass
|
|
279
|
+
|
|
270
280
|
# Compact near-duplicate memories (run occasionally, not every cycle)
|
|
271
281
|
if new_ingested:
|
|
272
282
|
try:
|
|
@@ -27,6 +27,7 @@ def create_app(db_path: str | None = None) -> FastAPI:
|
|
|
27
27
|
|
|
28
28
|
@app.get("/api/summary")
|
|
29
29
|
def summary():
|
|
30
|
+
from memor.types import GLOBAL_PROJECT
|
|
30
31
|
store = _store()
|
|
31
32
|
recall_stats = store.get_recall_stats()
|
|
32
33
|
ingestion = {}
|
|
@@ -38,17 +39,22 @@ def create_app(db_path: str | None = None) -> FastAPI:
|
|
|
38
39
|
project_count = store.db.execute(
|
|
39
40
|
"SELECT COUNT(DISTINCT project) as c FROM artifacts"
|
|
40
41
|
).fetchone()["c"]
|
|
42
|
+
global_count = store.db.execute(
|
|
43
|
+
"SELECT COUNT(*) as c FROM artifacts WHERE active=1 AND project=? AND kind='memory'",
|
|
44
|
+
(GLOBAL_PROJECT,)
|
|
45
|
+
).fetchone()["c"]
|
|
41
46
|
recall_stats["ingestion"] = ingestion
|
|
42
47
|
recall_stats["project_count"] = project_count
|
|
48
|
+
recall_stats["global_memories"] = global_count
|
|
43
49
|
return recall_stats
|
|
44
50
|
|
|
45
51
|
@app.get("/api/projects")
|
|
46
52
|
def projects():
|
|
47
53
|
store = _store()
|
|
48
54
|
recall_stats = store.get_project_stats()
|
|
49
|
-
if recall_stats
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
recall_projects = {r["project"] for r in recall_stats} if recall_stats else set()
|
|
56
|
+
|
|
57
|
+
artifact_rows = store.db.execute("""
|
|
52
58
|
SELECT project,
|
|
53
59
|
COUNT(*) as artifacts,
|
|
54
60
|
SUM(CASE WHEN kind='session_chunk' THEN 1 ELSE 0 END) as chunks,
|
|
@@ -59,7 +65,15 @@ def create_app(db_path: str | None = None) -> FastAPI:
|
|
|
59
65
|
GROUP BY project
|
|
60
66
|
ORDER BY artifacts DESC
|
|
61
67
|
""").fetchall()
|
|
62
|
-
|
|
68
|
+
|
|
69
|
+
if not recall_stats:
|
|
70
|
+
return [dict(r) for r in artifact_rows]
|
|
71
|
+
|
|
72
|
+
result = list(recall_stats)
|
|
73
|
+
for r in artifact_rows:
|
|
74
|
+
if r["project"] not in recall_projects:
|
|
75
|
+
result.append(dict(r))
|
|
76
|
+
return result
|
|
63
77
|
|
|
64
78
|
@app.get("/api/recalls")
|
|
65
79
|
def recalls(limit: int = Query(50, ge=1, le=500),
|
|
@@ -71,7 +85,9 @@ def create_app(db_path: str | None = None) -> FastAPI:
|
|
|
71
85
|
def quality():
|
|
72
86
|
store = _store()
|
|
73
87
|
rows = store.db.execute("""
|
|
74
|
-
SELECT q.artifact_id, q.recall_count, q.use_count,
|
|
88
|
+
SELECT q.artifact_id, q.recall_count, q.use_count,
|
|
89
|
+
COALESCE(q.negative_count, 0) as negative_count,
|
|
90
|
+
q.quality_score,
|
|
75
91
|
a.project, a.kind, json_extract(a.meta, '$.mem_type') as mem_type,
|
|
76
92
|
substr(a.text, 1, 100) as preview
|
|
77
93
|
FROM memory_quality q
|
|
@@ -152,6 +168,11 @@ def create_app(db_path: str | None = None) -> FastAPI:
|
|
|
152
168
|
return {"status": "no_runs"}
|
|
153
169
|
return result
|
|
154
170
|
|
|
171
|
+
@app.get("/api/version")
|
|
172
|
+
def version():
|
|
173
|
+
from memor import __version__
|
|
174
|
+
return {"version": __version__}
|
|
175
|
+
|
|
155
176
|
@app.get("/api/health")
|
|
156
177
|
def health():
|
|
157
178
|
store = _store()
|
|
@@ -113,7 +113,7 @@
|
|
|
113
113
|
/* ── Metric cards ──────────────────────────────────────── */
|
|
114
114
|
.metric-grid {
|
|
115
115
|
display: grid;
|
|
116
|
-
grid-template-columns: repeat(
|
|
116
|
+
grid-template-columns: repeat(5, 1fr);
|
|
117
117
|
gap: 14px;
|
|
118
118
|
}
|
|
119
119
|
@media (max-width: 900px) { .metric-grid { grid-template-columns: repeat(2, 1fr); } }
|
|
@@ -300,6 +300,16 @@
|
|
|
300
300
|
.badge-extractive::before { background: var(--warn); }
|
|
301
301
|
.badge-no_hits { background: var(--neutral-dim); color: var(--neutral); }
|
|
302
302
|
.badge-no_hits::before { background: var(--neutral); }
|
|
303
|
+
.badge-global { background: rgba(120,100,255,0.12); color: #a090ff; }
|
|
304
|
+
.badge-global::before { background: #a090ff; }
|
|
305
|
+
.badge-negative { background: rgba(232,84,84,0.12); color: var(--error); }
|
|
306
|
+
.badge-negative::before { background: var(--error); }
|
|
307
|
+
|
|
308
|
+
.quality-bar { display: flex; height: 6px; border-radius: 3px; overflow: hidden; background: var(--surface3); width: 100%; min-width: 60px; }
|
|
309
|
+
.quality-fill { height: 100%; border-radius: 3px; transition: width 0.3s; }
|
|
310
|
+
.quality-fill-ok { background: var(--ok); }
|
|
311
|
+
.quality-fill-warn { background: var(--warn); }
|
|
312
|
+
.quality-fill-error { background: var(--error); }
|
|
303
313
|
|
|
304
314
|
.empty-state {
|
|
305
315
|
text-align: center; padding: 40px 20px;
|
|
@@ -319,6 +329,7 @@
|
|
|
319
329
|
<div class="topbar-inner">
|
|
320
330
|
<div class="breadcrumb">Dashboard › <span>Overview</span></div>
|
|
321
331
|
<div class="topbar-right">
|
|
332
|
+
<span id="version-badge" class="mono" style="font-size:11px;color:var(--text-muted);margin-right:12px;"></span>
|
|
322
333
|
<div class="topbar-time">
|
|
323
334
|
<span class="refresh-dot"></span>
|
|
324
335
|
<span id="last-updated">Live</span>
|
|
@@ -369,6 +380,13 @@
|
|
|
369
380
|
</div>
|
|
370
381
|
<div class="mini-bars" id="mb-projects"></div>
|
|
371
382
|
</div>
|
|
383
|
+
<div class="metric-card">
|
|
384
|
+
<div class="metric-info">
|
|
385
|
+
<div class="metric-label">Global</div>
|
|
386
|
+
<div class="metric-value" id="m-global">–</div>
|
|
387
|
+
<div class="metric-sub">cross-project memories</div>
|
|
388
|
+
</div>
|
|
389
|
+
</div>
|
|
372
390
|
<div class="metric-card">
|
|
373
391
|
<div class="metric-info">
|
|
374
392
|
<div class="metric-label">Total Recalls</div>
|
|
@@ -485,6 +503,30 @@
|
|
|
485
503
|
</div>
|
|
486
504
|
</section>
|
|
487
505
|
|
|
506
|
+
<!-- ── Memory Quality ──────────────────────────────────── -->
|
|
507
|
+
<section>
|
|
508
|
+
<div class="table-card" id="quality-section" style="display:none;">
|
|
509
|
+
<div class="table-header">
|
|
510
|
+
<div class="table-title"><span class="dot"></span> Memory Quality</div>
|
|
511
|
+
</div>
|
|
512
|
+
<table>
|
|
513
|
+
<thead>
|
|
514
|
+
<tr>
|
|
515
|
+
<th>Memory</th>
|
|
516
|
+
<th>Project</th>
|
|
517
|
+
<th>Type</th>
|
|
518
|
+
<th>Recalls</th>
|
|
519
|
+
<th>Used</th>
|
|
520
|
+
<th>Rejected</th>
|
|
521
|
+
<th>Score</th>
|
|
522
|
+
<th style="min-width:80px;"></th>
|
|
523
|
+
</tr>
|
|
524
|
+
</thead>
|
|
525
|
+
<tbody id="quality-body"></tbody>
|
|
526
|
+
</table>
|
|
527
|
+
</div>
|
|
528
|
+
</section>
|
|
529
|
+
|
|
488
530
|
<!-- ── Recent recalls ─────────────────────────────────── -->
|
|
489
531
|
<section>
|
|
490
532
|
<div class="table-card">
|
|
@@ -603,6 +645,7 @@
|
|
|
603
645
|
document.getElementById('m-chunks').textContent = fmt(chunks);
|
|
604
646
|
document.getElementById('m-memories').textContent = fmt(memories);
|
|
605
647
|
document.getElementById('m-projects').textContent = fmt(d.project_count || 0);
|
|
648
|
+
document.getElementById('m-global').textContent = fmt(d.global_memories || 0);
|
|
606
649
|
document.getElementById('m-recalls').textContent = fmt(d.total_recalls);
|
|
607
650
|
if (d.total_recalls > 0) {
|
|
608
651
|
document.getElementById('m-recalls-sub').textContent =
|
|
@@ -695,6 +738,39 @@
|
|
|
695
738
|
});
|
|
696
739
|
}
|
|
697
740
|
|
|
741
|
+
/* ── Quality ───────────────────────────────────────────── */
|
|
742
|
+
function renderQuality(data) {
|
|
743
|
+
var section = document.getElementById('quality-section');
|
|
744
|
+
var tbody = document.getElementById('quality-body');
|
|
745
|
+
tbody.innerHTML = '';
|
|
746
|
+
if (!data || !data.length) { section.style.display = 'none'; return; }
|
|
747
|
+
section.style.display = '';
|
|
748
|
+
data.forEach(function(row) {
|
|
749
|
+
var neg = row.negative_count || 0;
|
|
750
|
+
var score = row.quality_score || 0;
|
|
751
|
+
var barClass = score >= 0.5 ? 'quality-fill-ok' : (score >= 0.25 ? 'quality-fill-warn' : 'quality-fill-error');
|
|
752
|
+
var barWidth = Math.max(2, Math.round(score * 100));
|
|
753
|
+
var projLabel = row.project === '_global'
|
|
754
|
+
? '<span class="badge badge-global">Global</span>'
|
|
755
|
+
: esc(row.project);
|
|
756
|
+
var typeLabel = esc(row.mem_type || row.kind);
|
|
757
|
+
var negHtml = neg > 0
|
|
758
|
+
? '<span style="color:var(--error);font-weight:600;">' + neg + '</span>'
|
|
759
|
+
: '<span style="color:var(--text-muted);">0</span>';
|
|
760
|
+
var tr = el('tr', {});
|
|
761
|
+
tr.innerHTML =
|
|
762
|
+
'<td class="truncate" style="max-width:260px" title="' + esc(row.preview) + '">' + esc(row.preview) + '</td>' +
|
|
763
|
+
'<td>' + projLabel + '</td>' +
|
|
764
|
+
'<td><span class="mono" style="font-size:11px;color:var(--text-muted);">' + typeLabel + '</span></td>' +
|
|
765
|
+
'<td>' + (row.recall_count || 0) + '</td>' +
|
|
766
|
+
'<td style="color:var(--ok);">' + (row.use_count || 0) + '</td>' +
|
|
767
|
+
'<td>' + negHtml + '</td>' +
|
|
768
|
+
'<td class="mono">' + fmtScore(score) + '</td>' +
|
|
769
|
+
'<td><div class="quality-bar"><div class="quality-fill ' + barClass + '" style="width:' + barWidth + '%;"></div></div></td>';
|
|
770
|
+
tbody.appendChild(tr);
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
|
|
698
774
|
/* ── Projects ──────────────────────────────────────────── */
|
|
699
775
|
function renderProjects(data) {
|
|
700
776
|
projectsData = data;
|
|
@@ -737,9 +813,13 @@
|
|
|
737
813
|
sorted.forEach(function(row) {
|
|
738
814
|
var tr = el('tr', {});
|
|
739
815
|
if (projectFilter === row.project) tr.classList.add('selected');
|
|
816
|
+
var isGlobal = row.project === '_global';
|
|
817
|
+
var projCell = isGlobal
|
|
818
|
+
? '<td><span class="badge badge-global">Global</span></td>'
|
|
819
|
+
: '<td class="truncate mono" title="' + esc(row.project) + '">' + esc(row.project) + '</td>';
|
|
740
820
|
if (isRecallMode) {
|
|
741
821
|
tr.innerHTML =
|
|
742
|
-
|
|
822
|
+
projCell +
|
|
743
823
|
'<td>' + fmt(row.recalls) + '</td><td>' + fmt(row.tokens) + '</td>' +
|
|
744
824
|
'<td>' + fmtScore(row.avg_score) + '</td>' +
|
|
745
825
|
'<td style="color:var(--ok)">' + (row.ok_count||0) + '</td>' +
|
|
@@ -747,10 +827,10 @@
|
|
|
747
827
|
'<td style="color:var(--warn)">' + (row.extractive_count||0) + '</td>';
|
|
748
828
|
} else {
|
|
749
829
|
var hasMemories = (row.memories||0) > 0;
|
|
750
|
-
var badgeCls = hasMemories ? 'badge-ok' : 'badge-extractive';
|
|
751
|
-
var status = hasMemories ? 'Active' : 'Ingested';
|
|
830
|
+
var badgeCls = isGlobal ? 'badge-global' : (hasMemories ? 'badge-ok' : 'badge-extractive');
|
|
831
|
+
var status = isGlobal ? 'Global' : (hasMemories ? 'Active' : 'Ingested');
|
|
752
832
|
tr.innerHTML =
|
|
753
|
-
|
|
833
|
+
projCell +
|
|
754
834
|
'<td>' + fmt(row.chunks||0) + '</td><td>' + fmt(row.memories||0) + '</td>' +
|
|
755
835
|
'<td>' + fmt(row.total_tokens||0) + '</td>' +
|
|
756
836
|
'<td><span class="badge ' + badgeCls + '">' + status + '</span></td>' +
|
|
@@ -931,6 +1011,7 @@
|
|
|
931
1011
|
async function loadTrend() { try { renderTrend(await api('/api/recall-trend?days=30')); } catch(e) { console.warn('trend',e); } }
|
|
932
1012
|
async function loadROI() { try { renderROI(await api('/api/roi')); } catch(e) { console.warn('roi',e); } }
|
|
933
1013
|
async function loadROITrend() { try { renderROITrend(await api('/api/roi-trend')); } catch(e) { console.warn('roi-trend',e); } }
|
|
1014
|
+
async function loadQuality() { try { renderQuality(await api('/api/quality')); } catch(e) { console.warn('quality',e); } }
|
|
934
1015
|
async function loadRecalls() {
|
|
935
1016
|
try {
|
|
936
1017
|
var url = '/api/recalls?limit=50' + (projectFilter ? '&project=' + encodeURIComponent(projectFilter) : '');
|
|
@@ -939,7 +1020,7 @@
|
|
|
939
1020
|
}
|
|
940
1021
|
|
|
941
1022
|
async function refresh() {
|
|
942
|
-
await Promise.allSettled([loadSummary(), loadProjects(), loadEfficiency(), loadSessionEfficiency(), loadHealth(), loadTrend(), loadRecalls(), loadROI(), loadROITrend()]);
|
|
1023
|
+
await Promise.allSettled([loadSummary(), loadProjects(), loadEfficiency(), loadSessionEfficiency(), loadHealth(), loadTrend(), loadRecalls(), loadROI(), loadROITrend(), loadQuality()]);
|
|
943
1024
|
document.getElementById('last-updated').textContent = new Date().toLocaleTimeString();
|
|
944
1025
|
|
|
945
1026
|
renderMiniBars('mb-chunks', null);
|
|
@@ -956,6 +1037,12 @@
|
|
|
956
1037
|
});
|
|
957
1038
|
});
|
|
958
1039
|
|
|
1040
|
+
(async function() {
|
|
1041
|
+
try {
|
|
1042
|
+
var v = await api('/api/version');
|
|
1043
|
+
document.getElementById('version-badge').textContent = 'v' + v.version;
|
|
1044
|
+
} catch(e) {}
|
|
1045
|
+
})();
|
|
959
1046
|
refresh();
|
|
960
1047
|
setInterval(refresh, 30000);
|
|
961
1048
|
})();
|
|
@@ -19,9 +19,29 @@ _MIN_WORDS = 4
|
|
|
19
19
|
_MATCH_RATIO = 0.10
|
|
20
20
|
_SEMANTIC_SIM_THRESHOLD = 0.45
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
_REJECTION_PATTERNS = [
|
|
23
|
+
"no that's wrong", "that's not right", "that's incorrect", "that's outdated",
|
|
24
|
+
"no, we", "no we", "actually we", "actually, we",
|
|
25
|
+
"we switched", "we moved", "we changed", "we no longer",
|
|
26
|
+
"that's not how", "not what i meant", "wrong approach",
|
|
27
|
+
"we don't use", "we stopped using", "we dropped",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
_CONTRADICTION_PATTERNS = [
|
|
31
|
+
"however, looking at the current code",
|
|
32
|
+
"but the current implementation",
|
|
33
|
+
"actually use", "actually uses",
|
|
34
|
+
"instead of what the memory",
|
|
35
|
+
"contrary to the recalled",
|
|
36
|
+
"the memory is outdated",
|
|
37
|
+
"this is no longer",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _extract_texts(transcript_path: Path) -> tuple[list[str], list[str]]:
|
|
42
|
+
"""Extract (assistant_texts, user_texts) from a transcript JSONL."""
|
|
43
|
+
assistant_texts = []
|
|
44
|
+
user_texts = []
|
|
25
45
|
for line in transcript_path.read_text().splitlines():
|
|
26
46
|
line = line.strip()
|
|
27
47
|
if not line:
|
|
@@ -30,17 +50,40 @@ def _extract_assistant_texts(transcript_path: Path) -> list[str]:
|
|
|
30
50
|
rec = json.loads(line)
|
|
31
51
|
except json.JSONDecodeError:
|
|
32
52
|
continue
|
|
33
|
-
if rec.get("type") != "assistant":
|
|
34
|
-
continue
|
|
35
53
|
msg = rec.get("message", {})
|
|
36
54
|
content = msg.get("content", "")
|
|
55
|
+
parts = []
|
|
37
56
|
if isinstance(content, str):
|
|
38
|
-
|
|
57
|
+
parts.append(content.lower())
|
|
39
58
|
elif isinstance(content, list):
|
|
40
59
|
for block in content:
|
|
41
60
|
if isinstance(block, dict) and block.get("type") == "text":
|
|
42
|
-
|
|
43
|
-
|
|
61
|
+
parts.append(block.get("text", "").lower())
|
|
62
|
+
if rec.get("type") == "assistant":
|
|
63
|
+
assistant_texts.extend(parts)
|
|
64
|
+
elif rec.get("type") == "human":
|
|
65
|
+
user_texts.extend(parts)
|
|
66
|
+
return assistant_texts, user_texts
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _extract_assistant_texts(transcript_path: Path) -> list[str]:
|
|
70
|
+
assistant_texts, _ = _extract_texts(transcript_path)
|
|
71
|
+
return assistant_texts
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _detect_negative_signals(assistant_texts: list[str], user_texts: list[str]) -> bool:
|
|
75
|
+
"""Detect if user rejected or assistant contradicted recalled content."""
|
|
76
|
+
combined_user = " ".join(user_texts)
|
|
77
|
+
for pattern in _REJECTION_PATTERNS:
|
|
78
|
+
if pattern in combined_user:
|
|
79
|
+
return True
|
|
80
|
+
|
|
81
|
+
combined_assistant = " ".join(assistant_texts)
|
|
82
|
+
for pattern in _CONTRADICTION_PATTERNS:
|
|
83
|
+
if pattern in combined_assistant:
|
|
84
|
+
return True
|
|
85
|
+
|
|
86
|
+
return False
|
|
44
87
|
|
|
45
88
|
|
|
46
89
|
def _text_was_used(memory_text: str, assistant_texts: list[str]) -> bool:
|
|
@@ -106,7 +149,7 @@ def analyze_session_feedback(
|
|
|
106
149
|
if not recalled_ids:
|
|
107
150
|
return 0
|
|
108
151
|
|
|
109
|
-
assistant_texts =
|
|
152
|
+
assistant_texts, user_texts = _extract_texts(transcript_path)
|
|
110
153
|
if not assistant_texts:
|
|
111
154
|
return 0
|
|
112
155
|
|
|
@@ -126,4 +169,7 @@ def analyze_session_feedback(
|
|
|
126
169
|
if used_ids:
|
|
127
170
|
store.record_usage(used_ids)
|
|
128
171
|
|
|
172
|
+
if _detect_negative_signals(assistant_texts, user_texts) and recalled_ids:
|
|
173
|
+
store.record_negative(list(recalled_ids))
|
|
174
|
+
|
|
129
175
|
return len(used_ids)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Cross-project global memory promotion.
|
|
2
|
+
|
|
3
|
+
Detects patterns that appear across 3+ projects and promotes them to the
|
|
4
|
+
_global scope so they're recalled everywhere. Fully local — no API keys.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
import hashlib
|
|
8
|
+
import math
|
|
9
|
+
from memor.store.sqlite_store import SqliteStore
|
|
10
|
+
from memor.types import Artifact, Scope, GLOBAL_PROJECT
|
|
11
|
+
from memor.tokencount import count_tokens
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _cosine(a: list[float], b: list[float]) -> float:
|
|
15
|
+
dot = sum(x * y for x, y in zip(a, b))
|
|
16
|
+
na = math.sqrt(sum(x * x for x in a))
|
|
17
|
+
nb = math.sqrt(sum(x * x for x in b))
|
|
18
|
+
return dot / (na * nb) if na and nb else 0.0
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def find_promotion_candidates(
|
|
22
|
+
store: SqliteStore, embedder, *,
|
|
23
|
+
min_projects: int = 3, sim_threshold: float = 0.85,
|
|
24
|
+
) -> list[dict]:
|
|
25
|
+
"""Find memories that appear in min_projects+ different projects.
|
|
26
|
+
|
|
27
|
+
Returns list of dicts: {"text": str, "source_ids": [str], "projects": [str]}
|
|
28
|
+
"""
|
|
29
|
+
rows = store.db.execute(
|
|
30
|
+
"SELECT * FROM artifacts WHERE kind='memory' AND active=1 AND project != ?",
|
|
31
|
+
(GLOBAL_PROJECT,)
|
|
32
|
+
).fetchall()
|
|
33
|
+
if len(rows) < min_projects:
|
|
34
|
+
return []
|
|
35
|
+
|
|
36
|
+
memories = [store._row_to_artifact(r) for r in rows]
|
|
37
|
+
vecs = embedder.embed([m.text for m in memories])
|
|
38
|
+
|
|
39
|
+
clusters: list[dict] = []
|
|
40
|
+
assigned = set()
|
|
41
|
+
|
|
42
|
+
for i, mem in enumerate(memories):
|
|
43
|
+
if i in assigned:
|
|
44
|
+
continue
|
|
45
|
+
cluster = {"text": mem.text, "source_ids": [mem.id], "projects": {mem.project}}
|
|
46
|
+
assigned.add(i)
|
|
47
|
+
for j in range(i + 1, len(memories)):
|
|
48
|
+
if j in assigned:
|
|
49
|
+
continue
|
|
50
|
+
if memories[j].project in cluster["projects"]:
|
|
51
|
+
continue
|
|
52
|
+
sim = _cosine(vecs[i], vecs[j])
|
|
53
|
+
if sim >= sim_threshold:
|
|
54
|
+
cluster["source_ids"].append(memories[j].id)
|
|
55
|
+
cluster["projects"].add(memories[j].project)
|
|
56
|
+
assigned.add(j)
|
|
57
|
+
if len(cluster["projects"]) >= min_projects:
|
|
58
|
+
cluster["projects"] = sorted(cluster["projects"])
|
|
59
|
+
clusters.append(cluster)
|
|
60
|
+
|
|
61
|
+
return clusters
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def promote_to_global(
|
|
65
|
+
store: SqliteStore, embedder,
|
|
66
|
+
text: str, source_ids: list[str],
|
|
67
|
+
) -> str | None:
|
|
68
|
+
"""Create a _global memory from text and deactivate source duplicates.
|
|
69
|
+
|
|
70
|
+
Returns the new global memory ID, or None if a similar global already exists.
|
|
71
|
+
"""
|
|
72
|
+
vec = embedder.embed([text])[0]
|
|
73
|
+
|
|
74
|
+
existing = store.search(vec, Scope(project=GLOBAL_PROJECT, kinds=["memory"]), k=1)
|
|
75
|
+
if existing and existing[0][1] >= 0.85:
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
mid = f"mem:global:{hashlib.sha1(text.encode()).hexdigest()[:8]}"
|
|
79
|
+
import time
|
|
80
|
+
art = Artifact(
|
|
81
|
+
id=mid, kind="memory", project=GLOBAL_PROJECT, source="promotion",
|
|
82
|
+
text=text, token_count=max(1, count_tokens(text)), created_at=time.time(),
|
|
83
|
+
meta={"mem_type": "global", "promoted_from": source_ids},
|
|
84
|
+
)
|
|
85
|
+
store.add_artifacts([art], [vec])
|
|
86
|
+
|
|
87
|
+
for sid in source_ids:
|
|
88
|
+
store.deactivate(sid, superseded_by=mid)
|
|
89
|
+
|
|
90
|
+
return mid
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def run_promotion(store: SqliteStore, embedder, *, min_projects: int = 3) -> int:
|
|
94
|
+
"""Find and promote cross-project patterns. Returns count promoted."""
|
|
95
|
+
candidates = find_promotion_candidates(
|
|
96
|
+
store, embedder, min_projects=min_projects)
|
|
97
|
+
promoted = 0
|
|
98
|
+
for c in candidates:
|
|
99
|
+
mid = promote_to_global(store, embedder, c["text"], c["source_ids"])
|
|
100
|
+
if mid:
|
|
101
|
+
promoted += 1
|
|
102
|
+
return promoted
|