fix(schema): reject duplicate [[fields.field]] names - #73
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements invariant 10. Refs TODO-0196 (arriving in #71).
The bug
Two
[[fields.field]]entries sharing anamewere accepted silently and the last one won. Downstream lookup keys on the bare name —cmd/check/validate.rs:43collects into aHashMap<&str, _>, as doesFieldValidators::build— socollect()kept the final entry and discarded the earlier one without a word.The failure was actively misleading, not merely lossy. Declaring
statusasStringunderblog/**andIntegerunderprojects/**produced two violations againstblog/post.md, both artifacts of the collapse rather than of the vault: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 (foovsfoo.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
statusandStatusare 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:
cargo test --features testing-mockscargo test ... -- --ignoredcargo clippy --all-targets --features testing-mocks -- -D warningscargo fmt --checkast-grep scanNo false positives on real vaults:
example_kb(46 files) clean, and Refractions (688 files) loads fine — its 2 violations are pre-existingNullNotAllowedon 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.