0b893295e1
Close the two locked post-deploy enhancements for the Matrix intake bot.
Fuzzy matching (server-side, ships in the s9pk): new find_intake_candidates in
server.py returns ranked deterministic near-matches (difflib name similarity +
token-set Jaccard, legal-suffix-aware, + email Levenshtein <= 2); GET
/api/intake/match now returns {match, candidates}. The bot surfaces a numbered
shortlist so a near-duplicate (Charlie/Charles, Acme Capital vs Acme Capital LLC,
a one-char email typo) is confirmed by a human instead of silently creating a
second investor. Exact match still auto-attaches; fuzzy candidates are never
auto-attached. The optional LLM-judge re-rank is deferred.
Conversational edits (bot-side, ships on the Spark): any in-thread reply that
isn't yes/no/edit field=value is treated as a natural-language revision and
re-run through local Qwen (parse.revise). Email integrity is preserved -- a
changed address must literally appear in the instruction; the model's email
field is structurally unreachable. No-op revisions re-prompt.
Docs/current-state brought current; 27/27 backend tests green.
Matrix intake bot
Turns a typed message in a dedicated Matrix room into a proposed fundraising-grid add/edit,
gated on in-thread human approval before any write. Runs as its own process (on the Spark),
separate from the CRM. Full design + rules: docs/guides/matrix-intake.md.
Run
# 1. Install the one third-party dep (isolated to this component — NOT the CRM runtime)
python3 -m pip install -r requirements.txt # matrix-nio
# 2. Fill the MATRIX_* and CRM_BOT_* vars in the repo .env (see ../../.env.example),
# and create a dedicated CRM user for CRM_BOT_USERNAME/PASSWORD (admin → invite user).
# 3. Start the listener
python3 bot.py
It primes the Matrix sync past history (no backlog replay), then listens. Post a message in the intake room; it replies in a thread with the parsed proposal. Reply yes to commit, edit field=value to change a field, or no to discard.
Layout
bot.py— entrypoint: connect, prime-then-listen, dispatch (lifts matrix-bridge's plumbing).parse.py— message → structured proposal via local Qwen (spark.py→backend/ingest/llm.py).proposals.py— in-memory pending-proposal store + the yes/edit/no state machine.crm_client.py— login +GET /api/intake/match+ write viaPOST /api/fundraising/log-communication.matrix_io.py— message splitting, thread-root detection, threaded-reply sender.settings.py— Matrix + CRM-API config (namedsettings, notconfig, to avoid shadowingingest/config).
Test (offline)
python3 test_parse.py && python3 test_proposals.py && python3 test_crm_client.py
# endpoint + create→match contract (boots the real server against a temp DB):
cd ../ && python3 test_intake_endpoints.py
Live Matrix behavior needs creds + matrix-nio and can only be smoke-tested on the Spark.