omind 10.3.4__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.
- omind/__init__.py +5 -0
- omind/__main__.py +8 -0
- omind/_omi_enforce.py +139 -0
- omind/access.py +226 -0
- omind/adapters.py +364 -0
- omind/agents.py +3895 -0
- omind/ai_usage.py +790 -0
- omind/audit.py +553 -0
- omind/backup.py +576 -0
- omind/bench.py +717 -0
- omind/checkpoint.py +367 -0
- omind/cli.py +2015 -0
- omind/clock.py +64 -0
- omind/compliance.py +393 -0
- omind/consolidate.py +277 -0
- omind/corpus.py +88 -0
- omind/deadline.py +145 -0
- omind/embed.py +209 -0
- omind/entities.py +479 -0
- omind/filelock.py +206 -0
- omind/fleet-sudo.sh +37 -0
- omind/git-fresh-base.sh +87 -0
- omind/graph.py +391 -0
- omind/guard.py +6098 -0
- omind/harness.py +808 -0
- omind/help_system.py +65 -0
- omind/hooks.py +1037 -0
- omind/journal.py +364 -0
- omind/learn.py +166 -0
- omind/lint.py +337 -0
- omind/loopguard.py +244 -0
- omind/maintain.py +192 -0
- omind/merge.py +461 -0
- omind/mesh.py +1209 -0
- omind/model_http.py +89 -0
- omind/namehints.py +729 -0
- omind/needle.py +344 -0
- omind/notes.py +80 -0
- omind/okf.py +152 -0
- omind/omi-gate-reset.sh +52 -0
- omind/omi-guard-hermes.sh +98 -0
- omind/omi-guard.dsh.js +390 -0
- omind/omi-guard.opencode.js +56 -0
- omind/omi-guard.sh +239 -0
- omind/paths.py +137 -0
- omind/policy.py +955 -0
- omind/proc.py +88 -0
- omind/provision.py +2764 -0
- omind/quickstart.py +530 -0
- omind/recall.py +166 -0
- omind/retrieve.py +500 -0
- omind/rules.py +1131 -0
- omind/scope_guard.py +73 -0
- omind/searchindex.py +2064 -0
- omind/secret-output-guard.sh +138 -0
- omind/seeds.py +405 -0
- omind/server.py +743 -0
- omind/skills/omind/SKILL.md +37 -0
- omind/skills/omind/agents/openai.yaml +4 -0
- omind/store.py +1835 -0
- omind/timeline.py +323 -0
- omind/transfer.py +347 -0
- omind/txn.py +429 -0
- omind/unwritten.py +129 -0
- omind/update.py +728 -0
- omind/verify.py +779 -0
- omind/web/__init__.py +3 -0
- omind/web/app.py +255 -0
- omind/web/static/app.css +841 -0
- omind/web/static/app.js +1062 -0
- omind/web/static/graph.js +336 -0
- omind/web/static/index.html +91 -0
- omind/web/static/vendor/fonts/bricolagegrotesque-1c7e673e77.woff2 +0 -0
- omind/web/static/vendor/fonts/bricolagegrotesque-40c2cb34af.woff2 +0 -0
- omind/web/static/vendor/fonts/bricolagegrotesque-fdb6b064da.woff2 +0 -0
- omind/web/static/vendor/fonts/jetbrainsmono-028edbf560.woff2 +0 -0
- omind/web/static/vendor/fonts/jetbrainsmono-0a7197f9e2.woff2 +0 -0
- omind/web/static/vendor/fonts/jetbrainsmono-acf87d8659.woff2 +0 -0
- omind/web/static/vendor/fonts/jetbrainsmono-c8b23d7bdf.woff2 +0 -0
- omind/web/static/vendor/fonts/jetbrainsmono-d2761cb6ca.woff2 +0 -0
- omind/web/static/vendor/fonts/jetbrainsmono-db1e206703.woff2 +0 -0
- omind/web/static/vendor/fonts/newsreader-1c520e8540.woff2 +0 -0
- omind/web/static/vendor/fonts/newsreader-2b5209ccd5.woff2 +0 -0
- omind/web/static/vendor/fonts/newsreader-34eec9781a.woff2 +0 -0
- omind/web/static/vendor/fonts/newsreader-72c495d39d.woff2 +0 -0
- omind/web/static/vendor/fonts/newsreader-a6a2a4bf8d.woff2 +0 -0
- omind/web/static/vendor/fonts/newsreader-f24c321523.woff2 +0 -0
- omind/web/static/vendor/fonts.css +251 -0
- omind/web/static/vendor/marked.min.js +69 -0
- omind/web/static/vendor/purify.min.js +3 -0
- omind/web/static/vendor/tailwind.css +2 -0
- omind/writecontext.py +463 -0
- omind-10.3.4.dist-info/METADATA +794 -0
- omind-10.3.4.dist-info/RECORD +97 -0
- omind-10.3.4.dist-info/WHEEL +4 -0
- omind-10.3.4.dist-info/entry_points.txt +3 -0
- omind-10.3.4.dist-info/licenses/LICENSE +201 -0
omind/__init__.py
ADDED
omind/__main__.py
ADDED
omind/_omi_enforce.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
omi-enforce.py — PostToolUse hook: prevent built-in Claude memory from persisting.
|
|
4
|
+
|
|
5
|
+
For each .md file found in ~/.claude/projects/*/memory/:
|
|
6
|
+
1. Parse name + description from YAML frontmatter
|
|
7
|
+
2. Check if a matching note already exists in the OMI vault (by filename)
|
|
8
|
+
3. If NOT found → migrate via `omind note` first
|
|
9
|
+
4. Delete the Claude memory file either way
|
|
10
|
+
"""
|
|
11
|
+
# Lazy annotations so the builtin-generic hints (dict[str, str]) don't need
|
|
12
|
+
# evaluation at import — this ships as a hook run under the host's system
|
|
13
|
+
# python3, whose version `requires-python` does not govern.
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import contextlib
|
|
17
|
+
import glob
|
|
18
|
+
import pathlib
|
|
19
|
+
import re
|
|
20
|
+
import subprocess
|
|
21
|
+
|
|
22
|
+
HOME = pathlib.Path.home()
|
|
23
|
+
OMIND = HOME / ".local/bin/omind"
|
|
24
|
+
VAULT = HOME / "Documents/Obsidian Vault"
|
|
25
|
+
OMI_DIR = VAULT / "OMI"
|
|
26
|
+
CLAUDE_PROJECTS = HOME / ".claude/projects"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def parse_frontmatter(text: str) -> tuple[dict[str, str], str]:
|
|
30
|
+
"""Return (fields_dict, body_text). Handles simple key: value and nested metadata.type."""
|
|
31
|
+
if not text.startswith("---"):
|
|
32
|
+
return {}, text
|
|
33
|
+
parts = text.split("---", 2)
|
|
34
|
+
if len(parts) < 3:
|
|
35
|
+
return {}, text
|
|
36
|
+
fm_raw, body = parts[1], parts[2].strip()
|
|
37
|
+
fields: dict[str, str] = {}
|
|
38
|
+
in_metadata = False
|
|
39
|
+
for line in fm_raw.splitlines():
|
|
40
|
+
if line.strip() == "metadata:":
|
|
41
|
+
in_metadata = True
|
|
42
|
+
continue
|
|
43
|
+
if in_metadata:
|
|
44
|
+
m = re.match(r"\s+(\w+):\s*(.*)", line)
|
|
45
|
+
if m:
|
|
46
|
+
fields[m.group(1)] = m.group(2).strip()
|
|
47
|
+
continue
|
|
48
|
+
else:
|
|
49
|
+
in_metadata = False
|
|
50
|
+
if ":" in line and not line.startswith(" "):
|
|
51
|
+
k, _, v = line.partition(":")
|
|
52
|
+
fields[k.strip()] = v.strip()
|
|
53
|
+
return fields, body
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def slug_to_title(slug: str) -> str:
|
|
57
|
+
return " ".join(w.capitalize() for w in re.split(r"[-_]", slug) if w)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def omi_exists(title: str) -> bool:
|
|
61
|
+
"""True only if the OMI vault already has a note with this EXACT filename.
|
|
62
|
+
|
|
63
|
+
A fuzzy "≥2 of 3 slug words appear in some filename" match used to declare a
|
|
64
|
+
memory already-covered and DELETE it without migrating its content — so
|
|
65
|
+
``docker-compose-tips`` was destroyed because a "Docker Compose Setup" note
|
|
66
|
+
existed. Exact match only: anything else is migrated before deletion.
|
|
67
|
+
"""
|
|
68
|
+
if not OMI_DIR.exists():
|
|
69
|
+
return False
|
|
70
|
+
safe_title = re.sub(r'[<>:"/\\|?*\x00-\x1f]', "", title).strip()
|
|
71
|
+
return bool(safe_title) and (OMI_DIR / f"{safe_title}.md").exists()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def migrate(title: str, summary: str, body: str, mem_type: str) -> bool:
|
|
75
|
+
"""Create a note in OMI via the omind CLI. Returns True on success."""
|
|
76
|
+
if not OMIND.exists():
|
|
77
|
+
return False
|
|
78
|
+
tags = f"claude-memory,{mem_type}" if mem_type else "claude-memory"
|
|
79
|
+
cmd = [
|
|
80
|
+
str(OMIND), "note",
|
|
81
|
+
"--vault", str(VAULT),
|
|
82
|
+
"--folder", "OMI",
|
|
83
|
+
"--title", title,
|
|
84
|
+
"--summary", summary or title,
|
|
85
|
+
"--tags", tags,
|
|
86
|
+
]
|
|
87
|
+
try:
|
|
88
|
+
# Timeout so a hung `omind note` (vault lock held by mesh sync, gpg
|
|
89
|
+
# pinentry, NFS stall) can't hang this PostToolUse hook — and therefore
|
|
90
|
+
# every agent turn — indefinitely.
|
|
91
|
+
result = subprocess.run(
|
|
92
|
+
cmd, input=body, text=True, capture_output=True, timeout=30
|
|
93
|
+
)
|
|
94
|
+
except (subprocess.SubprocessError, OSError):
|
|
95
|
+
return False
|
|
96
|
+
return result.returncode == 0
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _safe_unlink(path: pathlib.Path) -> None:
|
|
100
|
+
"""Delete a migrated memory file; a permission/read-only-FS error must not
|
|
101
|
+
crash a hook that fires on every tool call."""
|
|
102
|
+
with contextlib.suppress(OSError):
|
|
103
|
+
path.unlink(missing_ok=True)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def main() -> None:
|
|
107
|
+
pattern = str(CLAUDE_PROJECTS / "*/memory/*.md")
|
|
108
|
+
for filepath in glob.glob(pattern):
|
|
109
|
+
path = pathlib.Path(filepath)
|
|
110
|
+
|
|
111
|
+
# Always nuke stale MEMORY.md index files (a generated pointer, no content)
|
|
112
|
+
if path.name == "MEMORY.md":
|
|
113
|
+
_safe_unlink(path)
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
content = path.read_text(encoding="utf-8", errors="replace")
|
|
118
|
+
except OSError:
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
fm, body = parse_frontmatter(content)
|
|
122
|
+
slug = fm.get("name", "").strip()
|
|
123
|
+
description = fm.get("description", "").strip()
|
|
124
|
+
mem_type = fm.get("type", "").strip()
|
|
125
|
+
|
|
126
|
+
# A file with no `name:` slug still holds memory content — derive a title
|
|
127
|
+
# from its filename and MIGRATE it before deleting (never unlink blind).
|
|
128
|
+
title = slug_to_title(slug) if slug else slug_to_title(path.stem)
|
|
129
|
+
|
|
130
|
+
if omi_exists(title):
|
|
131
|
+
_safe_unlink(path) # content already in OMI under this exact title
|
|
132
|
+
elif migrate(title, description, body, mem_type):
|
|
133
|
+
_safe_unlink(path)
|
|
134
|
+
# If migration fails (or omind is unavailable), LEAVE the file — the whole
|
|
135
|
+
# point is to never lose memory content.
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
if __name__ == "__main__":
|
|
139
|
+
main()
|
omind/access.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright 2026 Aaron K. Clark
|
|
3
|
+
"""Machine-local access statistics for dynamic core-memory selection.
|
|
4
|
+
|
|
5
|
+
Two independent consumers share the one ``access`` table (2026-08-27 roundtable,
|
|
6
|
+
item #2): the #173 tiered-core scorer (:func:`core_members`) and the usefulness
|
|
7
|
+
feedback leg (:func:`usefulness_weights`). They *share the table and keep their
|
|
8
|
+
own scorer* — the two effects are never blended, or a note would be counted
|
|
9
|
+
twice.
|
|
10
|
+
|
|
11
|
+
The split that keeps them honest lives in the schema:
|
|
12
|
+
|
|
13
|
+
* ``count`` / ``last_access`` — every read, no dedupe. Feeds #173 unchanged; the
|
|
14
|
+
usefulness leg never looks at these.
|
|
15
|
+
* ``organic_count`` / ``organic_last`` — only *organic* reads (an agent
|
|
16
|
+
explicitly calling read-note/recall-note), deduped once per session. This is
|
|
17
|
+
the clean usefulness signal. Capsule/hook/turn-preflight reads are excluded
|
|
18
|
+
(their position basis is not evidence a human found the note useful — the
|
|
19
|
+
Cortana/Wintermute concern the panel adopted as mandatory).
|
|
20
|
+
* ``filtered_count`` — reads the usefulness leg deliberately dropped
|
|
21
|
+
(non-organic, or an organic re-read already seen this session). Kept only so
|
|
22
|
+
``search --explain`` can show *counted and filtered* reads side by side.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import contextlib
|
|
28
|
+
import math
|
|
29
|
+
import sqlite3
|
|
30
|
+
import time
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
from omind import paths
|
|
34
|
+
|
|
35
|
+
_MAX_AGE_DAYS = 90
|
|
36
|
+
|
|
37
|
+
#: Usefulness decay (item #2). Decay-only, no boost: a term that only subtracts
|
|
38
|
+
#: cannot self-reinforce, so this can only ever *demote* an organically-read
|
|
39
|
+
#: note that has since gone stale — never push a popular one up. The floor and
|
|
40
|
+
#: the 30-day onset are HAL9000's dissent, adopted: a note read recently (or
|
|
41
|
+
#: never read at all — new/never-surfaced) is untouched, and even a long-stale
|
|
42
|
+
#: note bottoms out at the floor rather than being buried.
|
|
43
|
+
_USEFUL_ONSET_DAYS = 30.0
|
|
44
|
+
_USEFUL_FLOOR = 0.7
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _ensure_schema(db: sqlite3.Connection) -> None:
|
|
48
|
+
db.execute(
|
|
49
|
+
"CREATE TABLE IF NOT EXISTS access ("
|
|
50
|
+
"filename TEXT PRIMARY KEY, count INTEGER NOT NULL, last_access REAL NOT NULL,"
|
|
51
|
+
" organic_count INTEGER NOT NULL DEFAULT 0,"
|
|
52
|
+
" organic_last REAL NOT NULL DEFAULT 0,"
|
|
53
|
+
" filtered_count INTEGER NOT NULL DEFAULT 0)"
|
|
54
|
+
)
|
|
55
|
+
# Migrate installs created before the usefulness columns existed. A duplicate
|
|
56
|
+
# column raises OperationalError, which is the "already migrated" signal.
|
|
57
|
+
for column, decl in (
|
|
58
|
+
("organic_count", "INTEGER NOT NULL DEFAULT 0"),
|
|
59
|
+
("organic_last", "REAL NOT NULL DEFAULT 0"),
|
|
60
|
+
("filtered_count", "INTEGER NOT NULL DEFAULT 0"),
|
|
61
|
+
):
|
|
62
|
+
with contextlib.suppress(sqlite3.OperationalError):
|
|
63
|
+
db.execute(f"ALTER TABLE access ADD COLUMN {column} {decl}")
|
|
64
|
+
db.execute(
|
|
65
|
+
"CREATE TABLE IF NOT EXISTS read_session ("
|
|
66
|
+
"filename TEXT NOT NULL, session TEXT NOT NULL, PRIMARY KEY(filename, session))"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def record(
|
|
71
|
+
omi_dir: Path | str,
|
|
72
|
+
filename: str,
|
|
73
|
+
*,
|
|
74
|
+
session: str = "",
|
|
75
|
+
organic: bool = True,
|
|
76
|
+
now: float | None = None,
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Record one note read. Advisory and fail-open.
|
|
79
|
+
|
|
80
|
+
``count``/``last_access`` always advance (the #173 scorer's inputs are
|
|
81
|
+
unchanged from before this table was shared). The usefulness columns advance
|
|
82
|
+
only for an ``organic`` read not already seen this ``session``; every other
|
|
83
|
+
read lands in ``filtered_count`` instead so ``--explain`` can account for it.
|
|
84
|
+
"""
|
|
85
|
+
path = paths.access_state_path(Path(omi_dir))
|
|
86
|
+
stamp = now if now is not None else time.time()
|
|
87
|
+
try:
|
|
88
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
89
|
+
with sqlite3.connect(path, timeout=2.0) as db:
|
|
90
|
+
db.execute("PRAGMA busy_timeout=2000")
|
|
91
|
+
_ensure_schema(db)
|
|
92
|
+
db.execute(
|
|
93
|
+
"INSERT INTO access(filename, count, last_access) VALUES (?, 1, ?)"
|
|
94
|
+
" ON CONFLICT(filename) DO UPDATE SET"
|
|
95
|
+
" count = count + 1, last_access = excluded.last_access",
|
|
96
|
+
(filename, stamp),
|
|
97
|
+
)
|
|
98
|
+
first_this_session = False
|
|
99
|
+
if organic:
|
|
100
|
+
# A session id lets a burst of re-reads count once; an unknown
|
|
101
|
+
# session ("") cannot be deduped, so each such read counts (rare
|
|
102
|
+
# — only non-MCP entry points omit it).
|
|
103
|
+
if session:
|
|
104
|
+
cur = db.execute(
|
|
105
|
+
"INSERT OR IGNORE INTO read_session(filename, session)"
|
|
106
|
+
" VALUES (?, ?)",
|
|
107
|
+
(filename, session),
|
|
108
|
+
)
|
|
109
|
+
first_this_session = cur.rowcount > 0
|
|
110
|
+
else:
|
|
111
|
+
first_this_session = True
|
|
112
|
+
if first_this_session:
|
|
113
|
+
db.execute(
|
|
114
|
+
"UPDATE access SET organic_count = organic_count + 1,"
|
|
115
|
+
" organic_last = ? WHERE filename = ?",
|
|
116
|
+
(stamp, filename),
|
|
117
|
+
)
|
|
118
|
+
else:
|
|
119
|
+
db.execute(
|
|
120
|
+
"UPDATE access SET filtered_count = filtered_count + 1"
|
|
121
|
+
" WHERE filename = ?",
|
|
122
|
+
(filename,),
|
|
123
|
+
)
|
|
124
|
+
except (OSError, sqlite3.Error):
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def core_members(
|
|
129
|
+
omi_dir: Path | str,
|
|
130
|
+
*,
|
|
131
|
+
limit: int = 3,
|
|
132
|
+
now: float | None = None,
|
|
133
|
+
max_age_days: int = _MAX_AGE_DAYS,
|
|
134
|
+
) -> list[str]:
|
|
135
|
+
"""Best recently/frequently read notes, newest score first (#173 scorer)."""
|
|
136
|
+
path = paths.access_state_path(Path(omi_dir))
|
|
137
|
+
if limit <= 0 or not path.is_file():
|
|
138
|
+
return []
|
|
139
|
+
current = now if now is not None else time.time()
|
|
140
|
+
cutoff = current - max_age_days * 86_400
|
|
141
|
+
try:
|
|
142
|
+
with sqlite3.connect(f"{path.resolve().as_uri()}?mode=ro", uri=True, timeout=1.0) as db:
|
|
143
|
+
rows = db.execute(
|
|
144
|
+
"SELECT filename, count, last_access FROM access WHERE last_access >= ?",
|
|
145
|
+
(cutoff,),
|
|
146
|
+
)
|
|
147
|
+
scored = [
|
|
148
|
+
(
|
|
149
|
+
math.log1p(int(count))
|
|
150
|
+
+ 1.0 / (1.0 + max(0.0, current - float(last_access)) / 2_592_000),
|
|
151
|
+
float(last_access),
|
|
152
|
+
str(filename),
|
|
153
|
+
)
|
|
154
|
+
for filename, count, last_access in rows
|
|
155
|
+
]
|
|
156
|
+
except (OSError, sqlite3.Error, TypeError, ValueError):
|
|
157
|
+
return []
|
|
158
|
+
scored.sort(key=lambda item: (-item[0], -item[1], item[2]))
|
|
159
|
+
return [filename for _score, _last_access, filename in scored[:limit]]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class Usefulness:
|
|
163
|
+
"""One note's usefulness signal, as ``search --explain`` reports it."""
|
|
164
|
+
|
|
165
|
+
__slots__ = ("weight", "useful_reads", "filtered_reads")
|
|
166
|
+
|
|
167
|
+
def __init__(self, weight: float, useful_reads: int, filtered_reads: int) -> None:
|
|
168
|
+
#: Multiplicative demotion in ``[floor, 1.0]``; 1.0 means untouched.
|
|
169
|
+
self.weight = weight
|
|
170
|
+
#: Organic, per-session-deduped reads (the counted signal).
|
|
171
|
+
self.useful_reads = useful_reads
|
|
172
|
+
#: Reads excluded as non-organic or same-session re-reads.
|
|
173
|
+
self.filtered_reads = filtered_reads
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _decay_weight(organic_last: float, organic_count: int, current: float) -> float:
|
|
177
|
+
"""Demotion for one note. Decay-only: never above 1.0.
|
|
178
|
+
|
|
179
|
+
A note never read organically, or last read within the onset window, is
|
|
180
|
+
untouched (1.0). Past the onset it decays linearly toward the floor, reached
|
|
181
|
+
at ``_MAX_AGE_DAYS`` — a gentle reorder nudge, never a bury.
|
|
182
|
+
"""
|
|
183
|
+
if organic_count <= 0 or organic_last <= 0:
|
|
184
|
+
return 1.0
|
|
185
|
+
age_days = max(0.0, (current - organic_last) / 86_400)
|
|
186
|
+
if age_days <= _USEFUL_ONSET_DAYS:
|
|
187
|
+
return 1.0
|
|
188
|
+
span = _MAX_AGE_DAYS - _USEFUL_ONSET_DAYS
|
|
189
|
+
frac = 1.0 if span <= 0 else min(1.0, (age_days - _USEFUL_ONSET_DAYS) / span)
|
|
190
|
+
return 1.0 - (1.0 - _USEFUL_FLOOR) * frac
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def usefulness_weights(
|
|
194
|
+
omi_dir: Path | str,
|
|
195
|
+
filenames: set[str] | None = None,
|
|
196
|
+
*,
|
|
197
|
+
now: float | None = None,
|
|
198
|
+
) -> dict[str, Usefulness]:
|
|
199
|
+
"""Usefulness demotion for each note (item #2 scorer), fail-open to ``{}``.
|
|
200
|
+
|
|
201
|
+
Reads only the ``organic_*``/``filtered_count`` columns — never ``count``,
|
|
202
|
+
which belongs to #173. Restricted to ``filenames`` when given so the search
|
|
203
|
+
path never scores notes it did not already match (reorder-never-add)."""
|
|
204
|
+
path = paths.access_state_path(Path(omi_dir))
|
|
205
|
+
if not path.is_file():
|
|
206
|
+
return {}
|
|
207
|
+
current = now if now is not None else time.time()
|
|
208
|
+
try:
|
|
209
|
+
with sqlite3.connect(f"{path.resolve().as_uri()}?mode=ro", uri=True, timeout=1.0) as db:
|
|
210
|
+
rows = db.execute(
|
|
211
|
+
"SELECT filename, organic_count, organic_last, filtered_count FROM access"
|
|
212
|
+
).fetchall()
|
|
213
|
+
except (OSError, sqlite3.Error):
|
|
214
|
+
return {}
|
|
215
|
+
out: dict[str, Usefulness] = {}
|
|
216
|
+
for filename, organic_count, organic_last, filtered_count in rows:
|
|
217
|
+
name = str(filename)
|
|
218
|
+
if filenames is not None and name not in filenames:
|
|
219
|
+
continue
|
|
220
|
+
with contextlib.suppress(TypeError, ValueError):
|
|
221
|
+
out[name] = Usefulness(
|
|
222
|
+
weight=_decay_weight(float(organic_last), int(organic_count), current),
|
|
223
|
+
useful_reads=int(organic_count),
|
|
224
|
+
filtered_reads=int(filtered_count),
|
|
225
|
+
)
|
|
226
|
+
return out
|