# M3.7.8 — Symptom projection: make an answer findable from an error | Field | Value | |---|---| | Phase | M3.7 — Tool context | | Size | M — 1–3 days | | Status | ⬜ Not started | | Flags | — | | Spec | inlined below | | Blocks | M3.7.6 | | Depends | M3.7.7, M2.4, M1.5 | ## Goal Give every memory a second vector describing the failures it would explain, so a stack trace can find an answer written in prose. ## Facts (inlined — no spec read needed) The asymmetry this exists to fix: ``` L1 memory (how it is written): "Requests over 10KB failed because Kong buffered the whole body before proxying; resolved with proxy-body-size: 0 on the ingress." Query (how it arrives): "413 Request Entity Too Large" + a curl trace ``` Same incident. Embedded with the same model and compared by cosine, they are mediocre neighbours — one is an explanation, the other is a symptom. This is the main reason retrieval that looks correct in a unit test disappoints in use. **Fix at write time, not read time.** When the gated loop accepts a memory, generate a short *symptom projection* — the errors, messages and observable behaviour this memory would explain — and embed that as a second vector: ``` symptom projection for the memory above: "413 Request Entity Too Large; large POST bodies rejected at the ingress; uploads over 10KB fail while small ones succeed; nginx/Kong body buffer limit" ``` The alternative, HyDE, generates a hypothetical answer per *query* and puts an LLM call on every lookup. Writes are rare here — the gate keeps acceptance under 30% by design — and lookups should be fast, so paying once at write is the right side of that trade. **It is a projection, so it obeys the projection rules.** Regenerated by `mem rebuild --from-log`, never authoritative, and byte-identical on replay — which means the generation call must be deterministic: temperature 0, pinned prompt, and the model id recorded in the log record so a model change is visible as a rebuild difference rather than silent drift. **Only L1 and L2 get one.** L0 is raw evidence already phrased as symptoms; R is documentation and its headings already read like problems. Generating projections for those doubles the index for no gain. **Empty is allowed.** Not every memory explains a failure — an architectural decision has no symptoms. The controller returns nothing, no vector is written, and the memory remains findable by its text vector alone. A model that invents symptoms for a memory that has none pollutes the symptom index with plausible nonsense, which is worse than a smaller index. **Signature linking.** Where the L0 evidence behind an accepted memory contains a parseable failure (M3.7.7), write a `failure_signature` row pointing at the L1. That is what turns the next occurrence into an exact-match tier-1 hit instead of a vector search. ## Steps 1. Prompt template `prompts/symptom.tmpl` — memory text in, symptom lines out, explicit "return nothing if this describes no failure". 2. Hook into the gated loop after a memory is accepted; L1 and L2 only. 3. Temperature 0; record model id and prompt sha on the log record. 4. Embed and write `memory_vector(kind='symptom')`. 5. Extract signatures from the memory's L0 parents; write `failure_signature` rows keyed to the memory. 6. `mem rebuild --from-log` regenerates projections; assert stability. 7. `mem symptoms show ` prints the projection for inspection. ## Acceptance - An accepted L1 that explains a failure gains a symptom vector. - A memory describing a decision gains none. - Symptom text retrieves its memory from a raw error string that shares no vocabulary with the memory text. - Rebuild regenerates projections byte-identically. - Signature rows link to the right memory. ## Verify **Harness:** fixture memories — five that explain failures, three that do not — plus raw error strings for the five, deliberately worded with no vocabulary overlap with the memory text. **Integration test** — `tests/it_symptom_projection.rs`: 1. `a1_projection_generated` — the five failure memories each gain a `kind='symptom'` vector. 2. `a2_no_projection_for_non_failure` — the three others gain none. 3. `a3_retrieval_by_symptom` — searching `kind='symptom'` with each raw error string returns its memory first. This is the assertion the task exists for. 4. `a4_beats_text_vector` — the same query against `kind='text'` ranks the correct memory *lower*; assert the symptom search strictly improves rank. Without this the projection could be doing nothing. 5. `a5_deterministic` — generate twice; assert byte-identical projections. 6. `a6_rebuild_stable` — drop and rebuild; assert projections and their embeddings match the originals. 7. `a7_signature_linked` — a memory whose evidence contains a parseable error has a `failure_signature` row pointing at it, with the tool set. 8. `a8_l0_and_r_skipped` — assert no L0 or R node has a symptom vector. 9. `a9_model_id_recorded` — the log record names the model and prompt sha. **Command:** `cargo test -p mem-core symptom && cargo test -p mem-cli rebuild` **False pass:** - Test queries that reuse the memory's own wording. The text vector already finds those, assertion 3 passes, and the projection is never exercised. The error strings must share no meaningful vocabulary — that constraint is the test. - Asserting 3 without 4. If the text vector already ranked it first, assertion 3 is satisfied by a projection that is empty or useless. - Skipping determinism because output "looks stable". A default temperature makes it stable for ten runs and different on the eleventh, and the symptom is a rebuild diff nobody can explain. ## Traps - Generating projections for rejected chunks. The gate rejected them; embedding their symptoms puts evidence-free content in the index through a side door. - Letting the projection restate the memory. If the model paraphrases the answer instead of naming the symptoms, the second vector duplicates the first and assertion 4 fails — which is the correct outcome, but the cause is the prompt, not the plumbing. - Treating an empty projection as an error and retrying. It is the right answer for most non-incident memories, and a retry loop turns it into invented symptoms. --- Background: [DESIGN.md](../DESIGN.md) — tool context, write path