msgsearch 0.1.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.
- msgsearch-0.1.0/.claude/agents/corpus-explorer.md +22 -0
- msgsearch-0.1.0/.claude/agents/retrieval-critic.md +30 -0
- msgsearch-0.1.0/.claude/commands/bench.md +12 -0
- msgsearch-0.1.0/.claude/commands/go.md +17 -0
- msgsearch-0.1.0/.claude/commands/recon.md +8 -0
- msgsearch-0.1.0/.claude/hooks/verify-retrieval.py +114 -0
- msgsearch-0.1.0/.claude/settings.json +52 -0
- msgsearch-0.1.0/.github/workflows/ci.yml +68 -0
- msgsearch-0.1.0/.github/workflows/release.yml +62 -0
- msgsearch-0.1.0/.gitignore +27 -0
- msgsearch-0.1.0/AGENTS.md +66 -0
- msgsearch-0.1.0/ARCHITECTURE.md +249 -0
- msgsearch-0.1.0/CHANGELOG.md +38 -0
- msgsearch-0.1.0/CLAUDE.md +1 -0
- msgsearch-0.1.0/CONTRIBUTING.md +100 -0
- msgsearch-0.1.0/LICENSE +21 -0
- msgsearch-0.1.0/PKG-INFO +303 -0
- msgsearch-0.1.0/README.md +257 -0
- msgsearch-0.1.0/eval/__init__.py +0 -0
- msgsearch-0.1.0/eval/bench.py +160 -0
- msgsearch-0.1.0/eval/gold.jsonl +9 -0
- msgsearch-0.1.0/eval/label.py +156 -0
- msgsearch-0.1.0/eval/results/baseline.json +29 -0
- msgsearch-0.1.0/eval/results/retriever.json +23 -0
- msgsearch-0.1.0/eval/results/retriever_bm25.json +29 -0
- msgsearch-0.1.0/eval/results/retriever_dense.json +29 -0
- msgsearch-0.1.0/eval/results/retriever_norerank.json +29 -0
- msgsearch-0.1.0/eval/retriever.py +94 -0
- msgsearch-0.1.0/eval/retriever_bm25.py +7 -0
- msgsearch-0.1.0/eval/retriever_dense.py +7 -0
- msgsearch-0.1.0/eval/retriever_norerank.py +7 -0
- msgsearch-0.1.0/pyproject.toml +81 -0
- msgsearch-0.1.0/src/msgsearch/__init__.py +18 -0
- msgsearch-0.1.0/src/msgsearch/attributed_body.py +59 -0
- msgsearch-0.1.0/src/msgsearch/chunk.py +268 -0
- msgsearch-0.1.0/src/msgsearch/cli.py +304 -0
- msgsearch-0.1.0/src/msgsearch/config.py +107 -0
- msgsearch-0.1.0/src/msgsearch/contacts.py +281 -0
- msgsearch-0.1.0/src/msgsearch/doctor.py +245 -0
- msgsearch-0.1.0/src/msgsearch/embedder.py +126 -0
- msgsearch-0.1.0/src/msgsearch/explore.py +157 -0
- msgsearch-0.1.0/src/msgsearch/extract.py +246 -0
- msgsearch-0.1.0/src/msgsearch/index.py +298 -0
- msgsearch-0.1.0/src/msgsearch/py.typed +0 -0
- msgsearch-0.1.0/src/msgsearch/search.py +447 -0
- msgsearch-0.1.0/src/msgsearch/tagging.py +123 -0
- msgsearch-0.1.0/tests/test_chunk.py +150 -0
- msgsearch-0.1.0/tests/test_cli.py +77 -0
- msgsearch-0.1.0/tests/test_contacts.py +178 -0
- msgsearch-0.1.0/tests/test_index.py +106 -0
- msgsearch-0.1.0/tests/test_search.py +39 -0
- msgsearch-0.1.0/tests/test_tagging.py +85 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: corpus-explorer
|
|
3
|
+
description: Answers structural questions about chat.db (schema, joins, Apple quirks) without exposing message content.
|
|
4
|
+
tools: Bash, Read, Grep
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You answer questions about the *shape* of the iMessage corpus — schema, joins,
|
|
8
|
+
distributions, encoding quirks — never its content.
|
|
9
|
+
|
|
10
|
+
Rules:
|
|
11
|
+
- Open only `~/msgsearch/chat.db` and only read-only (`file:...?mode=ro`).
|
|
12
|
+
`~/Library/Messages` is off limits; refuse if asked.
|
|
13
|
+
- Report aggregates, counts, and schema. Never print message bodies. If an example
|
|
14
|
+
is genuinely necessary to explain an encoding quirk, show a hexdump of the
|
|
15
|
+
structural bytes, not the decoded text.
|
|
16
|
+
- Known quirks to account for, and to check before trusting any query:
|
|
17
|
+
`message.date` mixes second- and nanosecond-scale Apple-epoch values; ~86% of
|
|
18
|
+
rows carry text in `attributedBody` rather than `text`;
|
|
19
|
+
`associated_message_type != 0` rows are tapbacks, not messages;
|
|
20
|
+
some messages have no `chat_message_join` row at all.
|
|
21
|
+
|
|
22
|
+
Return findings as a short table plus the SQL you ran, so it can be re-verified.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: retrieval-critic
|
|
3
|
+
description: Adversarially audits a retrieval change. Use after any change to ranking, chunking, embedding, or fusion — before claiming it works.
|
|
4
|
+
tools: Bash, Read, Grep
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You are a skeptical IR engineer. Your job is to find the reason the reported
|
|
8
|
+
improvement is fake. Assume it is until proven otherwise.
|
|
9
|
+
|
|
10
|
+
Check, in order:
|
|
11
|
+
|
|
12
|
+
1. **Is it measured at all?** Run `python3 eval/bench.py`. Queries with an empty
|
|
13
|
+
`relevant` list are scored as nothing. If the labeled set is under ~20 queries,
|
|
14
|
+
say plainly that the result is noise, not signal.
|
|
15
|
+
2. **Test-set contamination.** Was `eval/gold.jsonl` edited in the same change as
|
|
16
|
+
the retriever? Check `git diff`. Labeling the gold set to match new output is
|
|
17
|
+
the most common way to fake a win here.
|
|
18
|
+
3. **Metric selection.** Did the report cite only the metric that moved? Show all
|
|
19
|
+
four (p@10, r@50, MRR, nDCG@10). A gain in p@10 with a drop in r@50 usually
|
|
20
|
+
means the change narrowed recall rather than improving ranking.
|
|
21
|
+
4. **Degenerate wins.** Does the retriever return tapbacks, duplicates, or the same
|
|
22
|
+
message under multiple ids? Does it work on short queries and one-word queries?
|
|
23
|
+
Try a query with no plausible answer and confirm it returns few or no results
|
|
24
|
+
rather than confident garbage.
|
|
25
|
+
5. **Privacy.** Does the diff introduce anything that prints message bodies to
|
|
26
|
+
stdout in committed code, or sends text off the machine? This is a hard stop.
|
|
27
|
+
|
|
28
|
+
Report only defects you actually confirmed by running something. For each, give the
|
|
29
|
+
command you ran and its output. If the change is genuinely sound, say so in one line
|
|
30
|
+
— do not invent findings to seem useful.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Run the retrieval eval and report the before/after metrics table
|
|
3
|
+
---
|
|
4
|
+
Run the verification loop for this project:
|
|
5
|
+
|
|
6
|
+
1. `python3 eval/bench.py --against baseline`
|
|
7
|
+
2. If `index.sqlite` is missing, rebuild it first with `python3 search/index.py`.
|
|
8
|
+
3. Report the metrics table verbatim. Do NOT summarize it away or round the numbers.
|
|
9
|
+
4. If any metric regressed, say so plainly and explain which change caused it.
|
|
10
|
+
5. If there are unlabeled gold queries, list them — they measure nothing until labeled.
|
|
11
|
+
|
|
12
|
+
A retrieval change that does not move these numbers is not an improvement.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Verify, simplify, and commit the current change
|
|
3
|
+
---
|
|
4
|
+
Finish the current change end to end:
|
|
5
|
+
|
|
6
|
+
1. **Verify** — run `python3 eval/bench.py --against baseline` and paste the table.
|
|
7
|
+
For non-retrieval changes, run the affected script directly and show real output.
|
|
8
|
+
Never claim something works without having run it.
|
|
9
|
+
2. **Simplify** — reread the diff. Remove dead code, collapse needless abstraction,
|
|
10
|
+
and check it matches the conventions in AGENTS.md.
|
|
11
|
+
3. **Record** — if you got anything wrong during this task, append the correction to
|
|
12
|
+
the "Learned corrections" section of AGENTS.md. This is not optional; it is how
|
|
13
|
+
the project gets easier over time.
|
|
14
|
+
4. **Commit** — stage and commit with a message describing the *why*. Do not push
|
|
15
|
+
unless asked.
|
|
16
|
+
|
|
17
|
+
Confirm no message bodies appear in the diff before committing.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Re-run structural recon on the message corpus
|
|
3
|
+
---
|
|
4
|
+
Run `python3 explore.py` and interpret the output: message volume, decode coverage,
|
|
5
|
+
date range, top chats. Flag anything that changed since the numbers recorded in
|
|
6
|
+
AGENTS.md, and update AGENTS.md if the corpus shape has shifted.
|
|
7
|
+
|
|
8
|
+
Never print message bodies.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Stop hook: refuse to finish a retrieval change that made retrieval worse.
|
|
3
|
+
|
|
4
|
+
Must be launched with .venv/bin/python (settings.json does this). Under the
|
|
5
|
+
system python3, which is an x86_64 build, child processes inherit that
|
|
6
|
+
architecture and the arm64 numpy in the venv fails to load -- the bench then
|
|
7
|
+
exits non-zero and the check silently passes.
|
|
8
|
+
|
|
9
|
+
AGENTS.md asks an agent to justify retrieval changes with eval/bench.py. This
|
|
10
|
+
turns that request into a wall, because a request is something a model can
|
|
11
|
+
forget and a hook is not.
|
|
12
|
+
|
|
13
|
+
Fires only when a file that can change ranking was touched, and only when the
|
|
14
|
+
gold set actually has labels -- there is nothing to measure otherwise.
|
|
15
|
+
|
|
16
|
+
Exit 0 allows the stop. Exit 2 blocks it and sends stderr back to the agent.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import contextlib
|
|
20
|
+
import hashlib
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import re
|
|
24
|
+
import subprocess
|
|
25
|
+
import sys
|
|
26
|
+
import tempfile
|
|
27
|
+
|
|
28
|
+
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
29
|
+
PY_BIN = os.path.join(ROOT, ".venv", "bin", "python")
|
|
30
|
+
BASELINE = os.path.join(ROOT, "eval", "results", "baseline.json")
|
|
31
|
+
GOLD = os.path.join(ROOT, "eval", "gold.jsonl")
|
|
32
|
+
|
|
33
|
+
# Anything in the engine package can move a ranking, as can the eval adapter.
|
|
34
|
+
# Matched as path prefixes so this survives files being added or renamed -- an
|
|
35
|
+
# earlier version listed modules by bare name and broke the moment the project
|
|
36
|
+
# was restructured into src/.
|
|
37
|
+
WATCHED_PREFIXES = ("src/msgsearch/", "eval/retriever")
|
|
38
|
+
|
|
39
|
+
# A drop smaller than this is noise at the current gold-set size, not a regression.
|
|
40
|
+
TOLERANCE = 0.02
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def sh(*args):
|
|
44
|
+
return subprocess.run(args, cwd=ROOT, capture_output=True, text=True).stdout
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def touched():
|
|
48
|
+
diff = sh("git", "diff", "HEAD", "--name-only") + sh("git", "diff", "--name-only")
|
|
49
|
+
files = {line.strip() for line in diff.splitlines() if line.strip()}
|
|
50
|
+
return sorted(f for f in files if f.startswith(WATCHED_PREFIXES))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def labeled_queries():
|
|
54
|
+
if not os.path.exists(GOLD):
|
|
55
|
+
return 0
|
|
56
|
+
with open(GOLD) as f:
|
|
57
|
+
return sum(1 for line in f if line.strip() and json.loads(line).get("relevant"))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def already_blocked_for(state):
|
|
61
|
+
"""Block at most once per code state, so a change the agent cannot fix
|
|
62
|
+
does not trap it in a loop."""
|
|
63
|
+
marker = os.path.join(tempfile.gettempdir(), f"msgsearch-hook-{state}")
|
|
64
|
+
if os.path.exists(marker):
|
|
65
|
+
return True
|
|
66
|
+
open(marker, "w").close()
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def main():
|
|
71
|
+
with contextlib.suppress(Exception):
|
|
72
|
+
json.load(sys.stdin)
|
|
73
|
+
|
|
74
|
+
files = touched()
|
|
75
|
+
if not files or not os.path.exists(PY_BIN):
|
|
76
|
+
return 0
|
|
77
|
+
if labeled_queries() < 3 or not os.path.exists(BASELINE):
|
|
78
|
+
return 0
|
|
79
|
+
|
|
80
|
+
proc = subprocess.run(
|
|
81
|
+
[PY_BIN, "eval/bench.py", "-r", "eval.retriever", "--against", "baseline"],
|
|
82
|
+
cwd=ROOT,
|
|
83
|
+
capture_output=True,
|
|
84
|
+
text=True,
|
|
85
|
+
timeout=600,
|
|
86
|
+
)
|
|
87
|
+
out = proc.stdout
|
|
88
|
+
match = re.search(r"^mrr\s+([\d.]+)", out, re.M)
|
|
89
|
+
if not match:
|
|
90
|
+
return 0
|
|
91
|
+
|
|
92
|
+
mrr = float(match.group(1))
|
|
93
|
+
base = json.load(open(BASELINE))["metrics"]["mrr"]
|
|
94
|
+
if mrr >= base - TOLERANCE:
|
|
95
|
+
return 0
|
|
96
|
+
|
|
97
|
+
state = hashlib.sha1((sh("git", "diff", "HEAD") + f"{mrr}").encode()).hexdigest()[:12]
|
|
98
|
+
if already_blocked_for(state):
|
|
99
|
+
return 0
|
|
100
|
+
|
|
101
|
+
print(
|
|
102
|
+
f"Retrieval regression: MRR {base:.4f} -> {mrr:.4f} "
|
|
103
|
+
f"({mrr - base:+.4f}) after editing {', '.join(files)}.\n\n"
|
|
104
|
+
f"{out.strip()}\n\n"
|
|
105
|
+
"Either fix it, or revert the change. If the drop is intended and "
|
|
106
|
+
"justified, say so explicitly and re-snapshot with "
|
|
107
|
+
"`eval/bench.py -r eval.retriever --save baseline`.",
|
|
108
|
+
file=sys.stderr,
|
|
109
|
+
)
|
|
110
|
+
return 2
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
if __name__ == "__main__":
|
|
114
|
+
sys.exit(main())
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(./.venv/bin/python:*)",
|
|
5
|
+
"Bash(git add:*)",
|
|
6
|
+
"Bash(git checkout:*)",
|
|
7
|
+
"Bash(git commit:*)",
|
|
8
|
+
"Bash(git diff:*)",
|
|
9
|
+
"Bash(git log:*)",
|
|
10
|
+
"Bash(git status:*)",
|
|
11
|
+
"Bash(python3 -c:*)",
|
|
12
|
+
"Bash(python3 eval/bench.py:*)",
|
|
13
|
+
"Bash(python3 explore.py)",
|
|
14
|
+
"Bash(python3 search/index.py:*)",
|
|
15
|
+
"Edit(./AGENTS.md)",
|
|
16
|
+
"Edit(./eval/**)",
|
|
17
|
+
"Edit(./search/**)",
|
|
18
|
+
"Read(./**)"
|
|
19
|
+
],
|
|
20
|
+
"deny": [
|
|
21
|
+
"Read(~/Library/Messages/**)",
|
|
22
|
+
"Bash(sqlite3 ~/Library/Messages/**)",
|
|
23
|
+
"Write(~/msgsearch/**)"
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
"hooks": {
|
|
27
|
+
"PostToolUse": [
|
|
28
|
+
{
|
|
29
|
+
"matcher": "Write|Edit",
|
|
30
|
+
"hooks": [
|
|
31
|
+
{
|
|
32
|
+
"type": "command",
|
|
33
|
+
"command": "python3 -c \"import ast,sys,glob; [ast.parse(open(f).read(),f) for f in glob.glob('**/*.py',recursive=True) if '.venv' not in f]\" || true"
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"Stop": [
|
|
39
|
+
{
|
|
40
|
+
"matcher": "*",
|
|
41
|
+
"hooks": [
|
|
42
|
+
{
|
|
43
|
+
"type": "command",
|
|
44
|
+
"command": "${CLAUDE_PROJECT_DIR}/.venv/bin/python ${CLAUDE_PROJECT_DIR}/.claude/hooks/verify-retrieval.py",
|
|
45
|
+
"timeout": 600,
|
|
46
|
+
"statusMessage": "checking retrieval didn't regress"
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
test:
|
|
15
|
+
name: py${{ matrix.python-version }} on ${{ matrix.os }}
|
|
16
|
+
runs-on: ${{ matrix.os }}
|
|
17
|
+
strategy:
|
|
18
|
+
fail-fast: false
|
|
19
|
+
matrix:
|
|
20
|
+
os: [ubuntu-latest]
|
|
21
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
22
|
+
include:
|
|
23
|
+
- os: macos-latest
|
|
24
|
+
python-version: "3.12"
|
|
25
|
+
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v4
|
|
28
|
+
|
|
29
|
+
- uses: actions/setup-python@v5
|
|
30
|
+
with:
|
|
31
|
+
python-version: ${{ matrix.python-version }}
|
|
32
|
+
cache: pip
|
|
33
|
+
|
|
34
|
+
# PyTorch is ~1GB and nothing under test needs it: the parsing, chunking
|
|
35
|
+
# and tagging logic is pure Python, and the modules that do use models
|
|
36
|
+
# import them lazily. Installing numpy alone keeps CI to a few seconds
|
|
37
|
+
# while still checking that every module imports.
|
|
38
|
+
- name: Install
|
|
39
|
+
run: |
|
|
40
|
+
python -m pip install --upgrade pip
|
|
41
|
+
python -m pip install "numpy>=2,<3" "ruff==0.15.4"
|
|
42
|
+
python -m pip install -e . --no-deps
|
|
43
|
+
|
|
44
|
+
- name: Lint
|
|
45
|
+
run: ruff check .
|
|
46
|
+
|
|
47
|
+
- name: Format
|
|
48
|
+
run: ruff format --check .
|
|
49
|
+
|
|
50
|
+
- name: Unit tests
|
|
51
|
+
run: python -m unittest discover -s tests -v
|
|
52
|
+
|
|
53
|
+
- name: Every module imports
|
|
54
|
+
run: |
|
|
55
|
+
python -c "
|
|
56
|
+
import msgsearch, msgsearch.cli, msgsearch.config, msgsearch.chunk
|
|
57
|
+
import msgsearch.contacts
|
|
58
|
+
import msgsearch.embedder, msgsearch.explore, msgsearch.extract
|
|
59
|
+
import msgsearch.doctor, msgsearch.index, msgsearch.search, msgsearch.tagging
|
|
60
|
+
print('msgsearch', msgsearch.__version__)
|
|
61
|
+
"
|
|
62
|
+
|
|
63
|
+
- name: CLI responds
|
|
64
|
+
run: |
|
|
65
|
+
msgsearch --version
|
|
66
|
+
msgsearch --help
|
|
67
|
+
msgsearch search --help
|
|
68
|
+
msgsearch doctor --help
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Publishing is driven by tags, so the released artefact always corresponds to a
|
|
4
|
+
# specific commit that CI has already tested.
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
tags: ["v*"]
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build:
|
|
12
|
+
name: Build distributions
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.12"
|
|
20
|
+
|
|
21
|
+
- name: Build sdist and wheel
|
|
22
|
+
run: |
|
|
23
|
+
python -m pip install --upgrade pip build
|
|
24
|
+
python -m build
|
|
25
|
+
|
|
26
|
+
# A tag that disagrees with pyproject.toml would publish a version nobody
|
|
27
|
+
# asked for, and PyPI versions cannot be reused once taken.
|
|
28
|
+
- name: Tag must match the package version
|
|
29
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
30
|
+
run: |
|
|
31
|
+
tag="${GITHUB_REF_NAME#v}"
|
|
32
|
+
version=$(python -c "import tomllib;print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
|
|
33
|
+
echo "tag=$tag pyproject=$version"
|
|
34
|
+
test "$tag" = "$version"
|
|
35
|
+
|
|
36
|
+
- uses: actions/upload-artifact@v4
|
|
37
|
+
with:
|
|
38
|
+
name: distributions
|
|
39
|
+
path: dist/
|
|
40
|
+
|
|
41
|
+
publish:
|
|
42
|
+
name: Publish to PyPI
|
|
43
|
+
needs: build
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
46
|
+
|
|
47
|
+
# Trusted Publishing: PyPI verifies this workflow's OIDC identity, so there
|
|
48
|
+
# is no API token to store, leak or rotate. Configure the publisher once at
|
|
49
|
+
# https://pypi.org/manage/project/msgsearch/settings/publishing/
|
|
50
|
+
environment:
|
|
51
|
+
name: pypi
|
|
52
|
+
url: https://pypi.org/p/msgsearch
|
|
53
|
+
permissions:
|
|
54
|
+
id-token: write
|
|
55
|
+
|
|
56
|
+
steps:
|
|
57
|
+
- uses: actions/download-artifact@v4
|
|
58
|
+
with:
|
|
59
|
+
name: distributions
|
|
60
|
+
path: dist/
|
|
61
|
+
|
|
62
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Message data and anything derived from it must never be committed. The index is
|
|
2
|
+
# a plaintext, searchable copy of every private thing anyone has ever texted you.
|
|
3
|
+
*.db
|
|
4
|
+
*.db-shm
|
|
5
|
+
*.db-wal
|
|
6
|
+
*.sqlite
|
|
7
|
+
*.sqlite3
|
|
8
|
+
*.npy
|
|
9
|
+
*.npz
|
|
10
|
+
index/
|
|
11
|
+
msgsearch_index/
|
|
12
|
+
|
|
13
|
+
# Local configuration holds phone numbers and email addresses.
|
|
14
|
+
config_local.py
|
|
15
|
+
|
|
16
|
+
# Build artefacts
|
|
17
|
+
dist/
|
|
18
|
+
build/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
|
|
21
|
+
# Environments and build noise
|
|
22
|
+
.venv/
|
|
23
|
+
venv/
|
|
24
|
+
__pycache__/
|
|
25
|
+
*.py[cod]
|
|
26
|
+
.pytest_cache/
|
|
27
|
+
.DS_Store
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# query_messages
|
|
2
|
+
|
|
3
|
+
Semantic search over ~260k personal iMessages (2018-08 → 2026-09), by meaning
|
|
4
|
+
rather than keyword.
|
|
5
|
+
|
|
6
|
+
Source of truth for all coding agents; `CLAUDE.md` symlinks here.
|
|
7
|
+
|
|
8
|
+
## Rules
|
|
9
|
+
|
|
10
|
+
Kept deliberately short. A rule earns a line here only if breaking it fails
|
|
11
|
+
*silently* or *irreversibly* — anything that fails loudly teaches itself, and
|
|
12
|
+
anything already enforced in code or `settings.json` is not repeated here.
|
|
13
|
+
|
|
14
|
+
- **Never** touch `~/Library/Messages`. Work against `~/msgsearch/chat.db`,
|
|
15
|
+
opened `file:...?mode=ro`. (Irreversible.)
|
|
16
|
+
- Message content is private: none sent over the network, none in commits, logs or
|
|
17
|
+
eval fixtures, and no bodies printed by diagnostic or eval scripts. `search.py`
|
|
18
|
+
is the single exemption — printing results to the terminal is its entire
|
|
19
|
+
function. (Irreversible.)
|
|
20
|
+
- `message.date` is Apple-epoch and **mixes scales** — pre-10.13 rows are
|
|
21
|
+
seconds, later ones nanoseconds. Use `extract.apple_timestamp()`; `explore.py`
|
|
22
|
+
carries a standalone copy so it can run before anything else exists. (Silent:
|
|
23
|
+
the wrong scale yields plausible, wrong dates.)
|
|
24
|
+
- ~86% of rows have `text IS NULL` with content in the `attributedBody`
|
|
25
|
+
typedstream blob. Go through `attributed_body.message_text(text, blob)`.
|
|
26
|
+
(Silent: reading `message.text` looks fine and loses most of the corpus.)
|
|
27
|
+
|
|
28
|
+
## Commands
|
|
29
|
+
|
|
30
|
+
Use `./.venv/bin/python` — it is the arm64 interpreter. The system `python3` is an
|
|
31
|
+
Intel build and cannot install torch.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
./.venv/bin/msgsearch doctor # setup preflight
|
|
35
|
+
./.venv/bin/msgsearch contacts --unresolved # unnamed handles by volume
|
|
36
|
+
./.venv/bin/msgsearch explore # structural recon
|
|
37
|
+
./.venv/bin/msgsearch index # rebuild ~/msgsearch/index/
|
|
38
|
+
./.venv/bin/msgsearch search "atria login" # query it
|
|
39
|
+
|
|
40
|
+
./.venv/bin/ruff check . && ./.venv/bin/ruff format --check .
|
|
41
|
+
./.venv/bin/python -m unittest discover -s tests
|
|
42
|
+
|
|
43
|
+
./.venv/bin/python eval/label.py "a query" # add a labeled gold query
|
|
44
|
+
./.venv/bin/python eval/bench.py -r eval.retriever --against baseline
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Code lives in `src/msgsearch/`; install it with `pip install -e ".[dev]"` once.
|
|
48
|
+
|
|
49
|
+
Retriever variants for `-r`, to score one stage at a time:
|
|
50
|
+
`eval.retriever` (full) · `eval.retriever_bm25` · `eval.retriever_dense` ·
|
|
51
|
+
`eval.retriever_norerank`
|
|
52
|
+
|
|
53
|
+
## The verification loop
|
|
54
|
+
|
|
55
|
+
A retrieval change is justified by `eval/bench.py` or it is a guess. Report the
|
|
56
|
+
before/after table; don't claim an improvement without it.
|
|
57
|
+
|
|
58
|
+
Architecture: `ARCHITECTURE.md`.
|
|
59
|
+
|
|
60
|
+
## Learned corrections
|
|
61
|
+
|
|
62
|
+
Append here **only** when an agent actually gets something wrong in practice.
|
|
63
|
+
Nothing speculative. If a rule can instead be enforced in code, a hook, or
|
|
64
|
+
`settings.json`, do that and leave this file alone.
|
|
65
|
+
|
|
66
|
+
_(empty — nothing has gone wrong yet)_
|