mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-26 22:37:03 +00:00
c1076ce15f
* fix: a relation's "no target" has one stored form, and required means it names one (BUG-3028) A scalar relation had three stored spellings of "no target" (key absent, "" and whitespace-only), and required was enforced only against the first, so a required relation holding "" validated. The canonical form is now the one multi_relation uses: the key absent. Provenance decides the rest, per the lead's ruling. A blank a write SETS (create, fields_patch, a changed value in full fields, a move or copy override) is removed before validation, so a required one is refused as required. A patch blanking a required relation now gets the answer deleting it always got. A legacy blank a write only CARRIES is removed after validation and never refused: an untouched key under fields_patch, an unchanged full fields value, move, bulk update and bulk move, cross-workspace copy and its preflight, artifact import. On the patch path the store does this under its write lock (ItemUpdate.BlankRelationKeys), so a concurrent write's new value is never mistaken for a blank. An empty list filter on a scalar relation of a collection-scoped list (owner=) now matches all three spellings instead of only "". MCP ToolSurfaceVersion 0.40, a behavior bump. No migration: this instance has no relation fields, and the prod census is Dave's. Claude-Session: https://claude.ai/code/session_01X4uYKs5ZPF28eCWfm8VPvE * fix(items): a blank relation default is no default, so it cannot satisfy required (BUG-3028, codex round 1) Codex round 1 P2: a required relation whose schema default is "" had the blank injected by validation, which satisfied required, and was then normalised away after it, storing a required relation with no target. A blank default on a scalar relation now takes the no-default path: a required field is refused (the message names the blank default) and an optional one stays absent with nothing reported dropped. Claude-Session: https://claude.ai/code/session_01X4uYKs5ZPF28eCWfm8VPvE
70 lines
2.3 KiB
Go
70 lines
2.3 KiB
Go
package items
|
|
|
|
import (
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/PerpetualSoftware/pad/internal/models"
|
|
)
|
|
|
|
// BUG-3028: a scalar `relation` field had three stored spellings of "no
|
|
// target" — the key absent, `""`, and whitespace-only — and `required` was
|
|
// enforced only against the first, so a required relation holding `""`
|
|
// validated. The canonical form is the one multi_relation already uses (U4):
|
|
// the key ABSENT. These helpers find and remove the other two spellings.
|
|
//
|
|
// Which blanks a door removes BEFORE validation and which AFTER is the whole
|
|
// of the provenance rule (the lead's ruling on BUG-3028): a blank the write
|
|
// itself SUPPLIES is removed first, so the ordinary required check refuses it;
|
|
// a legacy blank the write merely CARRIES is removed after, so it becomes
|
|
// absent without refusing a write that never touched the field.
|
|
|
|
// IsBlankRelationValue reports whether v is a string that trims to "" — one
|
|
// of the two non-canonical spellings of "no target".
|
|
func IsBlankRelationValue(v any) bool {
|
|
s, ok := v.(string)
|
|
return ok && strings.TrimSpace(s) == ""
|
|
}
|
|
|
|
// BlankRelationKeys returns, sorted, the keys of fields that the schema
|
|
// declares as a scalar `relation` and whose value is blank.
|
|
func BlankRelationKeys(fields map[string]any, schema models.CollectionSchema) []string {
|
|
var keys []string
|
|
for _, def := range schema.Fields {
|
|
if def.Type != "relation" {
|
|
continue
|
|
}
|
|
if v, ok := fields[def.Key]; ok && IsBlankRelationValue(v) {
|
|
keys = append(keys, def.Key)
|
|
}
|
|
}
|
|
sort.Strings(keys)
|
|
return keys
|
|
}
|
|
|
|
// DropBlankRelations deletes the blank scalar relation keys for which keep is
|
|
// false (every one when keep is nil) and returns the keys it deleted.
|
|
func DropBlankRelations(fields map[string]any, schema models.CollectionSchema, keep func(key string) bool) []string {
|
|
var dropped []string
|
|
for _, k := range BlankRelationKeys(fields, schema) {
|
|
if keep != nil && keep(k) {
|
|
continue
|
|
}
|
|
delete(fields, k)
|
|
dropped = append(dropped, k)
|
|
}
|
|
return dropped
|
|
}
|
|
|
|
// ScalarRelationKeys returns the keys the schema declares as scalar
|
|
// `relation`, in schema order.
|
|
func ScalarRelationKeys(schema models.CollectionSchema) []string {
|
|
var keys []string
|
|
for _, def := range schema.Fields {
|
|
if def.Type == "relation" {
|
|
keys = append(keys, def.Key)
|
|
}
|
|
}
|
|
return keys
|
|
}
|