fix(ingestion): decide quoting with YAML itself, not a colon heuristic (#90)
`title: "Foo" — bar` opens with a quote and is not quoted; the previous prefix check read it as already-quoted and left two notes broken. Probing the value as the right-hand side of a mapping answers the real question — does this load as written — and leaves anything that already parses alone, whatever it parses into. Repairs the last two quotable files in mathias/brain. Only wiki/index.md remains unparseable, for an unrelated reason: a truncated UTF-8 sequence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BeAp5LTscnz2W7Ubt4eJ6m
This commit is contained in:
@@ -101,21 +101,23 @@ func quoteScalars(block []byte) []byte {
|
|||||||
return []byte(strings.Join(lines, "\n"))
|
return []byte(strings.Join(lines, "\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// needsQuoting reports whether a scalar value must be quoted to parse as the
|
// needsQuoting reports whether a scalar value has to be quoted to load as the
|
||||||
// string it plainly is. Only the colon case is handled: it is the one this
|
// string it plainly is.
|
||||||
// package exists for, and every other special case risks rewriting a value
|
//
|
||||||
// that was deliberately structured (a nested mapping, an anchor, a number).
|
// The test is YAML's own parser rather than a pattern: ask whether the value
|
||||||
|
// stands alone as the right-hand side of a mapping. Heuristics get this wrong
|
||||||
|
// in both directions — `title: "Foo" — bar` opens with a quote but is not
|
||||||
|
// quoted, and a value may contain a colon inside quotes and be perfectly fine.
|
||||||
|
//
|
||||||
|
// Anything that already parses is left alone, whatever it parses into — a flow
|
||||||
|
// collection, a number, a bool. It is not broken, and quoting would change its
|
||||||
|
// type.
|
||||||
func needsQuoting(value string) bool {
|
func needsQuoting(value string) bool {
|
||||||
if strings.HasPrefix(value, "'") || strings.HasPrefix(value, `"`) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(value, "|") || strings.HasPrefix(value, ">") {
|
if strings.HasPrefix(value, "|") || strings.HasPrefix(value, ">") {
|
||||||
return false // block scalar; its content is not parsed as YAML
|
return false // block scalar; its content is not parsed as YAML
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(value, "{") || strings.HasPrefix(value, "[") {
|
var probe map[string]any
|
||||||
return false // flow collection; quoting would silently make it a string
|
return yaml.Unmarshal([]byte("v: "+value+"\n"), &probe) != nil
|
||||||
}
|
|
||||||
return strings.Contains(value, ": ") || strings.HasSuffix(value, ":")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// block returns the raw YAML between the opening and closing fences.
|
// block returns the raw YAML between the opening and closing fences.
|
||||||
|
|||||||
@@ -90,6 +90,17 @@ func TestFix(t *testing.T) {
|
|||||||
wantKey: "title",
|
wantKey: "title",
|
||||||
wantValue: "Don't do this: it breaks",
|
wantValue: "Don't do this: it breaks",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// The value opens with a double quote and then keeps going after
|
||||||
|
// the closing one, so YAML reads it as a quoted scalar followed by
|
||||||
|
// junk. Treating "starts with a quote" as "already quoted" leaves
|
||||||
|
// this broken forever.
|
||||||
|
name: "quotes a value that only looks quoted",
|
||||||
|
doc: "---\ntitle: \"Evaluation Driven Generation\" — a TDD-style scaffold\ntags: [a, b]\n---\n\nbody\n",
|
||||||
|
wantChanged: true,
|
||||||
|
wantKey: "title",
|
||||||
|
wantValue: "\"Evaluation Driven Generation\" — a TDD-style scaffold",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "leaves an already-valid document untouched",
|
name: "leaves an already-valid document untouched",
|
||||||
doc: "---\ntitle: 'Already quoted: fine'\nwing: homelab\n---\n\nbody\n",
|
doc: "---\ntitle: 'Already quoted: fine'\nwing: homelab\n---\n\nbody\n",
|
||||||
@@ -129,6 +140,25 @@ func TestFix(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFix_LeavesStructuredValuesStructured(t *testing.T) {
|
||||||
|
// Only the broken scalar may be touched. A flow sequence on a neighbouring
|
||||||
|
// line still has to load as a sequence afterwards, not as a string.
|
||||||
|
doc := "---\ntitle: Broken: here\ntags: [prompting, llm]\ncount: 3\n---\n\nbody\n"
|
||||||
|
|
||||||
|
got, changed, err := Fix([]byte(doc))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, changed)
|
||||||
|
|
||||||
|
b, ok := block(got)
|
||||||
|
require.True(t, ok)
|
||||||
|
var m map[string]any
|
||||||
|
require.NoError(t, yaml.Unmarshal(b, &m))
|
||||||
|
|
||||||
|
assert.Equal(t, "Broken: here", m["title"])
|
||||||
|
assert.Equal(t, []any{"prompting", "llm"}, m["tags"])
|
||||||
|
assert.Equal(t, 3, m["count"])
|
||||||
|
}
|
||||||
|
|
||||||
func TestFix_PreservesBodyExactly(t *testing.T) {
|
func TestFix_PreservesBodyExactly(t *testing.T) {
|
||||||
body := "\nThe body has: colons, 'quotes', and\n---\na horizontal rule.\n"
|
body := "\nThe body has: colons, 'quotes', and\n---\na horizontal rule.\n"
|
||||||
doc := "---\ntitle: Broken: title\n---" + body
|
doc := "---\ntitle: Broken: title\n---" + body
|
||||||
|
|||||||
Reference in New Issue
Block a user