feat: implement M3.7.8 symptom projection (250 LOC) + 22 tests (10 unit + 12 integration)

IMPLEMENTATION:
- crates/mem-core/src/symptom_projection.rs (250 LOC)
  - project_symptom(tool, query) → SymptomVector
  - Three-stage normalization:
    - Stage 1: Extract keywords
    - Stage 2: Normalize (stop words, abbreviations)
    - Stage 3: Generate deterministic SHA256 hash
  - Tool-specific abbreviation mappings (npm, cargo, kubectl, docker, go)
  - Stop words list (30+ common words)
  - Confidence scoring based on keyword specificity

TEST COVERAGE: 22 tests passing
  - 10 unit tests in lib (determinism, abbreviations, stop words, tools, case, order)
  - 12 integration tests (a1-a6 assertions from design doc)
  - Real-world scenario tests (npm, cargo, kubectl)
  - 100% deterministic hashing verified

INTEGRATION:
- Module exported in crates/mem-core/src/lib.rs
- All 43 existing mem-core tests still passing
- Ready for M3.7.4 context endpoint integration

DESIGN ASSERTIONS (all passing):
 a1: Same symptom = same hash (deterministic)
 a2: Abbreviation expansion (ERESOLVE → error resolve)
 a3: Stop word removal (is, unable, to, the)
 a4: Tool consistency (npm ≠ cargo for same error)
 a5: Case insensitive (NPM = npm)
 a6: Keyword order irrelevant (sorted before hash)
This commit is contained in:
Story Crater Bot
2026-08-28 08:08:55 -07:00
parent ad9cbe1fdc
commit 19967d1699
4 changed files with 957 additions and 0 deletions
@@ -0,0 +1,178 @@
//! Integration tests for M3.7.8 symptom projection
//! Tests the six main assertions from the design doc
use mem_core::project_symptom;
#[test]
fn a1_same_symptom_same_hash() {
// ASSERTION 1: Same query always produces identical hash (deterministic)
let query = "npm ERR! ERESOLVE unable to resolve dependency tree";
let sym1 = project_symptom("npm", query);
let sym2 = project_symptom("npm", query);
let sym3 = project_symptom("npm", query);
assert_eq!(sym1.sym_sha, sym2.sym_sha);
assert_eq!(sym2.sym_sha, sym3.sym_sha);
assert_eq!(sym1.sym_sha.len(), 64); // SHA256 = 64 hex chars
}
#[test]
fn a2_abbrev_expansion() {
// ASSERTION 2: Tool-specific abbreviations expand correctly
// npm: ERESOLVE → error resolve
let npm_query = "npm ERESOLVE error";
let npm_sym = project_symptom("npm", npm_query);
assert!(npm_sym.normalised.contains("resolve"), "ERESOLVE should expand to 'resolve'");
// cargo: E0599 → error 0599
let cargo_query = "cargo E0599 method";
let cargo_sym = project_symptom("cargo", cargo_query);
assert!(cargo_sym.normalised.contains("0599"), "E0599 should expand");
// kubectl: CRD → custom resource definition
let kubectl_query = "error CRD";
let kubectl_sym = project_symptom("kubectl", kubectl_query);
assert!(kubectl_sym.normalised.contains("custom"), "CRD should expand to custom");
}
#[test]
fn a3_stop_word_removal() {
// ASSERTION 3: Stop words removed (is, unable, to, the, of, etc.)
let query = "npm is unable to resolve the dependency";
let symptom = project_symptom("npm", query);
// Stop words should not appear
assert!(!symptom.normalised.split_whitespace().any(|w| w == "is"));
assert!(!symptom.normalised.split_whitespace().any(|w| w == "unable"));
assert!(!symptom.normalised.split_whitespace().any(|w| w == "to"));
assert!(!symptom.normalised.split_whitespace().any(|w| w == "the"));
// Content should remain
assert!(symptom.normalised.contains("npm"));
assert!(symptom.normalised.contains("resolve"));
assert!(symptom.normalised.contains("dependency"));
}
#[test]
fn a4_tool_consistency() {
// ASSERTION 4: Tool name is part of identity
// Same query, different tools = different hashes
let query = "error 1234 module";
let npm_sym = project_symptom("npm", query);
let cargo_sym = project_symptom("cargo", query);
let kubectl_sym = project_symptom("kubectl", query);
assert_ne!(npm_sym.sym_sha, cargo_sym.sym_sha);
assert_ne!(cargo_sym.sym_sha, kubectl_sym.sym_sha);
assert_ne!(npm_sym.sym_sha, kubectl_sym.sym_sha);
}
#[test]
fn a5_case_insensitive() {
// ASSERTION 5: Case doesn't affect hash
// (all normalized to lowercase)
let q1 = project_symptom("npm", "NPM ERROR ERESOLVE");
let q2 = project_symptom("npm", "npm error eresolve");
let q3 = project_symptom("npm", "NpM eRrOr ErEsOlVe");
assert_eq!(q1.sym_sha, q2.sym_sha);
assert_eq!(q2.sym_sha, q3.sym_sha);
}
#[test]
fn a6_keyword_order_irrelevant() {
// ASSERTION 6: Keyword order irrelevant
// (keywords sorted alphabetically before hashing)
let q1 = project_symptom("npm", "error npm resolve typescript");
let q2 = project_symptom("npm", "npm typescript resolve error");
let q3 = project_symptom("npm", "typescript error npm resolve");
assert_eq!(q1.sym_sha, q2.sym_sha, "Different order should hash the same");
assert_eq!(q2.sym_sha, q3.sym_sha, "Different order should hash the same");
// Verify keywords are sorted
let mut words: Vec<_> = q1.normalised.split_whitespace().collect();
words.sort();
let normalised_words: Vec<_> = q1.normalised.split_whitespace().collect();
assert_eq!(words, normalised_words, "Keywords should be sorted");
}
#[test]
fn test_deterministic_across_calls() {
// Multiple calls with same query = identical results
let query = "npm unable to resolve typescript dependency";
let mut hashes = Vec::new();
for _ in 0..10 {
let sym = project_symptom("npm", query);
hashes.push(sym.sym_sha);
}
for hash in &hashes[1..] {
assert_eq!(hash, &hashes[0]);
}
}
#[test]
fn test_real_world_npm() {
// Real npm error scenarios
let queries = vec![
"npm ERR! code ERESOLVE",
"npm ERR! ERESOLVE unable to resolve dependency tree",
"npm error cannot find module typescript",
"npm E404 not found",
];
for query in queries {
let sym = project_symptom("npm", query);
assert!(!sym.sym_sha.is_empty());
assert_eq!(sym.tool, "npm");
assert!(sym.confidence > 0.0);
}
}
#[test]
fn test_real_world_cargo() {
// Real cargo error scenarios
let queries = vec![
"error E0599 no method found",
"error E0308 mismatched types",
"cargo error failed to compile",
];
for query in queries {
let sym = project_symptom("cargo", query);
assert!(!sym.sym_sha.is_empty());
assert_eq!(sym.tool, "cargo");
}
}
#[test]
fn test_matches_signature() {
// SymptomVector::matches_signature works correctly
let sym = project_symptom("npm", "npm error resolve dependency");
assert!(sym.matches_signature(&sym.sym_sha));
assert!(!sym.matches_signature("different_hash"));
assert!(!sym.matches_signature(&sym.sym_sha[0..32])); // partial hash
}
#[test]
fn test_raw_query_preserved() {
// Original query should be preserved for logging
let original = "npm ERR! ERESOLVE unable to resolve typescript";
let sym = project_symptom("npm", original);
assert_eq!(sym.raw_query, original);
}
#[test]
fn test_confidence_scoring() {
// More specific queries = higher confidence
let low = project_symptom("npm", "error");
let high = project_symptom("npm", "npm error ERESOLVE cannot resolve typescript module");
assert!(high.confidence >= low.confidence);
}