2026-08-19 09:52:07 -07:00
|
|
|
|
# M1.2 — Standing-query YAML loader
|
|
|
|
|
|
|
|
|
|
|
|
| Field | Value |
|
|
|
|
|
|
|---|---|
|
|
|
|
|
|
| Phase | M1 — Gated loop at L1 |
|
|
|
|
|
|
| Size | M — 1–3 days |
|
2026-08-22 21:43:23 -07:00
|
|
|
|
| Status | ✅ Done |
|
2026-08-19 09:52:07 -07:00
|
|
|
|
| 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.
|
|
|
|
|
|
|
2026-08-22 21:43:23 -07:00
|
|
|
|
## 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`, `QueryId` from `crates/mem-core/src/domain.rs` — **use these newtypes**, don't create new ones
|
|
|
|
|
|
- `serde_yaml` already used by `mem-ingest` — same pattern
|
|
|
|
|
|
- Validation pattern: `QueryId::new()` already rejects empty strings; extend with charset validation
|
|
|
|
|
|
|
2026-08-19 09:52:07 -07:00
|
|
|
|
## 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 <file>` 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.
|
|
|
|
|
|
|
2026-08-22 21:43:23 -07:00
|
|
|
|
**Command:** `cargo test --test it_query_loader`
|
2026-08-19 09:52:07 -07:00
|
|
|
|
|
|
|
|
|
|
**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
|