From 86122516f778fdea6ae224b00fb05bd9f332b8f9 Mon Sep 17 00:00:00 2001 From: Story Crater Bot <19826264+Riotpiaole@users.noreply.github.com> Date: Fri, 28 Aug 2026 07:49:34 -0700 Subject: [PATCH] =?UTF-8?q?docs:=20add=20M3.7.8=20symptom=20projection=20d?= =?UTF-8?q?esign=20=E2=80=94=203-stage=20normalization,=206=20test=20asser?= =?UTF-8?q?tions,=20250=20LOC=20implementation=20plan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/M3.7.8-SYMPTOM_PROJECTION.md | 554 ++++++++++++++++++++++++++++++ 1 file changed, 554 insertions(+) create mode 100644 docs/M3.7.8-SYMPTOM_PROJECTION.md diff --git a/docs/M3.7.8-SYMPTOM_PROJECTION.md b/docs/M3.7.8-SYMPTOM_PROJECTION.md new file mode 100644 index 0000000..feed843 --- /dev/null +++ b/docs/M3.7.8-SYMPTOM_PROJECTION.md @@ -0,0 +1,554 @@ +# M3.7.8 — Symptom Projection at Ingest + +**Status**: Design · Ready to implement +**Size**: M (1–2 days) +**Depends**: M3.7.7 (signature extraction) +**Blocks**: M3.7.4 (context endpoint), M3.7.6 (gate) + +--- + +## Goal + +Transform incoming user queries and agent failure reports into normalized symptom vectors that can be matched against extracted failure signatures from M3.7.7, enabling tier 1 (exact match) lookups in the three-tier context endpoint (M3.7.4). + +--- + +## The Problem + +**M3.7.7 extracts** failure signatures from logs and produces deterministic hashes: +``` +Log: npm ERR! code ERESOLVE unable to resolve dependency tree +Signature: npm_ERR_ERESOLVE_dependency_tree (normalized) +sig_sha: abc123def... (deterministic) +``` + +**M3.7.8 must handle** user queries that don't match the log format: +``` +User query: "npm can't find module tslib" +Expected: "npm error module not found" +Problem: These don't normalize to the same string +``` + +**Solution:** Symptom projection normalizes BOTH: +- Extracted signature ← M3.7.7 (log normalization) +- User query ← M3.7.8 (query normalization) +- If both normalize to the same sym_sha → tier 1 hit ✅ + +--- + +## Design + +### Three-Stage Normalization Pipeline + +#### Stage 1: Extract Symptom Keywords + +**Goal**: Pull actionable error signals from free text. + +```rust +Input query: "npm error: unable to resolve typescript dependency tree" + +Step 1a: Detect tool + → "npm" (from query context or user param) + +Step 1b: Identify error patterns + • Keywords: ["unable", "resolve", "typescript", "dependency", "tree"] + • Error phrases: ["unable to resolve", "dependency tree"] + +Step 1c: Expand abbreviations + • "ERESOLVE" → ["error", "resolve"] + • "ERR" → ["error"] + • "cannot" → "can not" (splits) + • "can't" → "cannot" (normalizes) + +Step 1d: Extract entity types + • Module/package names (typescript) + • Error codes (E400, EACCES) + • Version specifiers (^1.0, 2.1.3) + +Output: SymptomTokens { + tool: "npm", + keywords: ["unable", "resolve", "typescript", "dependency", "tree"], + error_codes: [], + modules: ["typescript"] +} +``` + +#### Stage 2: Normalize to Canonical Form + +**Goal**: Create identical strings from variant phrasings. + +```rust +Input: SymptomTokens { + tool: "npm", + keywords: ["unable", "resolve", "typescript", "dependency", "tree"], + error_codes: [], + modules: ["typescript"] +} + +Step 2a: Remove stop words + Remove: [a, an, the, is, are, be, can, may, to, in, of, ...] + Remaining: ["unable", "resolve", "typescript", "dependency", "tree"] + +Step 2b: Normalize case + → ["unable", "resolve", "typescript", "dependency", "tree"] + +Step 2c: Apply stemming (if needed) + resolve ← resolved, resolving, resolution + depend ← dependency, dependent + +Step 2d: Expand tool-specific shorthand + For npm: + • ERESOLVE → "error resolve" + • ENOENT → "not found" + • EACCES → "permission denied" + For cargo: + • "error[E0599]" → "error 0599" + For kubectl: + • "connection refused" → "connection refused" + +Step 2e: Dedup and sort (for idempotence) + Before: ["dependency", "error", "npm", "resolve", "tree"] + After: ["dependency", "error", "npm", "resolve", "tree"] + +Output: "dependency error npm resolve tree" +``` + +#### Stage 3: Generate Deterministic Hash + +**Goal**: Create sym_sha that matches M3.7.7 signatures. + +```rust +Input: "dependency error npm resolve tree" (sorted, deduplicated) + +Step 3a: Construct hashable string + → "npm\ndependency error npm resolve tree" + ↑ + tool is part of identity + (npm ERR vs cargo error are different) + +Step 3b: Hash with SHA256 + sym_sha = SHA256("npm\ndependency error npm resolve tree") + = "xyz789abc123..." + +Output: SymptomVector { + tool: "npm", + raw_query: "npm error: unable to resolve typescript dependency tree", + normalised: "dependency error npm resolve tree", + sym_sha: "xyz789abc123...", + keywords: ["dependency", "error", "npm", "resolve", "tree"], + confidence: 0.85 // based on keyword match strength +} +``` + +--- + +## Implementation + +### Core Functions + +#### `project_symptom(tool: &str, query: &str) -> SymptomVector` + +**Purpose**: Transform user query into normalized symptom vector. + +```rust +pub fn project_symptom(tool: &str, query: &str) -> SymptomVector { + // Stage 1: Extract keywords + let tokens = extract_keywords(tool, query); + + // Stage 2: Normalize + let normalised = normalize_tokens(&tokens); + + // Stage 3: Hash + let sym_sha = sha256(&format!("{}\n{}", tool, normalised)); + + SymptomVector { + tool: tool.to_string(), + raw_query: query.to_string(), + normalised, + sym_sha, + keywords: tokens.keywords, + confidence: tokens.confidence, + } +} +``` + +#### `extract_keywords(tool: &str, query: &str) -> SymptomTokens` + +**Goal**: Pull error signals from query text. + +```rust +fn extract_keywords(tool: &str, query: &str) -> SymptomTokens { + let mut keywords = Vec::new(); + let mut error_codes = Vec::new(); + let mut modules = Vec::new(); + + // Tokenize by whitespace + punctuation + let tokens = tokenize(query); + + for token in tokens { + // Skip stop words + if STOP_WORDS.contains(&token.to_lowercase()) { + continue; + } + + // Expand abbreviations + let expanded = expand_abbrev(tool, &token); + for word in expanded.split_whitespace() { + if !word.is_empty() { + keywords.push(word.to_lowercase()); + } + } + + // Extract structured signals + if let Some(code) = extract_error_code(&token) { + error_codes.push(code); + } + if let Some(module) = extract_module_name(tool, &token) { + modules.push(module); + } + } + + // Dedup + sort for idempotence + keywords.sort(); + keywords.dedup(); + + SymptomTokens { + keywords, + error_codes, + modules, + confidence: keyword_strength(&keywords), + } +} +``` + +#### `normalize_tokens(tokens: &SymptomTokens) -> String` + +```rust +fn normalize_tokens(tokens: &SymptomTokens) -> String { + let mut normalized = tokens.keywords.clone(); + + // Remove duplicates (again, for safety) + normalized.sort(); + normalized.dedup(); + + // Join with spaces + normalized.join(" ") +} +``` + +#### `expand_abbrev(tool: &str, word: &str) -> String` + +**Per-tool abbreviation mappings:** + +```rust +fn expand_abbrev(tool: &str, word: &str) -> String { + let lower = word.to_lowercase(); + + // Universal abbreviations + match lower.as_str() { + "err" | "error" => return "error".to_string(), + "rc" | "return" => return "return code".to_string(), + "oom" => return "out of memory".to_string(), + "enoent" => return "not found".to_string(), + "eacces" => return "permission denied".to_string(), + "eperm" => return "operation not permitted".to_string(), + "econnrefused" => return "connection refused".to_string(), + "econnreset" => return "connection reset".to_string(), + _ => {} + } + + // Tool-specific abbreviations + match tool { + "npm" => match lower.as_str() { + "eresolve" => "error resolve", + "eexist" => "already exists", + _ => word, + }, + "cargo" => match lower.as_str() { + "e0599" => "error 0599 no method", + "e0308" => "error 0308 type mismatch", + _ => word, + }, + "kubectl" => match lower.as_str() { + "crd" => "custom resource definition", + "etcd" => "etcd", + _ => word, + }, + _ => word, + } + .to_string() +} +``` + +#### `sha256(s: &str) -> String` + +```rust +use sha2::{Digest, Sha256}; + +fn sha256(s: &str) -> String { + let mut hasher = Sha256::new(); + hasher.update(s.as_bytes()); + hex::encode(hasher.finalize()) +} +``` + +--- + +## Data Structures + +### SymptomVector + +```rust +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SymptomVector { + /// Tool name (npm, cargo, kubectl, etc.) + pub tool: String, + + /// Original user query (for display) + pub raw_query: String, + + /// Normalized, sorted keywords + pub normalised: String, + + /// Deterministic hash (for lookup) + pub sym_sha: String, + + /// Extracted keywords + pub keywords: Vec, + + /// Confidence score (0.0-1.0) based on keyword coverage + pub confidence: f32, +} + +impl SymptomVector { + /// Check if this symptom matches a signature + pub fn matches_signature(&self, sig: &Signature) -> bool { + self.sym_sha == sig.sig_sha && self.tool == sig.tool + } +} +``` + +### SymptomTokens (internal) + +```rust +struct SymptomTokens { + keywords: Vec, + error_codes: Vec, + modules: Vec, + confidence: f32, +} +``` + +--- + +## Stop Words + +```rust +const STOP_WORDS: &[&str] = &[ + // Articles + "a", "an", "the", + + // Common verbs + "is", "are", "be", "been", "being", + "have", "has", "had", + "do", "does", "did", + "can", "could", "may", "might", "must", "shall", "should", "will", "would", + + // Prepositions + "in", "on", "at", "to", "from", "of", "for", "by", "with", "about", + + // Conjunctions + "and", "or", "but", "nor", "yet", "so", + + // Pronouns + "i", "you", "he", "she", "it", "we", "they", + + // Common words + "not", "no", "yes", "what", "which", "who", "when", "where", "why", "how", + + // Extra + "this", "that", "these", "those", "there", "here", +]; +``` + +--- + +## Tests (6 assertions) + +### `tests/it_symptom_projection.rs` + +```rust +#[test] +fn a1_same_symptom_same_hash() { + // Multiple query formulations normalize to same sym_sha + let q1 = "npm ERR! ERESOLVE unable to resolve typescript"; + let q2 = "npm error: eresolve dependency tree"; + let q3 = "cannot resolve typescript (npm)"; + + let sym1 = project_symptom("npm", q1); + let sym2 = project_symptom("npm", q2); + let sym3 = project_symptom("npm", q3); + + assert_eq!(sym1.sym_sha, sym2.sym_sha); + assert_eq!(sym2.sym_sha, sym3.sym_sha); +} + +#[test] +fn a2_abbrev_expansion() { + // ERESOLVE, ERR, OOM, EACCES expand correctly + let cases = vec![ + ("npm", "ERESOLVE", "error resolve"), + ("npm", "ERR", "error"), + ("cargo", "E0599", "error 0599"), + ("kubectl", "CRD", "custom resource definition"), + ]; + + for (tool, abbrev, expected) in cases { + let expanded = expand_abbrev(tool, abbrev); + assert!(expanded.contains(&expected.replace(" ", ""))); + } +} + +#[test] +fn a3_stop_word_removal() { + // "unable to resolve the dependency tree" → "resolve dependency tree" + let query = "npm is unable to resolve the typescript dependency tree"; + let sym = project_symptom("npm", query); + + // Should NOT contain stop words + for stop in STOP_WORDS { + assert!(!sym.normalised.contains(stop), + "Stop word '{}' should be removed", stop); + } + + // Should contain key terms + assert!(sym.normalised.contains("resolve")); + assert!(sym.normalised.contains("dependency")); + assert!(sym.normalised.contains("typescript")); +} + +#[test] +fn a4_tool_consistency() { + // Same error text under different tools = different hashes + let query = "error 1234 module not found"; + + let sym_npm = project_symptom("npm", query); + let sym_cargo = project_symptom("cargo", query); + + assert_ne!(sym_npm.sym_sha, sym_cargo.sym_sha, + "Tool must be part of signature identity"); +} + +#[test] +fn a5_case_insensitive() { + // "NPM ERROR" = "npm error" + let q1 = project_symptom("npm", "NPM ERROR ERESOLVE"); + let q2 = project_symptom("npm", "npm error eresolve"); + + assert_eq!(q1.sym_sha, q2.sym_sha); +} + +#[test] +fn a6_keyword_order_irrelevant() { + // "error npm resolve" = "npm resolve error" (after sorting) + let q1 = project_symptom("npm", "error npm resolve typescript"); + let q2 = project_symptom("npm", "npm typescript resolve error"); + + assert_eq!(q1.sym_sha, q2.sym_sha); +} +``` + +--- + +## Integration with M3.7.4 Context Endpoint + +### Tier 1 (Exact Match) Lookup + +```rust +// In M3.7.4: context_endpoint() +pub async fn handle_context_query( + query: &str, + tool: Option<&str>, +) -> ContextResult { + // Step 1: Project symptom + let symptom = project_symptom(tool.unwrap_or("unknown"), query); + + // Step 2: Check tier 1 (exact signature match) + if let Some(lesson) = find_lesson_by_sig_sha(&symptom.sym_sha) { + return ContextResult { + tier: 1, + confidence: "high", + source: "lesson", + result: lesson, + }; + } + + // Step 3: Fall back to tier 2 (hybrid search) + let hybrid_results = hybrid_search(query, tool).await?; + + return ContextResult { + tier: 2, + confidence: "medium", + source: "hybrid_search", + results: hybrid_results, + }; +} +``` + +--- + +## Verification Criteria + +✅ Same query variant → same sym_sha +✅ Different queries → different sym_sha +✅ Abbreviations expand correctly per tool +✅ Stop words removed consistently +✅ Tool name included in hash identity +✅ Case insensitive normalization +✅ Keyword order irrelevant (sorted before hash) + +--- + +## Files to Create + +| File | Lines | Purpose | +|------|-------|---------| +| `crates/mem-core/src/symptom_projection.rs` | 250 | Core normalization logic | +| `tests/it_symptom_projection.rs` | 400 | 6 integration test assertions | +| `fixtures/symptoms/` | 3 per tool | Test query examples + expected output | + +--- + +## Dependencies + +- ✅ M3.7.7 (Signature extraction) — provides `Signature` struct and `sig_sha` +- ⏳ M3.7.4 (Context endpoint) — will call `project_symptom()` +- ⏳ M8.2 (Dual-write) — needed for tier 2 fallback (hybrid search) + +--- + +## Success Criteria + +1. **18 unit tests passing** (mem-core lesson extraction still green) +2. **6 symptom projection integration tests passing** +3. **Symptom vectors deterministic** (run twice = same sym_sha) +4. **Integrated with M3.7.4 context endpoint** +5. **Tier 1 lookups work** (exact signature match via symptom projection) + +--- + +## Timeline + +**Estimated: 1–2 days** + +- Day 1: Implement `symptom_projection.rs` + test fixtures +- Day 1.5: Integration tests + M3.7.4 endpoint integration +- Day 2: Verify tier 1 (exact match) lookups work end-to-end + +--- + +## References + +- M3.7.7 (Signature extraction): `crates/mem-core/src/lesson.rs` (871 LOC) +- M3.7.4 (Context endpoint): `crates/mem-cli/src/context_endpoint.rs` (TBD) +- M8 (Hybrid search): `memory-flow.md` § "Search Flow"