# M1.4 — Gate-response parser | Field | Value | |---|---| | Phase | M1 — Gated loop at L1 | | Size | M — 1–3 days | | Status | ✅ Done | | Flags | — | | Spec | inlined below | | Blocks | M1.3 | ## Goal Turn the model's tagged output into `(U_t, M̂_t, E_t)`, strictly — because a lenient parser silently fabricates gate decisions. ## Files | Action | Path | |---|---| | Create | `crates/mem-core/src/gate_parser.rs` — `parse_gate_response()`, `GateResponse`, `ParseError` | | Modify | `crates/mem-core/src/lib.rs` — add `pub mod gate_parser;` and re-exports | | Create | `fixtures/gate-response-valid.txt` — well-formed model output | | Create | `fixtures/gate-response-nested-think.txt` — nested `` tags | | Create | `tests/it_gate_parser.rs` — integration tests (workspace root) | ## Dependencies **None new.** Use `str::find()` and `str::rfind()` for tag extraction. No regex crate needed — the tags are simple XML-like delimiters, not a grammar. ## Existing code to reuse - Pattern reference: `lesson.rs` uses similar string scanning for error markers (`markers()`, `GENERIC_MARKERS`). Same technique, different tags. - `thiserror` already in `mem-core` deps for error enum derivation. ## Expected input/output ``` Input: This chunk shows a kubectl error. The user fixed it by adding --namespace. yes kubectl get pods fails without --namespace; fixed by adding --namespace=kube-system continue Output: GateResponse { think: "This chunk shows a kubectl error. The user fixed it by adding --namespace.", update_gate: true, candidate: "kubectl get pods fails without --namespace; fixed by adding --namespace=kube-system", exit_gate: false, } ``` ## Facts (inlined — no spec read needed) Expected response shape: ``` ... yes|no candidate memory, or the previous memory verbatim continue|end ``` Semantics, from the paper: | tag | value | meaning | |---|---|---| | `` | `yes` | `U_t = true` — memory becomes `M̂_t` | | `` | `no` | `U_t = false` — memory stays `M_{t-1}`, chunk discarded | | `` | `continue` | `E_t = false` | | `` | `end` | `E_t = true` | **Strict parsing is the design, matching the paper's `r_format`:** it awards 1 only when *every* turn in the trajectory parses, 0 otherwise, "because we can not infer whether the incorrect format is caused by the previous erroneous parsing". So: exactly one of each tag, properly closed, `` content exactly `yes` or `no` after trimming, `` exactly `continue` or `end`. Anything else is a `ParseError` naming which tag failed and carrying the raw text. **A parse failure must not default.** Defaulting `U_t` to `false` silently drops evidence; defaulting to `true` pollutes memory. The loop (M1.5) decides the retry policy; the parser only reports. Reasoning models emit `` natively, which can nest or repeat. Extract by locating the *last* `` before the first ``, not by regex over the whole body. ## Steps 1. `parse_gate_response(&str) -> Result` in `mem-core`. 2. `GateResponse { think: String, update_gate: bool, candidate: String, exit_gate: bool }`. 3. Reject duplicates of any tag, a missing tag, an unclosed tag, and any ``/`` value outside the allowed set. 4. `ParseError` variants name the tag and include the raw response, truncated. 5. Trim surrounding whitespace inside tags; do not otherwise normalise — memory text is preserved verbatim. 6. When `U_t = false`, still capture `candidate` so the log can show what the model *would* have written. The loop ignores it; the record is diagnostic. ## Acceptance - All four tags parse from a well-formed response. - Every malformed shape errors, naming the failing tag. - No input produces a default `U_t` or `E_t`. ## Verify **Harness:** table-driven over recorded real responses plus hand-built malformed cases. Capture real ones with `MEM_LLM_RECORD` from M1.1. **Integration test** — `tests/it_gate_parser.rs`: 1. `a1_wellformed_yes_continue` — `U=true`, `E=false`, candidate matches. 2. `a2_wellformed_no_end` — `U=false`, `E=true`. 3. `a3_missing_check_errors` — error names `check`. 4. `a4_duplicate_update_errors` — two `` blocks error. 5. `a5_bad_check_value_errors` — `maybe` errors, message shows the value. 6. `a6_unclosed_tag_errors` — `` never closed. 7. `a7_nested_think` — a response with `` inside `` still finds the right boundary. 8. `a8_no_defaults` — property test over 1000 random mutations of a valid response: every result is either an exact parse or an error, never a silently-defaulted `GateResponse`. 9. `a9_real_responses` — every recorded real response parses. **Command:** `cargo test --test it_gate_parser` **False pass:** - A regex that finds the first `` and stops. It passes 1–2 and silently accepts duplicates, which is assertion 4's job. - Testing only hand-written responses. Real 3B output has whitespace, markdown fences and stray prose the author would not think to write — assertion 9 is the only one that sees it. - Omitting assertion 8. A parser with `unwrap_or(false)` anywhere passes every positive test and quietly halves the update rate. ## Traps - Defaulting on parse failure. The one thing that must not happen: a fabricated gate decision is indistinguishable from a real one in the log, and it poisons the M5 training data at the source. - Normalising the candidate memory (collapsing whitespace, stripping markdown). Memory text is content; the hash and every downstream projection depend on it being verbatim. --- Background: [DESIGN.md](../DESIGN.md) — Architecture · paper §3.2.1 `r_format`