Semantic search over a Markdown knowledge base, served over MCP.
groove validate checks a document’s frontmatter against groove-schema.toml. Until
this release the schema could name five keys — title, date, topic, depth,
tags — and no others: the parser deserialised the YAML block into a struct with
those five fields, and serde’s default dropped every other key before anything
downstream could see it. A schema naming status was refused at load time, and a
“strict” mode that flags keys the schema does not name could not exist, because
the validator never learned which keys the block had carried.
A user running a 74-document platform-engineering corpus put it this way (#252):
a corpus with no domain metadata does not need strict mode; a corpus with domain
metadata cannot use it. Their frontmatter carries twenty-one distinct keys, of
which five were declarable. They proposed a one-line loader change —
[fields.status] known = true — so a schema could name a key without checking it.
ADR-0010 removed a --strict
flag that was accepted and discarded, and priced its return: adding it back when
[options].allow_unknown_fields exists is a minor release.
The question this answers: who decides which frontmatter keys exist — the parser or the schema?
Frontmatter is — which every
parser constructs and every reader of the index touches.date is a string the schema checks
with a pattern; depth is a string; this has been the rule since the schema
existed and the reporter did not ask for it to change.source; a schema that made naming a key imply requiring it
would flag every document they have.docs/stability.md). Validation is the one place this corpus needs the keys.known = true as a rule key (the reporter’s proposal). Rejected: on its
own it cannot work, since the parser still drops the key; and once the parser
retains the key it may as well retain the value, at which point known is a
keyword for “an empty table”.pattern would then match against YAML syntax, and a
mapping would pass type = "string".tags would
lose the Vec<String> guarantee its type carries.extra: BTreeMap<String, FieldValue> with three
shapes. Taken.type = "bool". Rejected: it breaks the strings-throughout rule for one
shape assertion that enum = ["true", "false"] already expresses. The
documentation says so rather than leaving it to be discovered.Frontmatter.extra
under its own name. The YAML merge key << is the one exception: it would
surface unexpanded, and it was never a field.!!binary are opaque: the parser keeps the shape’s
name (mapping, nested sequence, binary) and never reads the value. A
null is neither — status: with nothing after it is the key without a value,
and it counts as absent. An alias arrives already resolved to the value it
names, and a standard tag is resolved before the shape is decided, so neither
is a shape of its own.[fields.<name>] accepts any name; an empty
table declares the key and checks nothing; required = true stays explicit.
The existing rules apply to a retained value by its shape when type is not
given, and against the declared type when it is. An opaque value satisfies
required and reports one type_mismatch naming its shape against any rule
that would read it; a null satisfies nothing, so required catches a blank
status: the way it catches a blank title:.[options].allow_unknown_fields = false, or groove validate --strict,
reports each key the schema does not name as one undeclared_field
violation, in key order — a key with no value included, since the key is
still there. The five named fields are always declared. A block
the parser refused is still one frontmatter_unparsed and nothing else. The
flag only tightens: there is no --no-strict.groove validate and nothing else, in this release.
Making a key the schema declares filterable — in search, graph and the
MCP tools — is the intended next step, not a door this record closes (#289
asked for that intent to be written down either way). It is a separate
decision because it touches the index and the frozen result and filter
surfaces, each with its own pricing; this record decides only what the
parser keeps and what validation does with it.groove validate --strict parses again, removed in 1.0.0 by ADR-0010 and back
in 1.9.0 with the meaning that ADR reserved for it.
[index].fail_on_frontmatter_error keeps its own name for its own meaning.Frontmatter gained a field; every parser but Markdown leaves it empty. Code
that constructs a Frontmatter by naming every field has to name this one.unsupported field loads on 1.9.0.
A schema that carried [fields.tags] pattern with no type started reporting
in 1.8.0; nothing else that loaded before changes what it reports, because the
default allow_unknown_fields = true is the behavior 1.8.0 had.#[serde(flatten)]
would have been shorter and does the opposite — it materialises every unknown
value first, which refused documents 1.8.0 indexed.topic: [a]) is still refused, and that a number given to
title is still coerced to the string "123" as it always was. No earlier
test covered either.grooveseek/src/parser/markdown.rs (RawFrontmatter, FieldValueVisitor),
grooveseek/src/schema.rs (check_extra, require_declared_fields).