Files
poimen-memory/tasks/M1.2-standing-query-loader.md

5.3 KiB
Raw Permalink Blame History

M1.2 — Standing-query YAML loader

Field Value
Phase M1 — Gated loop at L1
Size M — 13 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.rsQuerySet, 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.rsuse 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

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

  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 testtests/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_rejectedinfra/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 --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 id contain / or spaces. It is a path segment in three places.

Background: DESIGN.md — Standing queries