Skip to content

Commit a6b1d43

Browse files
author
Test User
committed
feat(rlm): add KnowledgeGraphValidator::from_config() with thesaurus loading + 5 tests Refs #2671
1 parent b109f12 commit a6b1d43

3 files changed

Lines changed: 313 additions & 0 deletions

File tree

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Implementation Plan: Extract remaining value from PR #2692
2+
3+
**Status**: Draft
4+
**Research Doc**: `.docs/research-pr2692-kg-validation.md`
5+
**Author**: AI Agent
6+
**Date**: 2026-06-29
7+
**Estimated Effort**: 30 minutes
8+
9+
## Overview
10+
11+
### Summary
12+
Add `from_config()` method to `KnowledgeGraphValidator` (deduplicating `build_validator` in rlm.rs and executor/mod.rs), add `#[derive(Debug)]` to `ExecuteResult`, and add 5 tests for `from_config()`. Close PR #2692 as superseded.
13+
14+
### Scope
15+
**In Scope:**
16+
- `from_config()` on `KnowledgeGraphValidator`
17+
- `strictness()` accessor
18+
- 5 unit tests for `from_config()`
19+
- `#[derive(Debug)]` on `ExecuteResult`
20+
21+
**Out of Scope:**
22+
- Restructuring QueryLoop validation (main has a better architecture)
23+
- Adding `kg_thesaurus_path` config field (main uses `thesaurus` field differently)
24+
- Noise files from PR (39 files of handoff/review artifacts)
25+
26+
**Avoid At All Cost:**
27+
- Rewriting existing validation architecture
28+
- Cherry-picking the PR's competing QueryLoop approach
29+
- Pulling in .review_tmp/, .handoff/, .beads/, .sessions/ files
30+
31+
## Architecture
32+
33+
### Design Decision
34+
Main already has a working validation architecture (excutor-based validate_command). The PR's approach (direct validator field in QueryLoop) is incompatible and unnecessary. We extract only the complementary pieces.
35+
36+
### Simplicity Check
37+
The simplest approach: add `from_config()` to `KnowledgeGraphValidator` as a convenience constructor. Don't change anything else. Don't break existing code.
38+
39+
## File Changes
40+
41+
### Modified Files
42+
| File | Changes |
43+
|------|---------|
44+
| `crates/terraphim_rlm/src/validator.rs` | Add `from_config()`, `strictness()` accessor, 5 tests |
45+
| `crates/terraphim_rlm/src/query_loop.rs` | Add `#[derive(Debug)]` on `ExecuteResult` |
46+
47+
### Files NOT Touched
48+
- `rlm.rs` — existing `build_validator` stays (migration deferred)
49+
- `executor/mod.rs` — existing pattern stays
50+
- `config.rs` — existing fields stay
51+
- All noise files — excluded
52+
53+
## API Design
54+
55+
### `KnowledgeGraphValidator::from_config()`
56+
```rust
57+
/// Build a validator from RLM config fields.
58+
///
59+
/// Eliminates the build_validator() duplication between rlm.rs and
60+
/// executor/mod.rs. Optionally loads a thesaurus from disk.
61+
pub fn from_config(
62+
strictness: KgStrictness,
63+
max_retries: u32,
64+
thesaurus_path: Option<&str>,
65+
) -> Self
66+
```
67+
68+
### `KnowledgeGraphValidator::strictness()`
69+
```rust
70+
/// Return the strictness level this validator is configured with.
71+
pub fn strictness(&self) -> KgStrictness
72+
```
73+
74+
## Test Strategy
75+
76+
### Tests to Add (in validator.rs)
77+
| Test | Purpose |
78+
|------|---------|
79+
| `test_from_config_normal_no_path_has_no_thesaurus` | GAP-2: deduplication |
80+
| `test_from_config_permissive_no_path_passes_silently` | Permissive always passes |
81+
| `test_from_config_strict_respects_max_retries` | Config propagation |
82+
| `test_from_config_bad_path_falls_back_gracefully` | GAP-1: no panic on bad path |
83+
| `test_from_config_valid_thesaurus_json` | GAP-1: real thesaurus loading |
84+
85+
## Implementation Steps
86+
87+
### Step 1: Add `from_config()` and `strictness()` to validator.rs
88+
**Files:** `crates/terraphim_rlm/src/validator.rs`
89+
**Description:** Add the two methods after the existing constructors
90+
**Tests:** Implicit (next step)
91+
92+
### Step 2: Add tests for `from_config()`
93+
**Files:** `crates/terraphim_rlm/src/validator.rs`
94+
**Description:** Add 5 unit tests in the `mod tests` block
95+
**Dependencies:** Step 1
96+
97+
### Step 3: Add `#[derive(Debug)]` to `ExecuteResult`
98+
**Files:** `crates/terraphim_rlm/src/query_loop.rs`
99+
**Description:** Add `Debug` derive to the enum
100+
101+
### Step 4: Verify build + close PR
102+
**Files:** None
103+
**Description:** `cargo check --workspace`, `cargo test -p terraphim_rlm --lib`, close PR #2692
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Research Document: PR #2692 — Wire KG Validation into QueryLoop Hot Paths
2+
3+
**Status**: Draft
4+
**Author**: AI Agent
5+
**Date**: 2026-06-29
6+
7+
## Executive Summary
8+
9+
PR #2692 aimed to wire KG validation into QueryLoop hot paths but was created before the merge sprint. The merge sprint landed multiple PRs (#2902, #2913, #3012, #3033, #3034) that independently implemented most of the PR's goals. The PR is largely superseded. Remaining extractable value: `from_config()` on `KnowledgeGraphValidator`, `#[derive(Debug)]` on `ExecuteResult`, and associated tests.
10+
11+
## Essential Questions Check
12+
13+
| Question | Answer | Evidence |
14+
|----------|--------|----------|
15+
| Energising? | Yes | Closes a stale PR and extracts remaining value |
16+
| Leverages strengths? | Yes | Understanding of already-merged codebase |
17+
| Meets real need? | Yes | Issue #2671 still closed; PR adds missing `from_config()` deduplication |
18+
19+
**Proceed**: Yes (3/3 YES)
20+
21+
## Problem Statement
22+
23+
### Description
24+
PR #2692 (47 files, 9566 additions) attempts to wire KG validation but has 39 noise files (handoff artifacts, CI fixes, review dumps) and 8 core RLM files. The core work was independently implemented on main through the merge sprint (19 RLM files changed, 1773 lines added). The PR now has merge conflicts and cannot be merged as-is.
25+
26+
### Impact
27+
- Wastes review time on noise
28+
- PR blocks issue #2671 which should be closed
29+
- `from_config()` deduplication method is missing from main
30+
31+
### Success Criteria
32+
1. Extract unique remaining value from PR #2692
33+
2. Apply it cleanly to main
34+
3. Verify build passes
35+
4. Close PR #2692 as superseded
36+
37+
## Current State Analysis
38+
39+
### What PR #2692 Changed (merge-base to PR branch)
40+
| File | Lines | What |
41+
|------|-------|------|
42+
| `query_loop.rs` | +214 | Validator field, validation in hot paths, tests |
43+
| `rlm.rs` | +32/-? | Arc<Validator>, from_config calls |
44+
| `validator.rs` | +134 | from_config() with thesaurus loading + 5 tests |
45+
| `executor/mod.rs` | +20 | with_validator() wiring in select_executor |
46+
| `executor/docker.rs` | +27 | validator field, with_validator(), validate impl |
47+
| `executor/firecracker.rs` | +32 | Same pattern |
48+
| `executor/local.rs` | +48 | Same pattern + 2 tests |
49+
| `config.rs` | +21 | kg_thesaurus_path field, blocks_unknown Normal fix |
50+
51+
### What's Already in Main (merge-base to current main)
52+
| Feature | Where | How |
53+
|---------|-------|-----|
54+
| Executor validate() with KG | executor/{local,docker,firecracker}.rs | `Option<Arc<KnowledgeGraphValidator>>` field |
55+
| with_validator() | executor/{local,docker}.rs | `pub fn with_validator(mut self, v: Option<Arc<...>>)` |
56+
| validate_command() in QueryLoop | query_loop.rs | `self.executor.validate(input).await` with retry logic |
57+
| validation_retries cell | query_loop.rs | `Cell<u32>` for retry tracking |
58+
| blocks_unknown Normal fix | config.rs | Already returns true for Normal mode |
59+
| Arc<KnowledgeGraphValidator> in rlm | rlm.rs | Already present |
60+
61+
### What's NOT Yet in Main (extractable from PR)
62+
| Feature | Value |
63+
|---------|-------|
64+
| `from_config()` on KnowledgeGraphValidator | Eliminates duplicate build_validator in rlm.rs and executor/mod.rs |
65+
| `#[derive(Debug)]` on ExecuteResult | Minor but useful for debugging |
66+
| 5 from_config tests in validator.rs | Needed test coverage |
67+
| `strictness()` accessor | Exposes validator strictness |
68+
69+
## Constraints
70+
71+
### Technical Constraints
72+
- Must not break existing validation architecture (executor-based validate_command)
73+
- Must compile with `cargo check --workspace`
74+
- Must pass `cargo clippy --workspace`
75+
- Must pass `cargo fmt --all -- --check`
76+
77+
## Vital Few
78+
79+
| Constraint | Why Vital | Evidence |
80+
|------------|-----------|----------|
81+
| Only add from_config, don't restructure | Existing architecture works; PR's approach is incompatible | Main already has 1773 lines of validated RLM changes |
82+
| Don't pull noise files | 39 files are review artifacts, not code | File list shows .review_tmp/, .handoff/, .beads/, .sessions/ |
83+
84+
## Risks
85+
86+
| Risk | Likelihood | Impact | Mitigation |
87+
|------|------------|--------|------------|
88+
| `from_config()` signature mismatch with main | Medium | Low | Check existing validator API before implementing |
89+
| Tests fail due to different test framework | Low | Medium | Run tests before commit |
90+
91+
## Recommendations
92+
93+
**Proceed** with minimal extraction: add `from_config()` (+ tests) and `#[derive(Debug)]` to main, close PR #2692.

crates/terraphim_rlm/src/validator.rs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,68 @@ impl KnowledgeGraphValidator {
469469
pub fn has_role_graph(&self) -> bool {
470470
self.role_graph.is_some()
471471
}
472+
473+
/// Return the strictness level this validator is configured with.
474+
pub fn strictness(&self) -> KgStrictness {
475+
self.config.strictness
476+
}
477+
478+
/// Build a validator from RLM config fields.
479+
///
480+
/// Eliminates the `build_validator()` duplication between rlm.rs and
481+
/// executor/mod.rs (GAP-2). Optionally loads a thesaurus from disk
482+
/// so Normal/Strict validation performs real term matching (GAP-1).
483+
pub fn from_config(
484+
strictness: KgStrictness,
485+
max_retries: u32,
486+
thesaurus_path: Option<&str>,
487+
) -> Self {
488+
let mut vcfg = match strictness {
489+
KgStrictness::Permissive => ValidatorConfig::permissive(),
490+
KgStrictness::Normal => ValidatorConfig::default(),
491+
KgStrictness::Strict => ValidatorConfig::strict(),
492+
};
493+
vcfg.max_retries = max_retries;
494+
let mut validator = Self::new(vcfg);
495+
496+
match thesaurus_path {
497+
Some(path) => match std::fs::read_to_string(path) {
498+
Ok(json) => match serde_json::from_str::<Thesaurus>(&json) {
499+
Ok(thesaurus) => {
500+
log::info!(
501+
"KG validator: loaded {} terms from {}",
502+
thesaurus.len(),
503+
path
504+
);
505+
validator = validator.with_thesaurus(thesaurus);
506+
}
507+
Err(e) => log::warn!(
508+
"KG validator: could not parse thesaurus at '{}': {}. \
509+
Real term matching is disabled.",
510+
path,
511+
e
512+
),
513+
},
514+
Err(e) => log::warn!(
515+
"KG validator: could not read thesaurus at '{}': {}. \
516+
Real term matching is disabled.",
517+
path,
518+
e
519+
),
520+
},
521+
None if strictness != KgStrictness::Permissive => {
522+
log::warn!(
523+
"KG validator: no thesaurus configured (thesaurus_path is None). \
524+
{:?} mode will not perform real term matching. \
525+
Set thesaurus_path to a JSON thesaurus file to enable.",
526+
strictness
527+
);
528+
}
529+
None => {}
530+
}
531+
532+
validator
533+
}
472534
}
473535

474536
/// Extract words from a command string.
@@ -766,4 +828,59 @@ mod tests {
766828
);
767829
assert!(!result.matched_terms.is_empty());
768830
}
831+
832+
// -- from_config tests (GAP-2: deduplication; GAP-1: thesaurus loading) --
833+
834+
#[test]
835+
fn test_from_config_normal_no_path_has_no_thesaurus() {
836+
let v = KnowledgeGraphValidator::from_config(KgStrictness::Normal, 3, None);
837+
assert!(!v.has_thesaurus());
838+
assert_eq!(v.strictness(), KgStrictness::Normal);
839+
assert_eq!(v.config().max_retries, 3);
840+
}
841+
842+
#[test]
843+
fn test_from_config_permissive_no_path_passes_silently() {
844+
let v = KnowledgeGraphValidator::from_config(KgStrictness::Permissive, 0, None);
845+
assert!(!v.has_thesaurus());
846+
let result = v.validate("any command here").unwrap();
847+
assert!(result.passed);
848+
}
849+
850+
#[test]
851+
fn test_from_config_strict_respects_max_retries() {
852+
let v = KnowledgeGraphValidator::from_config(KgStrictness::Strict, 5, None);
853+
assert_eq!(v.config().max_retries, 5);
854+
assert_eq!(v.strictness(), KgStrictness::Strict);
855+
}
856+
857+
#[test]
858+
fn test_from_config_bad_path_falls_back_gracefully() {
859+
let v = KnowledgeGraphValidator::from_config(
860+
KgStrictness::Normal,
861+
3,
862+
Some("/nonexistent/path/thesaurus.json"),
863+
);
864+
assert!(!v.has_thesaurus());
865+
let result = v.validate("some command").unwrap();
866+
assert!(result.passed);
867+
}
868+
869+
#[test]
870+
fn test_from_config_valid_thesaurus_json() {
871+
use std::io::Write;
872+
let dir = tempfile::tempdir().unwrap();
873+
let path = dir.path().join("thesaurus.json");
874+
let thesaurus_json = r#"{"name":"test","data":{}}"#;
875+
let mut f = std::fs::File::create(&path).unwrap();
876+
f.write_all(thesaurus_json.as_bytes()).unwrap();
877+
drop(f);
878+
879+
let v = KnowledgeGraphValidator::from_config(
880+
KgStrictness::Normal,
881+
3,
882+
Some(path.to_str().unwrap()),
883+
);
884+
assert!(v.has_thesaurus());
885+
}
769886
}

0 commit comments

Comments
 (0)