Engine — duplicate keys last-wins, inline-comment re-splice

Duplicates: Yams' duplicate-key error is disarmed by placeholder-
renaming first occurrences and re-composing (line count preserved, so
span layout stays exact); reads take the last occurrence, set()
rewrites the winner and clears earlier twins, remove() clears all.
Top-level only — nested duplicates stay YAML errors (doc updated).
Re-splice: an inline comment on a rewritten scalar line survives, with
quote-aware detection so a # inside quotes is never a comment. +31
tests (118 total).

Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
This commit is contained in:
2026-07-26 16:07:06 -04:00
parent 31f7691062
commit 4085dd7a46
3 changed files with 498 additions and 31 deletions
+278 -2
View File
@@ -69,6 +69,19 @@ private enum Fixture {
---
body
"""
/// A hand-edited file that says `title` twice strict YAML would reject it; the editor reads
/// the last one and keeps both on disk (01-storage-format.md § Frontmatter).
static let duplicated = """
---
schema: 1
title: First
order: 1024
title: Second
---
body
"""
}
private func parseError(_ text: String) -> FrontmatterError? {
@@ -122,6 +135,11 @@ struct FrontmatterRoundTripTests {
"---\nschema: 1\n---\nbody without newline",
// CRLF throughout
"---\r\nschema: 1\r\n---\r\nbody\r\n",
// duplicate top-level keys every occurrence keeps its own span
Fixture.duplicated,
"---\ntitle: a\ntitle: b\ntitle: c\n---\nbody\n",
// a duplicate whose earlier twin owns a comment and a block scalar
"---\nnotes: |\n one\n# about notes\nnotes: two\n---\nbody\n",
])
func serializingAnUntouchedDocumentIsByteIdentical(text: String) throws {
let document = try FrontmatterDocument.parse(text)
@@ -165,10 +183,13 @@ struct FrontmatterMalformedTests {
"---\nfoo: [1, 2\n---\nbody\n",
"---\nschema: 1\n\tindented-with-tab: 2\n---\nbody\n",
"---\nschema: 1\n bad: indent\n---\nbody\n",
"---\ndupe: 1\ndupe: 2\n---\nbody\n",
"---\n\"unterminated: 1\n---\nbody\n",
"---\nfoo: {a: 1\n---\nbody\n",
"---\n*undefined-alias\n---\nbody\n",
// last-wins rescues duplicates at the top level only nested ones stay a hard failure
"---\nouter:\n a: 1\n a: 2\n---\nbody\n",
"---\nflow: {a: 1, a: 2}\n---\nbody\n",
"---\ndupe: 1\ndupe: 2\nouter:\n a: 1\n a: 2\n---\nbody\n",
])
func unparseableYAML(text: String) {
guard case .unparseableYAML = parseError(text) else {
@@ -215,6 +236,155 @@ struct FrontmatterMalformedTests {
#expect(parseError(text) == nil)
}
}
/// Strict YAML rejects a repeated mapping key; the editor deliberately does not.
@Test func duplicateTopLevelKeysAreNotAParseError() throws {
for text in [
Fixture.duplicated,
"---\ndupe: 1\ndupe: 2\n---\nbody\n",
"---\ntitle: a\ntitle: b\ntitle: c\n---\nbody\n",
] {
#expect(parseError(text) == nil)
}
}
}
// MARK: - Duplicate keys (last one wins)
/// 01-storage-format.md § Frontmatter: a key written twice reads as its last occurrence, the
/// earlier ones preserved verbatim on disk and invisible; an app write of that key rewrites the
/// winner and removes the twins; a removal removes them all.
struct FrontmatterDuplicateKeyTests {
@Test func theLastOccurrenceIsTheOneThatReads() throws {
let document = try FrontmatterDocument.parse(Fixture.duplicated)
#expect(document.title == .valid("Second"))
#expect(document.value(for: "title") == .string("Second"))
#expect(document.rawValue(for: "title") == "Second")
#expect(document.contains("title"))
}
@Test func threeOccurrencesStillReadAsTheLast() throws {
let document = try FrontmatterDocument.parse("---\ntitle: a\ntitle: b\ntitle: c\n---\nbody\n")
#expect(document.title == .valid("c"))
#expect(document.keys == ["title"])
}
/// The effective view has one entry per key, sitting where the winner sits the order the
/// file itself takes once `set` collapses the twins.
@Test func iterationPresentsTheEffectiveView() throws {
let document = try FrontmatterDocument.parse(Fixture.duplicated)
#expect(document.keys == ["schema", "order", "title"])
#expect(document.fields.map(\.key) == ["schema", "order", "title"])
#expect(document.fields.last?.rawValue == "Second")
}
@Test func bothOccurrencesSurviveAnUntouchedRoundTrip() throws {
let document = try FrontmatterDocument.parse(Fixture.duplicated)
let output = document.serialized()
#expect(output == Fixture.duplicated)
#expect(output.contains("title: First"))
#expect(output.contains("title: Second"))
}
@Test func settingRewritesTheWinnerAndDeletesTheTwins() throws {
var document = try FrontmatterDocument.parse(Fixture.duplicated)
document.set("title", to: .string("Third"))
#expect(document.serialized() == "---\nschema: 1\norder: 1024\ntitle: Third\n---\nbody\n")
#expect(document.title == .valid("Third"))
}
@Test func settingCollapsesThreeOccurrencesToOne() throws {
var document = try FrontmatterDocument.parse("---\ntitle: a\ntitle: b\ntitle: c\n---\nbody\n")
document.set("title", to: .string("d"))
#expect(document.serialized() == "---\ntitle: d\n---\nbody\n")
}
/// A stale twin left behind would resurrect itself the moment the winner were removed.
@Test func removingTakesEveryOccurrence() throws {
var document = try FrontmatterDocument.parse(Fixture.duplicated)
document.remove("title")
#expect(document.serialized() == "---\nschema: 1\norder: 1024\n---\nbody\n")
#expect(document.title == .missing)
#expect(!document.contains("title"))
#expect(document.keys == ["schema", "order"])
}
@Test func removingTakesAllThreeOccurrences() throws {
var document = try FrontmatterDocument.parse("---\nschema: 1\ntitle: a\ntitle: b\ntitle: c\n---\nbody\n")
document.remove("title")
#expect(document.serialized() == "---\nschema: 1\n---\nbody\n")
}
/// Own-line comments outlive the twin they were written above (01-storage-format.md).
@Test func aCommentAboveTheFirstOccurrenceSurvivesTheCollapse() throws {
let text = "---\n# the original title\ntitle: First\ntitle: Second\n---\nbody\n"
var document = try FrontmatterDocument.parse(text)
#expect(document.serialized() == text)
document.set("title", to: .string("Third"))
#expect(document.serialized() == "---\n# the original title\ntitle: Third\n---\nbody\n")
}
/// Same rule when the comment trails the earlier twin rather than heading it.
@Test func aCommentBelowTheFirstOccurrenceSurvivesTheCollapse() throws {
var document = try FrontmatterDocument.parse("---\ntitle: First\n# still relevant\ntitle: Second\n---\nbody\n")
document.remove("title")
#expect(document.serialized() == "---\n# still relevant\n---\nbody\n")
}
@Test func twoDifferentKeysMayEachBeDuplicated() throws {
let text = "---\na: 1\nb: 1\na: 2\nb: 2\n---\nbody\n"
var document = try FrontmatterDocument.parse(text)
#expect(document.serialized() == text)
#expect(document.value(for: "a") == .int(2))
#expect(document.value(for: "b") == .int(2))
#expect(document.keys == ["a", "b"])
document.set("a", to: .int(3))
#expect(document.serialized() == "---\nb: 1\na: 3\nb: 2\n---\nbody\n")
}
/// A duplicated twin whose value is a block scalar still spans its own lines.
@Test func aDuplicateWithAMultiLineTwinCollapsesCleanly() throws {
let text = "---\nnotes: |\n one\n# about notes\nnotes: two\n---\nbody\n"
var document = try FrontmatterDocument.parse(text)
#expect(document.serialized() == text)
#expect(document.value(for: "notes") == .string("two"))
document.set("notes", to: .string("three"))
#expect(document.serialized() == "---\n# about notes\nnotes: three\n---\nbody\n")
}
/// The twins need not be spelled alike YAML reads `"title"` and `title` as the same key,
/// and each occurrence is rewritten (or dropped) in its own spelling.
@Test func twinsSpelledDifferentlyAreStillTheSameKey() throws {
let text = "---\n\"title\": First\ntitle: Second\n---\nbody\n"
var document = try FrontmatterDocument.parse(text)
#expect(document.serialized() == text)
#expect(document.title == .valid("Second"))
document.set("title", to: .string("Third"))
#expect(document.serialized() == "---\ntitle: Third\n---\nbody\n")
}
@Test func duplicatesInACRLFFileRoundTripAndCollapse() throws {
let text = "---\r\ntitle: First\r\norder: 1\r\ntitle: Second\r\n---\r\nbody\r\n"
var document = try FrontmatterDocument.parse(text)
#expect(document.serialized() == text)
#expect(document.title == .valid("Second"))
document.set("title", to: .string("Third"))
#expect(document.serialized() == "---\r\norder: 1\r\ntitle: Third\r\n---\r\nbody\r\n")
}
@Test func duplicatesSurviveReparsing() throws {
var document = try FrontmatterDocument.parse(Fixture.duplicated)
document.set("order", to: .double(2048))
let reparsed = try FrontmatterDocument.parse(document.serialized())
#expect(reparsed.serialized() == document.serialized())
#expect(reparsed.title == .valid("Second"))
#expect(reparsed.order == .valid(2048))
}
}
// MARK: - Generic access
@@ -436,7 +606,7 @@ struct FrontmatterEditTests {
var document = try FrontmatterDocument.parse(text)
document.set("project", to: .string("kanban"))
#expect(document.serialized()
== "---\nschema: 1\nproject: kanban\n\n# a note\norder: 5\n---\nbody\n")
== "---\nschema: 1\nproject: kanban # agent overlay\n\n# a note\norder: 5\n---\nbody\n")
}
@Test func appendingANewKeyGoesBeforeTheClosingDelimiter() throws {
@@ -538,6 +708,112 @@ struct FrontmatterEditTests {
}
}
// MARK: - Inline comments on rewritten lines
/// 01-storage-format.md § Frontmatter, "App rewrites preserve comments": an inline comment on a
/// rewritten value line is re-spliced after the new value best-effort, guaranteed for plain
/// single-line scalars, allowed to drop in pathological shapes.
struct FrontmatterInlineCommentTests {
private func edited(_ frontmatter: String, _ key: String, _ value: FrontmatterValue) throws -> String {
var document = try FrontmatterDocument.parse("---\n\(frontmatter)\n---\nbody\n")
document.set(key, to: value)
return String(document.serialized().dropFirst("---\n".count).dropLast("\n---\nbody\n".count))
}
@Test func aPlainScalarKeepsItsCommentAndItsSpacing() throws {
#expect(try edited("order: 3072 # keep at top", "order", .double(4096))
== "order: 4096 # keep at top")
#expect(try edited("order: 3072 # tight", "order", .double(4096)) == "order: 4096 # tight")
#expect(try edited("order: 3072\t# tabbed", "order", .double(4096)) == "order: 4096\t# tabbed")
}
@Test func aValueLineWithoutACommentIsRewrittenAsBefore() throws {
#expect(try edited("order: 3072", "order", .double(4096)) == "order: 4096")
#expect(try edited("title: My Board", "title", .string("Renamed")) == "title: Renamed")
}
/// A `#` inside a quoted scalar is value text, not a comment it must not be re-spliced.
@Test func aHashInsideAQuotedScalarIsNotAComment() throws {
#expect(try edited("title: \"hash # inside\"", "title", .string("Renamed")) == "title: Renamed")
#expect(try edited("title: 'hash # inside'", "title", .string("Renamed")) == "title: Renamed")
#expect(try edited("background: \"#ff8800\"", "background", .string("slate")) == "background: slate")
}
/// but a real comment *after* such a value still is one.
@Test func aCommentAfterAQuotedScalarIsStillSpliced() throws {
#expect(try edited("title: \"hash # inside\" # real note", "title", .string("Renamed"))
== "title: Renamed # real note")
#expect(try edited("title: 'it''s # fine' # real note", "title", .string("Renamed"))
== "title: Renamed # real note")
}
/// An apostrophe mid-scalar does not open a quoted scalar, so the comment after it is found.
@Test func aPlainScalarWithAnApostropheKeepsItsComment() throws {
#expect(try edited("title: it's fine # note", "title", .string("Renamed"))
== "title: Renamed # note")
#expect(try edited("title: say \"hi\" # note", "title", .string("Renamed"))
== "title: Renamed # note")
}
@Test func aSingleLineFlowCollectionKeepsItsComment() throws {
#expect(try edited("labels: [a, \"b # c\"] # note", "labels", .raw("[x]"))
== "labels: [x] # note")
#expect(try edited("template: {order: 3} # picker slot", "template", .raw("{order: 7}"))
== "template: {order: 7} # picker slot")
}
@Test func aCommentOnAnEmptyValueIsKept() throws {
#expect(try edited("title: # to be filled in", "title", .string("Named"))
== "title: Named # to be filled in")
}
@Test func theCommentSurvivesSuccessiveSets() throws {
var document = try FrontmatterDocument.parse("---\norder: 3072 # keep at top\n---\nbody\n")
document.set("order", to: .double(4096))
document.set("order", to: .double(8192))
#expect(document.serialized() == "---\norder: 8192 # keep at top\n---\nbody\n")
}
@Test func theCommentSurvivesAReparse() throws {
var document = try FrontmatterDocument.parse("---\norder: 3072 # keep at top\n---\nbody\n")
document.set("order", to: .double(4096))
let reparsed = try FrontmatterDocument.parse(document.serialized())
#expect(reparsed.order == .valid(4096))
#expect(reparsed.serialized() == document.serialized())
}
/// The winning occurrence's own comment is what carries over; a collapsed twin's inline
/// comment goes with the twin.
@Test func theWinningOccurrencesCommentIsTheOneKept() throws {
var document = try FrontmatterDocument.parse("---\norder: 1 # old\norder: 2 # current\n---\nbody\n")
document.set("order", to: .double(3))
#expect(document.serialized() == "---\norder: 3 # current\n---\nbody\n")
}
/// Pathological shapes are allowed to lose the comment rather than have the editor guess.
/// Pinning the chosen behavior: a value spanning several lines is not scanned at all.
@Test func aMultiLineValueDropsItsHeaderComment() throws {
var document = try FrontmatterDocument.parse("---\nnotes: | # about the notes\n line one\n---\nbody\n")
document.set("notes", to: .string("flattened"))
#expect(document.serialized() == "---\nnotes: flattened\n---\nbody\n")
var flow = try FrontmatterDocument.parse("---\nflow: {a: 1,\nb: 2} # spread out\nlast: x\n---\nbody\n")
flow.set("flow", to: .raw("{a: 9}"))
#expect(flow.serialized() == "---\nflow: {a: 9}\nlast: x\n---\nbody\n")
}
/// Comments on their own lines were always preserved and still are including the one that
/// heads the frontmatter and the one that trails it.
@Test func ownLineCommentsAreUntouchedByARewrite() throws {
var document = try FrontmatterDocument.parse(Fixture.rich)
document.set("title", to: .string("Renamed"))
let output = document.serialized()
#expect(output.hasPrefix("---\n# board settings, hand-written\nschema: 1\n"))
#expect(output.contains("\n# trailing note\n---\n"))
#expect(output.contains("project: lanework # agent overlay\n"))
}
}
// MARK: - Scalar emission
struct FrontmatterEmissionTests {