What you're trying to do
.vouch/config.yaml is currently parsed as an untyped dict (yaml.safe_load)
and read defensively at each call site — e.g. storage.py writes the starter
config as a literal dict, and proposals.py re-reads it with nested .get() +
isinstance guards and a silent except Exception fallback to {} for
review.approver_role. As more features grow config keys (retrieval.backends,
retrieval.reflex, mcp.publish_skills, page_kinds, …) this pattern spreads
the schema across the codebase, swallows typos (reveiw: is silently ignored),
and gives no single source of truth for what a valid config looks like.
A typed Config pydantic model parsed once at store-open turns config into a
load-bearing, validated shape — consistent with the project's "prefer pydantic
models for any persisted shape" rule.
Suggested shape
- New
Config (and nested ReviewConfig, RetrievalConfig, …) pydantic models
in models.py, with defaults matching today's starter config.
KBStore.config parses config.yaml into Config once (cached), raising a
clear ConfigError with the offending key/path on malformed input.
- Unknown top-level keys produce a warning surfaced by
vouch doctor rather than
silent drop (so typos are visible, not lost).
- All current ad-hoc readers (
proposals.py, retrieval backend selection, etc.)
switch to store.config.<field>.
Acceptance
- A
config.yaml with a mistyped key surfaces a warning in vouch doctor.
- A malformed value (e.g.
retrieval.default_limit: "ten") fails fast with a
per-field error instead of a silent fallback.
- An existing
.vouch/ with no mcp:/retrieval: blocks loads with documented
defaults (no behavior change for current KBs) — covered by a round-trip test.
Out of scope
- A config-migration tool (separate from
vouch migrate for the on-disk KB).
- Env-var overlays on top of the file (follow-up).
What you're trying to do
.vouch/config.yamlis currently parsed as an untypeddict(yaml.safe_load)and read defensively at each call site — e.g.
storage.pywrites the starterconfig as a literal dict, and
proposals.pyre-reads it with nested.get()+isinstanceguards and a silentexcept Exceptionfallback to{}forreview.approver_role. As more features grow config keys (retrieval.backends,retrieval.reflex,mcp.publish_skills,page_kinds, …) this pattern spreadsthe schema across the codebase, swallows typos (
reveiw:is silently ignored),and gives no single source of truth for what a valid config looks like.
A typed
Configpydantic model parsed once at store-open turns config into aload-bearing, validated shape — consistent with the project's "prefer pydantic
models for any persisted shape" rule.
Suggested shape
Config(and nestedReviewConfig,RetrievalConfig, …) pydantic modelsin
models.py, with defaults matching today's starter config.KBStore.configparsesconfig.yamlintoConfigonce (cached), raising aclear
ConfigErrorwith the offending key/path on malformed input.vouch doctorrather thansilent drop (so typos are visible, not lost).
proposals.py, retrieval backend selection, etc.)switch to
store.config.<field>.Acceptance
config.yamlwith a mistyped key surfaces a warning invouch doctor.retrieval.default_limit: "ten") fails fast with aper-field error instead of a silent fallback.
.vouch/with nomcp:/retrieval:blocks loads with documenteddefaults (no behavior change for current KBs) — covered by a round-trip test.
Out of scope
vouch migratefor the on-disk KB).