5.3 KiB
M1.2 — Standing-query YAML loader
| Field | Value |
|---|---|
| Phase | M1 — Gated loop at L1 |
| Size | M — 1–3 days |
| Status | ✅ Done |
| 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.
Files
| Action | Path |
|---|---|
| Create | crates/mem-core/src/query.rs — QuerySet, Query, SynthesisQuery, QueryLoadError |
| Modify | crates/mem-core/src/lib.rs — add pub mod query; and re-exports |
| Create | queries/poimen.yaml — first real standing query file |
| Create | tests/it_query_loader.rs — integration tests (workspace root) |
| Create | fixtures/query-valid.yaml — test fixture (valid) |
| Create | fixtures/query-empty-question.yaml — test fixture (empty question) |
| Create | fixtures/query-duplicate-id.yaml — test fixture (duplicate id) |
| Create | fixtures/query-bad-charset.yaml — test fixture (invalid id chars) |
Dependencies
| Crate | Where | Already present? |
|---|---|---|
serde_yaml |
workspace deps | ✅ yes (in workspace [workspace.dependencies]) |
serde |
crates/mem-core/Cargo.toml |
✅ yes |
regex |
crates/mem-core/Cargo.toml |
❌ add (for [a-z0-9-]+ validation) or hand-roll |
Existing code to reuse
ProjectId,QueryIdfromcrates/mem-core/src/domain.rs— use these newtypes, don't create new onesserde_yamlalready used bymem-ingest— same pattern- Validation pattern:
QueryId::new()already rejects empty strings; extend with charset validation
Facts (inlined — no spec read needed)
# 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
QuerySet::load(path)inmem-core,serde_yaml.- Validate at load: non-empty
project; at least one query; everyidunique, non-empty, and[a-z0-9-]+; everyquestionnon-empty;memory_budget > 0. - Every failure names the file, the query id, and the field.
mem query validate <file>prints the resolved set and exits non-zero on any error.- Defaults apply per query and are overridable per query.
idcollision across two files for the same project is an error.
Acceptance
- An empty or missing
questionfails 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:
a1_valid_loads— the reference file above resolves to the expected struct.a2_empty_question_rejected— error text contains the query id.a3_duplicate_id_rejected— error names both occurrences.a4_bad_id_charset_rejected—infra/root causesis rejected, message mentions the filename constraint.a5_defaults_and_overrides— a query withoutexit_gategetsfalse; one withtruekeeps it.a6_l1_exit_gate_defaults_false— assert explicitly, because a silent flip totruetruncates every extraction and looks like a model quality problem.a7_missing_question_field— absent key behaves as empty, same error.
Command: cargo test --test it_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
idcontain/or spaces. It is a path segment in three places.
Background: DESIGN.md — Standing queries