Replacing a scalar that carries a YAML anchor drops the anchor and leaves every alias to it dangling, so Dump returns nil and writes a file that Load then refuses.
Found on feat/188-durable-opt-in while probing which of driver/yaml's error branches are reachable through the public seam.
It reproduces identically on main at a3dc8f8; nothing in #187 or #188 touched it.
Reproduce
host: &h localhost
other: *h
type config struct {
Host string `ferry:"host"`
}
err := ferry.Dump(ctx, config{Host: "example"}, yaml.NewSink(path))
// err == nil
The file afterwards:
And loading it back:
ferry: opening the plane: invalid value: the plane is not a YAML document: yaml: unknown anchor 'h' referenced
Where it comes from
writer.Set replaces the node wholesale:
spelled.HeadComment, spelled.LineComment, spelled.FootComment = at.HeadComment, at.LineComment, at.FootComment
*at = *spelled
Three comment fields are carried across because they are the operator's.
Anchor is not, and it is the operator's too.
spell builds a fresh leaf with an empty Anchor, so the assignment clears it, while other's AliasNode still points at the same *yamlv3.Node and still emits *h.
Set is right that the value, the tag and the style are ferry's.
An anchor is neither: it is the operator's name for a place in their document, exactly like a comment.
Why this is worse than the tag case in #155
#155 is a mapped scalar losing its tag, which is a value that reads back as the wrong kind.
This is a whole document that no longer parses, so it is not a lossy round trip but a save that destroys the file for every reader including ferry.
driver/yaml's entire argument is that a hand-maintained config file survives being loaded and written back, and an anchor is ordinary in a hand-maintained file.
It is also not in doc.go's exception list, which names five things a Dump does not preserve.
None of them is "the file stops being valid YAML".
What needs deciding
- Carry the anchor across, beside the three comments, so
host: &h example is written and the alias still resolves.
This is the small fix and it makes the common case correct.
- What happens to an alias that ferry cannot keep valid, if such a case exists once (1) is done.
Worth checking whether a mapped node that is itself an AliasNode behaves, since replacing one turns an alias into an inline value and silently unshares it from its anchor.
- Whether either outcome belongs in
doc.go's exception list.
Scope
Not #187's and not #188's.
Both were durability tickets and neither goes near Set.
The probes that found this were throwaway and are deleted.
Replacing a scalar that carries a YAML anchor drops the anchor and leaves every alias to it dangling, so
Dumpreturnsniland writes a file thatLoadthen refuses.Found on
feat/188-durable-opt-inwhile probing which ofdriver/yaml's error branches are reachable through the public seam.It reproduces identically on
mainata3dc8f8; nothing in #187 or #188 touched it.Reproduce
The file afterwards:
And loading it back:
Where it comes from
writer.Setreplaces the node wholesale:Three comment fields are carried across because they are the operator's.
Anchoris not, and it is the operator's too.spellbuilds a freshleafwith an emptyAnchor, so the assignment clears it, whileother'sAliasNodestill points at the same*yamlv3.Nodeand still emits*h.Setis right that the value, the tag and the style are ferry's.An anchor is neither: it is the operator's name for a place in their document, exactly like a comment.
Why this is worse than the tag case in #155
#155 is a mapped scalar losing its tag, which is a value that reads back as the wrong kind.
This is a whole document that no longer parses, so it is not a lossy round trip but a save that destroys the file for every reader including ferry.
driver/yaml's entire argument is that a hand-maintained config file survives being loaded and written back, and an anchor is ordinary in a hand-maintained file.It is also not in
doc.go's exception list, which names five things aDumpdoes not preserve.None of them is "the file stops being valid YAML".
What needs deciding
host: &h exampleis written and the alias still resolves.This is the small fix and it makes the common case correct.
Worth checking whether a mapped node that is itself an
AliasNodebehaves, since replacing one turns an alias into an inline value and silently unshares it from its anchor.doc.go's exception list.Scope
Not #187's and not #188's.
Both were durability tickets and neither goes near
Set.The probes that found this were throwaway and are deleted.