feat: shared matcher (phase 2A) — embeddings, /match, attribution #1
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/shared-matcher"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Phase 2A of the shared-knowledge design — the hub-side per-prompt lesson matcher. This contains no TypeScript and no in-session injection; the OpenCode plugin is phase 2B.
lesson_vec+match_stattables — additive (CREATE TABLE IF NOT EXISTS); existing rows are untouched.reflect/embed.py— a pluggable embedder: local ONNX MiniLM by default (optional[embed]extra:onnxruntime+tokenizers, imported lazily), an HTTP/v1/embeddingsoverride when configured, a deterministicFakeEmbedderfor tests, andfetch_bundlefor one-time model provisioning.reflect/match.py— cosine ranking over normalised vectors with a similarity floor, per-session dedupe, a shared-block ranking lift, and a read-only FTS5 keyword fallback. Matching never writes to the store.POST /match(read token) andPOST /attribution(worker token) on the existing MCP Starlette apps, token-guarded by the existing bearer wrapper./matchis fail-open: any internal error is a benign empty result, never a 5xx.reflect similarandreflect embed fetchCLI commands.Design
docs/superpowers/specs/2026-10-14-shared-memory-design.md(§6.1, §6.2, §7, §10, §14, §15, §16)docs/superpowers/plans/2026-10-15-shared-matcher.mdTests
489 passed, 1 skipped (
uv run pytest). The skip is the env-gated real-ONNX integration test, which passes when a model bundle is provisioned. The ONNX inference contract (padding, input/output names, normalisation) is additionally pinned withoutonnxruntimevia an injected fake session.Existing strength arithmetic and every pre-existing
reflect statsfigure are unchanged — matching and attribution are evidence only.Out of scope
The OpenCode plugin, pointer injection, in-session dedupe/cache,
/attributionreporting from a session, and the host-side e2e (all phase 2B, which waits on a runtime probe of the plugin injection point).Deferred
lessons.db(the plan's Task 8 Step 5) — not runnable off the hub.match_stat.- Task 2 breadth counts action IN ('useful','judge_useful'), not judge_useful alone - Task 5 action set is judge_useful / judge_not_useful / retrieve_observed / inject, with retrieve_observed deliberately not spelled retrieve (stats counts every retrieve row) - Task 7 emits retrieve_observed for a get_lesson part and nothing for search_lessons - Task 7 no longer touches reflect/opencode_db.pyImportant 3 and Important 4 (plus the /match half of Important 1) from the final whole-branch review of the shared matcher. Lazy embedder: create_server built the embedder when the server was constructed, so every per-session stdio `reflect mcp` server and the worker app paid the local ONNX bundle's ~420 ms / ~200 MB even though neither calls /match in this phase. It is now built on the first /match request and cached, at most once per process; a sentinel separates "not yet built" from a cached None ("no backend"), so a failed build is not retried per request. An injected `embedder=` is still used as-is and never rebuilt. The route tests therefore no longer depend on the host: the tests that build a client without an injected embedder (auth, missing query, malformed body) pass a stub, so a hub with the bundle provisioned at the default embed_model_dir can no longer turn them into real ONNX runs. Attribution kind alias: spec §8 has the plugin report {session_id, project, lesson_id, kind} with kind in {inject, retrieve, judge_useful, judge_not_useful}, but Store.record_usage reads `action` and knows `retrieve_observed`, not `retrieve`. A plugin written from §8 got 200 {recorded: 0, skipped: 1} — a silent no-op, and the injection->pull figure read zero. The handler now maps `kind` onto `action` before delegating (`retrieve` -> `retrieve_observed`, the other three names unchanged). An explicit `action` still wins, and non-dict records pass through so record_usage keeps owning validation and the skip counts. Tests: construction loads nothing, two /match calls build exactly once, a build that answers None is cached rather than repeated, and the kind wire shape records the alias plus the pass-through names.