Skip to content

Commit 81da8b6

Browse files
author
gluck
committed
form: structural patches, and the four laws as property tests
The patch algebra Gluck specified, built alongside the flat one so the laws can be proven before anything is ripped out. Named StructPatch only because form.Patch is an alias to the flat message.Patch today. A patch describes itself -- the reader dispatches on which shape is populated rather than consulting a schema, because figaro's form is open and no declared type exists to generate a shape from: {Before, After} a scalar changed {Set, Delete, Update} an object gained, lost or altered keys {Create, Delete, Update, Order} a keyed list changed Every operation carries what it destroyed -- Delete holds its value, a scalar change holds Before -- so Inverse is a mechanical swap rather than something reconstructed by replaying history. Lists are addressed by KEY, never index. Two writers inserting "at position 3" are not touching the same thing, and position is Order's separate job: a partial window of the keys that moved plus one neighbour either side, so an append costs two entries rather than the list. ## WHAT THE PROPERTY TESTS FOUND, WHICH FIXTURES WOULD NOT HAVE Random values, random edits, 8,000 trials per law. Every one of these was a real defect in the first draft: - Inverse ignored Order, so a pure reorder did not undo. Order now carries Prior: NOT Declared reversed, because a window describes the sequence after creates and deletes land, and the inverse creates what this deleted. Both directions are computed at the diff, where both orders are known. - Merge returned the later patch when kinds disagreed, which cannot apply (an object patch against a number). Invertibility is the bridge: where P replaced the whole value, Q runs against P.After; where Q did, P.Inverse carries Q.Before back to the original. - Merge sorted creates alphabetically. Apply appends them in array order, so the array IS positional information and sorting it silently reordered the list a merged patch produced. - Merge collapsed delete-then-recreate into an update. In an ordered list the item leaves its seat and is appended, so the pair is not an update. Objects have no position and do collapse. ## WHAT DOES NOT HOLD YET, STATED RATHER THAN NARROWED AWAY apply(diff(A,B), A) = B PASS 8,000 trials diff(A,A) = Identity PASS 8,000 trials inverse(diff(A,B)) = diff(B,A) PASS 8,000 trials patch survives JSON PASS 8,000 trials apply(merge(P,Q),A) = apply(Q,apply(P,A)) FAILS for ordered lists Order windows are relative to the sequence after creates and deletes, so composing two of them needs the base, which Merge by design does not have. Scalars and objects compose. A list whose membership AND order both change does not, at roughly 1 random pair in 1,000. The test is skipped with the reason, not narrowed until green. Two ways out, both design decisions: give Merge the base (MergeAgainst(p,q,base), trivially correct by re-diff), or make Order absolute rather than a window -- which composes cleanly and costs the whole key list on every reorder. Nothing is wired in and nothing is removed. 34 non-test files and 57 write sites still use the flat patch.
1 parent 0206e8e commit 81da8b6

3 files changed

Lines changed: 1031 additions & 0 deletions

File tree

‎api/form/structdiff.go‎

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
package form
2+
3+
import (
4+
"encoding/json"
5+
)
6+
7+
// Diff is the patch that transforms a into b: apply(diff(A,B), A) = B.
8+
//
9+
// It classifies both values and produces the patch shape that matches. Two
10+
// objects diff structurally, key by key; two keyed lists diff by key with an
11+
// Order window over what moved; anything else is a scalar swap, which is also
12+
// the honest answer when the KIND changes (an object that became a string did
13+
// not have its keys edited).
14+
func Diff(a, b Value) StructPatch {
15+
if a.Equal(b) {
16+
return StructPatch{}
17+
}
18+
if ao, ok := asObject(a); ok {
19+
if bo, ok2 := asObject(b); ok2 {
20+
return diffObject(ao, bo)
21+
}
22+
}
23+
if al, ok := asList(a); ok {
24+
if bl, ok2 := asList(b); ok2 {
25+
return diffList(al, bl)
26+
}
27+
}
28+
return StructPatch{Scalar: &ScalarPatch{Before: a, After: b}}
29+
}
30+
31+
func diffObject(a, b map[string]Value) StructPatch {
32+
out := &ObjectPatch{}
33+
for k, bv := range b {
34+
av, had := a[k]
35+
switch {
36+
case !had:
37+
if out.Set == nil {
38+
out.Set = map[string]Value{}
39+
}
40+
out.Set[k] = bv
41+
case !av.Equal(bv):
42+
child := Diff(av, bv)
43+
if !child.IsIdentity() {
44+
if out.Update == nil {
45+
out.Update = map[string]StructPatch{}
46+
}
47+
out.Update[k] = child
48+
}
49+
}
50+
}
51+
for k, av := range a {
52+
if _, still := b[k]; !still {
53+
if out.Delete == nil {
54+
out.Delete = map[string]Value{}
55+
}
56+
out.Delete[k] = av
57+
}
58+
}
59+
return StructPatch{Object: out}
60+
}
61+
62+
func diffList(a, b []KeyedValue) StructPatch {
63+
out := &ListPatch{}
64+
ai := make(map[string]Value, len(a))
65+
for _, it := range a {
66+
ai[it.Key] = it.Value
67+
}
68+
bi := make(map[string]Value, len(b))
69+
for _, it := range b {
70+
bi[it.Key] = it.Value
71+
}
72+
73+
for _, it := range b {
74+
av, had := ai[it.Key]
75+
switch {
76+
case !had:
77+
out.Create = append(out.Create, it)
78+
case !av.Equal(it.Value):
79+
child := Diff(av, it.Value)
80+
if !child.IsIdentity() {
81+
if out.Update == nil {
82+
out.Update = map[string]StructPatch{}
83+
}
84+
out.Update[it.Key] = child
85+
}
86+
}
87+
}
88+
for _, it := range a {
89+
if _, still := bi[it.Key]; !still {
90+
out.Delete = append(out.Delete, it)
91+
}
92+
}
93+
// The window is computed over the sequence AS APPLY WILL SEE IT: after
94+
// creates are appended and deletes removed. Both directions are computed
95+
// here because only here are both orders in hand.
96+
fwdFrom := intermediate(a, bi, out.Create)
97+
fwdTo := keysOf(b)
98+
revFrom := intermediate(b, ai, out.Delete)
99+
revTo := keysOf(a)
100+
101+
fwd := windowFor(fwdFrom, fwdTo)
102+
rev := windowFor(revFrom, revTo)
103+
if fwd != nil || rev != nil {
104+
o := &Order{}
105+
if fwd != nil {
106+
o.Declared, o.Moved = fwd.Declared, fwd.Moved
107+
}
108+
if rev != nil {
109+
o.Prior, o.PriorMoved = rev.Declared, rev.Moved
110+
}
111+
out.Order = o
112+
}
113+
return StructPatch{List: out}
114+
}
115+
116+
// intermediate is the sequence apply produces before Order runs: the survivors
117+
// in their original order, then the created keys appended.
118+
func intermediate(from []KeyedValue, survives map[string]Value, created []KeyedValue) []string {
119+
var out []string
120+
for _, it := range from {
121+
if _, ok := survives[it.Key]; ok {
122+
out = append(out, it.Key)
123+
}
124+
}
125+
for _, c := range created {
126+
out = append(out, c.Key)
127+
}
128+
return out
129+
}
130+
131+
func keysOf(items []KeyedValue) []string {
132+
out := make([]string, 0, len(items))
133+
for _, it := range items {
134+
out = append(out, it.Key)
135+
}
136+
return out
137+
}
138+
139+
// windowFor is the PARTIAL WINDOW transforming from into to: the positions
140+
// where they disagree, widened by one neighbour either side as an anchor.
141+
//
142+
// The scan is linear, so moving one item a long way marks every key it passed
143+
// as moved -- those keys really are in different positions relative to the
144+
// survivors, and a scan cannot know which single move explains it. A known and
145+
// accepted approximation.
146+
func windowFor(from, to []string) *Order {
147+
if len(from) != len(to) {
148+
return nil
149+
}
150+
movedAt := map[int]bool{}
151+
for i := range to {
152+
if from[i] != to[i] {
153+
movedAt[i] = true
154+
}
155+
}
156+
if len(movedAt) == 0 {
157+
return nil
158+
}
159+
keep := map[int]bool{}
160+
for i := range movedAt {
161+
for _, j := range []int{i - 1, i, i + 1} {
162+
if j >= 0 && j < len(to) {
163+
keep[j] = true
164+
}
165+
}
166+
}
167+
o := &Order{}
168+
for i := 0; i < len(to); i++ {
169+
if !keep[i] {
170+
continue
171+
}
172+
if movedAt[i] {
173+
o.Moved = append(o.Moved, len(o.Declared))
174+
}
175+
o.Declared = append(o.Declared, to[i])
176+
}
177+
return o
178+
}
179+
180+
// asObject reports the value as a JSON object, and false for anything else.
181+
func asObject(v Value) (map[string]Value, bool) {
182+
raw := v.Raw()
183+
if len(raw) == 0 || firstToken(raw) != '{' {
184+
return nil, false
185+
}
186+
var m map[string]json.RawMessage
187+
if json.Unmarshal(raw, &m) != nil {
188+
return nil, false
189+
}
190+
out := make(map[string]Value, len(m))
191+
for k, r := range m {
192+
out[k] = NewValue(r)
193+
}
194+
return out, true
195+
}
196+
197+
// asList reports the value as a KEYED list. A bare JSON array is NOT one: its
198+
// elements have no identity, so it is a scalar and changes wholesale. Keys are
199+
// what make a list mergeable, and they are supplied by whoever builds it.
200+
func asList(v Value) ([]KeyedValue, bool) {
201+
raw := v.Raw()
202+
if len(raw) == 0 || firstToken(raw) != '[' {
203+
return nil, false
204+
}
205+
var probe []json.RawMessage
206+
if json.Unmarshal(raw, &probe) != nil {
207+
return nil, false
208+
}
209+
items := make([]KeyedValue, 0, len(probe))
210+
seen := make(map[string]bool, len(probe))
211+
for _, r := range probe {
212+
var kv struct {
213+
Key *string `json:"Key"`
214+
Value json.RawMessage `json:"Value"`
215+
}
216+
if json.Unmarshal(r, &kv) != nil || kv.Key == nil {
217+
return nil, false
218+
}
219+
if seen[*kv.Key] {
220+
return nil, false // duplicate keys: not a keyed list
221+
}
222+
seen[*kv.Key] = true
223+
items = append(items, KeyedValue{Key: *kv.Key, Value: NewValue(kv.Value)})
224+
}
225+
return items, true
226+
}
227+
228+
func firstToken(raw []byte) byte {
229+
for _, c := range raw {
230+
switch c {
231+
case ' ', '\t', '\n', '\r':
232+
continue
233+
default:
234+
return c
235+
}
236+
}
237+
return 0
238+
}

0 commit comments

Comments
 (0)