Repository navigation
validate_spec reports false positives for every single-select enum field (status, priority, assignee, reviewer) #315
Description
validate_spec reports false positives for every single-select enum field (status, priority, assignee, reviewer)
Repository: https://github.com/codervisor/leanspec
Issues: https://github.com/codervisor/leanspec/issues
Package: @leanspec/mcp@0.3.0 (latest — this is not a version-skew problem)
Adapter: markdown (default)
Verified on commit: ee122d6fe4bdf3c22f2507e2944f5a213ddcf7a6 (main, 2026-05-20) — bug is still present at HEAD
Summary
validate_spec rejects every spec in a markdown project with:
field 'status': value type does not match field kind
field 'priority': value type does not match field kind
The specs are valid. The frontmatter is well-formed, the values are legal enum members, and the same files parse correctly through list_specs / get_spec / the CLI. The failure is an unconditional type-dispatch gap in the read path.
Affected fields: all FieldKind::Enum fields with multi: false — that is the built-in status, priority, assignee, reviewer (and any custom single-select enum). tags is unaffected because it is multi: true.
Impact: the tool is unusable for validation in any markdown project. It also produces a misleading signal for AI agents, which naturally try to "fix" the spec — but no frontmatter edit can clear the error.
Root cause
Two halves of the codebase disagree about how a single-select enum value is represented.
1. The markdown adapter writes single-select enums as FieldValue::String (singular)
rust/leanspec-core/src/adapters/markdown/mod.rs:367-382
fields.insert(field::STATUS.into(), FieldValue::String(fm.status.to_string())); // 367-370
if let Some(p) = fm.priority {
fields.insert(field::PRIORITY.into(), FieldValue::String(p.to_string())); // 371-373
}
if let Some(ref a) = fm.assignee {
fields.insert(field::ASSIGNEE.into(), FieldValue::String(a.clone())); // 377-379
}
if let Some(ref r) = fm.reviewer {
fields.insert(field::REVIEWER.into(), FieldValue::String(r.clone())); // 380-382
}Note tags uses the plural variant, which is why it validates fine:
if !fm.tags.is_empty() {
fields.insert(field::TAGS.into(), FieldValue::Strings(fm.tags.clone())); // 374-376
}2. The validator only accepts FieldValue::Strings (plural) for enum kinds
rust/leanspec-mcp/src/tools/markdown_only.rs:146-163
(
FieldKind::Enum {
options,
allow_custom,
.. // <-- `multi` is discarded here
},
FieldValue::Strings(values), // <-- only the plural variant is matched
) => { ... }There is no arm matching FieldKind::Enum against FieldValue::String. The value therefore falls through to the catch-all at line 166:
_ => Err("value type does not match field kind".into()),3. The model's own documentation says the plural variant covers both cases
rust/leanspec-core/src/model.rs:180-198
/// [`FieldValue::Strings`] covers single-select (len 1), multi-select (len N),
/// and free-form string lists (tags). There is no `Null` variant: an absent
/// key in [`SpecDoc::fields`] means the field is not set.
pub enum FieldValue {
String(String),
...
Strings(Vec<String>),So either the adapter should emit Strings(vec![s]) for single-select, or validate_value should accept String(s) for FieldKind::Enum { multi: false, .. }. Right now the two sides simply disagree.
4. The write path gets it right, which isolates the defect to the read path
rust/leanspec-mcp/src/tools/create_spec.rs:106-139 normalizes a bare string into a Strings vector before validating:
FieldKind::Enum { options, multi, allow_custom, .. } => {
let strings: Vec<String> = match value {
Value::String(s) => vec![s.clone()], // string -> vec![s]
Value::Array(arr) => ...,
_ => return Err("expected string or array of strings".into()),
};
if !*multi && strings.len() > 1 {
return Err("single-select field accepts only one value".into());
}
...
FieldValue::Strings(strings)
}That is why the error string single-select field accepts only one value exists in the binary while single-select fields still fail validation: the single-select concept is implemented on write, but not on validate.
Minimal reproduction
Create a project with a single spec containing the minimum required fields:
mkdir -p /tmp/leanspec-repro/specs/900-probe
cd /tmp/leanspec-repro
cat > .lean-spec/config.json <<'EOF'
{ "specsDir": "specs" }
EOF
cat > specs/900-probe/README.md <<'EOF'
---
status: draft
created: 2026-01-01
---
# Probe
body
EOFThen call the MCP tool validate_spec (or validate_spec for all specs) and observe:
{
"checked": 1,
"results": [
{
"id": "900-probe",
"title": "Probe",
"valid": false,
"errors": ["field 'status': value type does not match field kind"]
}
],
"total_errors": 1,
"valid": false
}status: draft is a legal value of a declared enum option, yet it is rejected.
Evidence that the specs are not at fault
Each of these was run against the reproduction above.
1. It reproduces on an untouched, freshly created spec
The probe contains nothing but status + created. This rules out project-specific content as the cause.
2. Quoting the value changes nothing
status: "complete"
priority: "high"Identical errors. This rules out YAML scalar-type parsing.
3. Each field fails independently
Removing priority from the frontmatter leaves exactly:
field 'status': value type does not match field kind
…so the check fails per-field and unconditionally, for every non-multi enum present.
4. Only single-select enums are affected; other kinds pass
In one file, with status (single enum), tags (multi enum), breaking (bool) and due (text):
---
status: draft
created: 2026-01-01
tags: []
breaking: true
due: 2026-05-05
---Result — only the single-select enum fails:
field 'status': value type does not match field kind
5. Adding more single-select enums adds exactly one error each
---
status: draft
created: 2026-01-01
tags:
- zzz
assignee: alice
reviewer: bob
---field 'status': value type does not match field kind
field 'assignee': value type does not match field kind
field 'reviewer': value type does not match field kind
tags (multi) passes; all three single-select enums fail. This is the decisive experiment: the discriminator is precisely the multi flag.
6. The same files read back correctly through every other code path
list_specs returns the parsed values as plain strings:
{ "id": "001-board", "status": "complete", "priority": "high", "tags": ["board", "hardware", "driver"] }The CLI leanspec view 001-board prints Status: complete, Priority: high. So parsing is fine; only validate_value rejects the parsed representation.
7. The schema confirms the shapes differ exactly as described
leanspec schema show leanspec:base -o json:
status -> {"kind":"enum","options":[...]} // no `multi` key
priority -> {"kind":"enum","options":[...]} // no `multi` key
tags -> {"kind":"enum","options":[],"multi":true,...} // `multi` present(multi is #[serde(default)], so it is omitted from the schema JSON when false. That part is internally consistent — the mismatch is on the value side: the markdown adapter emits FieldValue::String for exactly these multi: false fields, while validate_value only matches FieldValue::Strings.)
Suggested fix
Option A (validator-side) — accept the singular variant for single-select enums in validate_value, keeping the existing plural arm:
(
FieldKind::Enum { options, allow_custom, multi, .. },
FieldValue::String(v),
) if !*multi => {
if *allow_custom || options.is_empty() {
return Ok(());
}
if !options.iter().any(|o| o.value == *v) {
return Err(format!("value '{v}' is not a declared option"));
}
Ok(())
}Option B (adapter-side) — normalize at the source in markdown/mod.rs so single-select enums emit FieldValue::Strings(vec![s]), matching both the model doc-comment and what create_spec produces. This makes the two representations consistent everywhere, but touches every adapter that currently emits the singular form (jira/adapter.rs, ado.rs, github/mod.rs all insert FieldValue::String for status/priority/assignee), so it is the larger change.
Option A is the smaller, lower-risk fix and matches the documented contract at model.rs:182-184.
Additional notes
-
Not a version-skew issue.
0.3.0is the latest published version (dist-tags:latest = 0.3.0,dev = 0.2.28-dev.23044283727). The published tarball'sbinaries/linux-x64/leanspec-mcpis byte-identical (md5d8e33d6dad77b0f5ddef6ff0996c4e73) to the binary that exhibits the bug, and the defect is still present atmainHEAD. -
The CLI cannot be used to cross-check.
leanspec validatereturnsError: 'validate' is not yet migrated to the adapter API, so the buggyvalidate_specMCP tool has no CLI equivalent.list/view/schema showwere used as the cross-checks above instead. -
statusmust not be removed as a workaround. It isrequired, so dropping it just replaces the false positive with a legitimateMissing required field: status. -
Related, possibly the same oversight:
FieldKind::References { multi }and the otherserde(default)enum sub-fields follow the same pattern, so it may be worth auditing whethervalidate_valuehandlesReferences/Checklistvariants symmetrically when values come from a non-markdown adapter.
Activity