Matrix intake: fuzzy investor matching + conversational in-thread edits (v0.1.0:86)

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.
This commit is contained in:
Keysat
2026-06-17 18:50:58 -05:00
parent fa6c9da0e6
commit 0b893295e1
15 changed files with 734 additions and 41 deletions
+20
View File
@@ -0,0 +1,20 @@
import { VersionInfo } from '@start9labs/start-sdk'
// Matrix intake — fuzzy investor matching. GET /api/intake/match now returns, alongside the
// exact `match`, a ranked list of `candidates`: fuzzy near-matches (deterministic difflib name
// similarity + token overlap + email edit-distance ≤ 2, legal-suffix-aware) the intake bot can
// surface in-thread for the human to pick from — so a near-duplicate name ("Charlie"/"Charles",
// "Acme Capital"/"Acme Capital LLC", a one-char email typo) no longer silently creates a second
// investor. Server-side only (the bot's disambiguation + conversational-edit UX ships on the
// Spark, not in the s9pk). Code-only, no schema change.
export const v_0_1_0_86 = VersionInfo.of({
version: '0.1.0:86',
releaseNotes: {
en_US: [
'Matrix intake: the new-vs-existing lookup now also returns ranked fuzzy near-matches,',
'so a typod or near-duplicate investor name is surfaced for confirmation instead of',
'silently creating a duplicate. No data changes.',
].join(' '),
},
migrations: { up: async () => {}, down: async () => {} },
})