# M1.2 — Standing-query YAML loader | Field | Value | |---|---| | Phase | M1 — Gated loop at L1 | | Size | M — 1–3 days | | Status | ⬜ Not started | | Flags | — | | Spec | inlined below | | Blocks | M0.2 | ## Goal Load the standing questions that give the update gate its referent, and fail at load rather than mid-run when one is wrong. ## Facts (inlined — no spec read needed) ```yaml # queries/poimen.yaml project: poimen roots: - /Users/rockliang/workplace/Poimen/agent-rust # matched against session cwd sources: [pi, claude] queries: - id: architecture-decisions question: What architectural decisions were made, with reasoning and rejected alternatives? - id: infra-root-causes question: What infrastructure bugs were found, what was the root cause, how was it isolated? synthesis: question: What is the current state of this project, and what should someone know before working on it? exit_gate: true defaults: memory_budget: 1024 chunk_tokens: 5000 exit_gate: false # L1 default — see below ``` **Why a question is mandatory.** The GRU-Mem memory agent is `φθ(Q, C_t, M_{t-1})` and its update gate is defined as "does this chunk contain useful information *about the problem*". With no `Q` the gate has no referent, and `r_update` is undefinable — which forecloses post-training (M5) entirely. A query with an empty question is a load error, not a warning. **`exit_gate: false` at L1 is deliberate.** Paper §3.3: for "what are *all* the X" questions you cannot know evidence is sufficient without reading everything, so the paper itself provides a without-exit-gate inference mode. L1 extraction is that shape. The gate is still *recorded* — its signal is needed for M5. `query.id` is stable and frozen. It names the L1 memory, the Obsidian note, and the log directory; renaming it orphans all three. ## Steps 1. `QuerySet::load(path)` in `mem-core`, `serde_yaml`. 2. Validate at load: non-empty `project`; at least one query; every `id` unique, non-empty, and `[a-z0-9-]+`; every `question` non-empty; `memory_budget > 0`. 3. Every failure names the file, the query id, and the field. 4. `mem query validate ` prints the resolved set and exits non-zero on any error. 5. Defaults apply per query and are overridable per query. 6. `id` collision across two files for the same project is an error. ## Acceptance - An empty or missing `question` fails at load with a message naming the id. - An id outside `[a-z0-9-]+` fails at load — it becomes a filename. - Defaults resolve; per-query overrides win. ## Verify **Harness:** table-driven over fixture YAML files, one per failure mode. **Integration test** — `tests/it_query_loader.rs`: 1. `a1_valid_loads` — the reference file above resolves to the expected struct. 2. `a2_empty_question_rejected` — error text contains the query id. 3. `a3_duplicate_id_rejected` — error names both occurrences. 4. `a4_bad_id_charset_rejected` — `infra/root causes` is rejected, message mentions the filename constraint. 5. `a5_defaults_and_overrides` — a query without `exit_gate` gets `false`; one with `true` keeps it. 6. `a6_l1_exit_gate_defaults_false` — assert explicitly, because a silent flip to `true` truncates every extraction and looks like a model quality problem. 7. `a7_missing_question_field` — absent key behaves as empty, same error. **Command:** `cargo test -p mem-core query_loader` **False pass:** - Testing only the happy path. Every assertion except 1 and 5 is a rejection test, and rejection is the entire point of load-time validation. - Asserting an error occurred without asserting the message names the offending id. "invalid config" sends someone to read the whole file by hand. ## Traps - Allowing an empty question "for now". It loads, the gate has no referent, the model updates on nearly everything, and it reads as a bad model rather than a bad config. - Letting `id` contain `/` or spaces. It is a path segment in three places. --- Background: [DESIGN.md](../DESIGN.md) — Standing queries