Skip to content

fix(schema): reject duplicate [[fields.field]] names - #73

Merged
edochi merged 2 commits into
mainfrom
fix/duplicate-field-names
Aug 20, 2026
Merged

fix(schema): reject duplicate [[fields.field]] names#73
edochi merged 2 commits into
mainfrom
fix/duplicate-field-names

Conversation

@edochi

@edochi edochi commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Implements invariant 10. Refs TODO-0196 (arriving in #71).

The bug

Two [[fields.field]] entries sharing a name were accepted silently and the last one won. Downstream lookup keys on the bare name — cmd/check/validate.rs:43 collects into a HashMap<&str, _>, as does FieldValidators::build — so collect() kept the final entry and discarded the earlier one without a word.

The failure was actively misleading, not merely lossy. Declaring status as String under blog/** and Integer under projects/** produced two violations against blog/post.md, both artifacts of the collapse rather than of the vault:

status │ Wrong type  │ type Integer               │ blog/post.md (got String)
status │ Not allowed │ allowed in ["projects/**"] │ blog/post.md

The blog/** declaration simply vanished, so the blog file was judged against a rule written for a different directory. None of the existing invariants caught it — invariant 8 covers shape conflicts (foo vs foo.bar), not two declarations of the same leaf.

The fix

Invariant 10: no two entries may share a name. Checked before the per-field loop so a structural problem is reported ahead of any per-field complaint.

Rejected on the name alone, regardless of whether the entries agree on type. Scoping a field per directory is a real need, but a repeated entry shouldn't back into it — that belongs to the [[scope]] design in TODO-0194. Encoded as its own test so the rule isn't relaxed by accident.

Names are matched exactly. YAML keys are case-sensitive, so status and Status are genuinely different frontmatter fields and both stay legal — case-insensitive rejection would forbid a valid vault. Invariant 7 already canonicalises dotted names, so no normalisation ambiguity remains.

Relaxing this later is backward-compatible: configs that error would start working, none that work would start failing.

Also

Documents invariant 9 (Array(Object) not representable on disk), which was enforced in the body since TODO-0155 but missing from the docstring. The documented contract promised eight rules while the code enforced nine; both now say ten.

Verified on 1.97.1

The original repro now fails at config load across every command, exit 2:

Error: mdvs.toml is invalid: field 'status' is declared more than once — each
[[fields.field]] name must be unique. Merge the entries into one, or rename one
of them. — fix the file or run 'mdvs init --force'
Check Result
cargo test --features testing-mocks 983 pass, 0 fail (up from 979 — four new tests)
cargo test ... -- --ignored 12 pass, 0 fail
cargo clippy --all-targets --features testing-mocks -- -D warnings clean
cargo fmt --check clean
ast-grep scan clean

No false positives on real vaults: example_kb (46 files) clean, and Refractions (688 files) loads fine — its 2 violations are pre-existing NullNotAllowed on content, which a duplicate name could not produce since that would be a config load error.

Merge order

Red on the h2 advisory until #72 lands. After #71 merges this branch can pick up TODO-0196 and flip it to done.

edochi and others added 2 commits August 20, 2026 15:15
Two entries sharing a `name` were accepted silently and the last one won.
Downstream lookup keys on the bare name — `cmd/check/validate.rs` collects
into a `HashMap<&str, _>`, as does `FieldValidators::build` — so `collect()`
kept the final entry and discarded the earlier one without a word.

The failure was actively misleading rather than merely lossy. Declaring
`status` as String under `blog/**` and Integer under `projects/**` produced
two violations against `blog/post.md`, both artifacts of the collapse:

    status │ Wrong type  │ type Integer               │ blog/post.md (got String)
    status │ Not allowed │ allowed in ["projects/**"] │ blog/post.md

Adds invariant 10: no two entries may share a name. Rejected on the name
alone, regardless of whether the entries agree on type — scoping a field
per directory is a separate feature, not something a repeated entry should
back into. Checked before the per-field loop so the structural problem is
reported ahead of any per-field complaint.

Names are matched exactly. YAML keys are case-sensitive, so `status` and
`Status` are genuinely different frontmatter fields and both stay legal;
invariant 7 already canonicalises dotted names, so no normalisation
ambiguity remains.

Also documents invariant 9 (`Array(Object)` not representable on disk),
which was enforced in the body but missing from the docstring — the
contract promised eight rules while the code enforced nine.

Refs TODO-0196.

Co-Authored-By: Claude <noreply@anthropic.com>
@edochi
edochi merged commit 721ac38 into main Aug 20, 2026
8 checks passed
@edochi
edochi deleted the fix/duplicate-field-names branch August 20, 2026 14:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant