Summary
bd --set-metadata key=value infers the JSON type of value from its string content. Numeric-looking strings like "1" are stored as JSON integers (not strings), and that asymmetry breaks consumers that unmarshal metadata into map[string]string and then do string equality on the values. The bd CLI's own --metadata-field filter happens to normalize input for comparison, so the round trip looks fine from the CLI; the breakage only surfaces when a Go program (internal/beads.ListQuery.Matches style) compares the stored value against a string literal.
Reproduction
bd 1.0.4 (56477c26e), dolt-backed store:
$ bd init
$ ID=$(bd create --title "probe" --type task --json | jq -r .id)
$ bd update "$ID" --set-metadata "probe=1"
$ bd update "$ID" --set-metadata "probe2=order"
# Storage type — what dolt actually holds
$ bd sql "SELECT id, JSON_TYPE(JSON_EXTRACT(metadata, '\$.probe')) AS t1,
JSON_TYPE(JSON_EXTRACT(metadata, '\$.probe2')) AS t2
FROM issues WHERE id='$ID'"
+----+---------+--------+
| id | t1 | t2 |
+----+---------+--------+
| .. | INTEGER | STRING |
+----+---------+--------+
# JSON round-trip — what Go consumers see
$ bd list --metadata-field "probe=1" --json | jq '.[0].metadata'
{
"probe": 1, ← JSON number, not "1"
"probe2": "order" ← JSON string, as expected
}
Why it matters
internal/beads.Bead.Metadata is typed map[string]string (internal/beads/beads.go). Consumers that build a ListQuery{Metadata: map[string]string{"probe": "1"}} and rely on the cache's per-key equality match (internal/beads/caching_store_reads.go::matchesMetadata — b.Metadata[k] != v string equality) get a silent zero result because the stored value never round-trips back as the string "1". There is no error, no warning — the query just always misses.
This bit gascity in gastownhall/gascity#2556 (a metadata-flag-based pool-demand signal). The first iteration of that PR stored gc.pool_demand="1", the supervisor queried for Metadata{"gc.pool_demand": "1"}, and the count silently stayed at zero across every reconcile tick despite the wisp being created correctly. The workaround there was to pick a non-numeric sentinel ("order") and pin it with a regression test, but that's a workaround per-consumer, not a fix in bd.
The bd CLI's own filter (bd list --metadata-field probe=1) happens to work — it must be normalizing input or comparing on the JSON-typed value rather than the round-tripped string — so the asymmetry is invisible from the shell. Only Go-level callers hit it.
What I'd want from the fix
One of these:
- Preserve write-time type —
--set-metadata key=value always stores value as a JSON string. Authors who want a JSON number can use --set-metadata-json or similar. Removes the surprise but is a behavior change.
- Round-trip stability —
bd list --json normalizes all metadata values to strings on the way out, so map[string]string consumers see what they wrote. Behavior change in the JSON output shape.
- Document the cast — keep the current behavior, add a paragraph in the bd CLI docs + a
// NOTE on Bead.Metadata warning that numeric-looking values won't round-trip stably for map[string]string consumers. Cheapest fix, just documents the trap so the next person doesn't lose a day to it.
Happy to file a PR for any of the above once direction is picked. Even option 3 (the doc-only fix) would have saved me roughly half a debug session.
Adjacent
The internal bdstore.go comment at line ~382 mentions bd CLI type inference for strings like "true" and "42" — so the behavior is known at the read boundary, just not warned about at the write boundary or in the public type. Could lift that comment up to the Bead.Metadata field doc as the cheapest version of option 3.
Summary
bd --set-metadata key=valueinfers the JSON type ofvaluefrom its string content. Numeric-looking strings like"1"are stored as JSON integers (not strings), and that asymmetry breaks consumers that unmarshalmetadataintomap[string]stringand then do string equality on the values. The bd CLI's own--metadata-fieldfilter happens to normalize input for comparison, so the round trip looks fine from the CLI; the breakage only surfaces when a Go program (internal/beads.ListQuery.Matchesstyle) compares the stored value against a string literal.Reproduction
bd 1.0.4 (
56477c26e), dolt-backed store:Why it matters
internal/beads.Bead.Metadatais typedmap[string]string(internal/beads/beads.go). Consumers that build aListQuery{Metadata: map[string]string{"probe": "1"}}and rely on the cache's per-key equality match (internal/beads/caching_store_reads.go::matchesMetadata—b.Metadata[k] != vstring equality) get a silent zero result because the stored value never round-trips back as the string"1". There is no error, no warning — the query just always misses.This bit gascity in gastownhall/gascity#2556 (a metadata-flag-based pool-demand signal). The first iteration of that PR stored
gc.pool_demand="1", the supervisor queried forMetadata{"gc.pool_demand": "1"}, and the count silently stayed at zero across every reconcile tick despite the wisp being created correctly. The workaround there was to pick a non-numeric sentinel ("order") and pin it with a regression test, but that's a workaround per-consumer, not a fix in bd.The bd CLI's own filter (
bd list --metadata-field probe=1) happens to work — it must be normalizing input or comparing on the JSON-typed value rather than the round-tripped string — so the asymmetry is invisible from the shell. Only Go-level callers hit it.What I'd want from the fix
One of these:
--set-metadata key=valuealways storesvalueas a JSON string. Authors who want a JSON number can use--set-metadata-jsonor similar. Removes the surprise but is a behavior change.bd list --jsonnormalizes all metadata values to strings on the way out, somap[string]stringconsumers see what they wrote. Behavior change in the JSON output shape.// NOTEonBead.Metadatawarning that numeric-looking values won't round-trip stably formap[string]stringconsumers. Cheapest fix, just documents the trap so the next person doesn't lose a day to it.Happy to file a PR for any of the above once direction is picked. Even option 3 (the doc-only fix) would have saved me roughly half a debug session.
Adjacent
The internal
bdstore.gocomment at line ~382 mentionsbdCLI type inference for strings like"true"and"42"— so the behavior is known at the read boundary, just not warned about at the write boundary or in the public type. Could lift that comment up to theBead.Metadatafield doc as the cheapest version of option 3.