From d88db3334547e6ef680638d3d21f4adf7491e0b7 Mon Sep 17 00:00:00 2001 From: Julian Matschinske Date: Sat, 26 Sep 2026 19:32:23 +0200 Subject: [PATCH 1/2] conformance: dispose the declared cases and verify full trees across carriers against released bitruntime. Adds conformance/wiretree, a separately identified family for decision 0012 with independently authored expectations: 18 structural cases, 1 addressed-bridge case and 7 carrier cases. Go and TypeScript drivers run it against the released bitruntime v0.2.0 through a test-only reference interpreter and through bitruntime's own Compose/Select/Send/AsAddressed, on the local pair and on real WebSockets in both directions. All 26 cases pass in both languages and both realizations. Seven deliberately unlawful TypeScript realizations are each rejected. disposition.json maps every one of the 39 historical declared cases, the 19 recorded production gaps and both limitations to current cases or to an explicit historical addressed limitation (suffix delivery, path-observing interception, addressed views). The historical fixture and both gap ledgers are unchanged and pinned by digest. Carrier composition uses two test-only adapters, bind and serve, because bitruntime v0.2.0 has no public facility for either; bitruntime#15 tracks them. Refs #29, #39. --- CHANGELOG.md | 11 + conformance/README.md | 6 +- conformance/declared/README.md | 6 + conformance/runtime/go/wiretree/main.go | 945 ++++++++++++++++++++++++ conformance/runtime/ts/wiretree.ts | 547 ++++++++++++++ conformance/trees/README.md | 4 + conformance/wiretree/README.md | 145 ++++ conformance/wiretree/cases.json | 790 ++++++++++++++++++++ conformance/wiretree/disposition.json | 164 ++++ docs/composition.md | 23 + scripts/README.md | 15 +- scripts/check.mjs | 2 +- scripts/conformance-runtime.mjs | 44 +- scripts/wiretree-lib.mjs | 213 ++++++ scripts/wiretree.test.mjs | 69 ++ 15 files changed, 2975 insertions(+), 9 deletions(-) create mode 100644 conformance/runtime/go/wiretree/main.go create mode 100644 conformance/runtime/ts/wiretree.ts create mode 100644 conformance/wiretree/README.md create mode 100644 conformance/wiretree/cases.json create mode 100644 conformance/wiretree/disposition.json create mode 100644 scripts/wiretree-lib.mjs create mode 100644 scripts/wiretree.test.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index bb14ea5..e3600b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ ## Unreleased +- Add the full-tree conformance family (`conformance/wiretree`) for decision + 0012: 18 structural, 1 bridge and 7 carrier cases with independently authored + expectations, run in Go and TypeScript against released bitruntime v0.2.0 on + the local pair and real WebSockets in both directions. It separates + structural, bridge and carrier evidence, and rejects seven deliberately + unlawful realizations. A disposition maps all 39 historical declared cases, + their 19 recorded gaps and two limitations to current cases or explicit + historical addressed limitations; the historical files are unchanged and + pinned by digest. The adapters that bind a Wire to a carrier path and serve a + tree on a dispatcher are test-only until bitruntime provides them. + ## 0.3.0 — 2026-09-26 - Publish the immutable source release at `f825f3f4a79135646b775e25dcd770656546b4a1`. diff --git a/conformance/README.md b/conformance/README.md index 7cccd6b..4ebc3bb 100644 --- a/conformance/README.md +++ b/conformance/README.md @@ -42,6 +42,7 @@ the case files' SHA-256s and every family's result in each language. | Declared, reference | The 39 [declared](declared/cases.json) cases through the test-only interpreter over bitruntime's carriers | the same three | 39/39 on each | 39/39 on each | | Declared, production | The same cases through bitruntime's child-only addressed mount, with its selection and forwarding | the same three | 20 conform; 19 match bitruntime's own [gap ledger](runtime/production-gaps.json) | the same | | Trees | The [0.3 observations](trees/expected.json) through bitruntime's tree construction, selection, sending and addressed bridge | none, structural | 14/14 | 14/14 | +| Full trees | The [full-tree cases](wiretree/README.md): 18 structural, 1 bridge, 7 carrier; test-only reference and bitruntime's tree operations | none for structure and bridge; the three carriers for carrier cases, with test-only bind and serve adapters | 26/26 in each realization | 26/26 in each realization | The Go drivers use `core.Invocation`, `core.NewPair`, the WebSocket engine, `dispatch`, `core.At`, `core.Mount`, `core.Forward`, `core.Compose`, @@ -63,7 +64,10 @@ every refusal in these fixtures happens before a carrier or forwarder. Lifecycle, composition and declared results remain evidence about the 0.2 addressed contract (`AddressedWire` in 0.3); trees is the 0.3 structural -contract. +contract. The full-tree family separates structural, bridge and carrier +evidence, rejects seven deliberately unlawful TypeScript realizations, and +[disposes](wiretree/disposition.json) every historical declared case and +recorded gap. ## Current released runtime baseline diff --git a/conformance/declared/README.md b/conformance/declared/README.md index 28aa2a3..5f6fc29 100644 --- a/conformance/declared/README.md +++ b/conformance/declared/README.md @@ -11,6 +11,12 @@ defines the realization: an origin at every node, complete named children, and construction parts retained by their owner. Run `node scripts/conformance-current.mjs` to exercise the [fixtures](cases.json) in Go and TypeScript. +**Disposition under decision 0012:** [`../wiretree/disposition.json`](../wiretree/disposition.json) +maps each of these 39 cases, the 19 recorded gaps and both limitations to +current [full-tree cases](../wiretree/README.md) or to an explicit historical +addressed limitation. This fixture and its gap ledgers stay unchanged; the +disposition pins them by digest. + ## Two realizations, one oracle The runner removes every `expected` value before handing inputs to a driver, and diff --git a/conformance/runtime/go/wiretree/main.go b/conformance/runtime/go/wiretree/main.go new file mode 100644 index 0000000..2eebadb --- /dev/null +++ b/conformance/runtime/go/wiretree/main.go @@ -0,0 +1,945 @@ +// Full-tree driver for ../../../wiretree/cases.json, run by +// scripts/conformance-runtime.mjs; the same interpretation as ../../ts/wiretree.ts. +// Expectations are withheld: this program only interprets inputs and records +// what happened. "production" is bitruntime's core.Compose, core.Select, +// core.Send and core.AsAddressed; "reference" is a test-only structural +// interpreter that is never evidence about a runtime. Carrier cases always use +// bitruntime's carriers, dispatcher, Forward, At and Mount. bind and serve are +// test-only because bitruntime v0.2.0 has no public facility for either +// (bitruntime#15). +package main + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "os" + "reflect" + "slices" + "strings" + "sync" + "time" + "unicode/utf8" + + "bitwire.conformance/runtime/internal/carrier" + core "github.com/Bitspark/bitruntime/core/go" + dispatch "github.com/Bitspark/bitruntime/dispatch/go" + transports "github.com/Bitspark/bitruntime/transports/go" + wire "github.com/Bitspark/bitwire/wire/go" +) + +type tree = wire.WireTree +type child = wire.Child[wire.Wire] + +// ---- Realizations of the structural operations ---- + +type trees interface { + Compose(own wire.Wire, children []child) (tree, error) + Select(t tree, path wire.TreePath) (tree, bool) + Send(t tree, path wire.TreePath, message wire.Message) error + AsAddressed(t tree) wire.AddressedWire +} + +type production struct{} + +func (production) Compose(own wire.Wire, children []child) (tree, error) { + return core.Compose(own, children) +} +func (production) Select(t tree, path wire.TreePath) (tree, bool) { return core.Select(t, path) } +func (production) Send(t tree, path wire.TreePath, m wire.Message) error { + return core.Send(t, path, m) +} +func (production) AsAddressed(t tree) wire.AddressedWire { return core.AsAddressed(t) } + +// node is a test-only interpreter of the contract: complete, exact, immutable, acyclic. +type node struct { + own wire.Wire + children []child +} + +func copyChildren(children []child) []child { + out := make([]child, len(children)) + for i, c := range children { + out[i] = child{Key: bytes.Clone(c.Key), Tree: c.Tree} + if out[i].Key == nil { + out[i].Key = []byte{} + } + } + return out +} +func (n *node) Own() wire.Wire { return n.own } +func (n *node) Children() []child { return copyChildren(n.children) } +func (n *node) At(path wire.TreePath) (tree, bool) { return reference{}.Select(n, path) } +func (n *node) Decompose() (wire.Wire, []child) { return n.own, n.Children() } + +type reference struct{} + +func (reference) Compose(own wire.Wire, children []child) (tree, error) { + seen := map[string]bool{} + for _, c := range children { + if c.Tree == nil || reflect.ValueOf(c.Tree).Kind() == reflect.Pointer && reflect.ValueOf(c.Tree).IsNil() { + return nil, errors.New("missing child") + } + if seen[string(c.Key)] { + return nil, errors.New("duplicate key") + } + seen[string(c.Key)] = true + } + active := map[tree]bool{} + var visit func(t tree) error + visit = func(t tree) error { + if _, local := t.(*node); local { + return nil + } + if active[t] { + return errors.New("cycle") + } + active[t] = true + for _, c := range t.Children() { + if err := visit(c.Tree); err != nil { + return err + } + } + delete(active, t) + return nil + } + for _, c := range children { + if err := visit(c.Tree); err != nil { + return nil, err + } + } + return &node{own: own, children: copyChildren(children)}, nil +} +func (reference) Select(t tree, path wire.TreePath) (tree, bool) { + current := t + for _, key := range path { + var next tree + for _, c := range current.Children() { + if bytes.Equal(c.Key, key) { + next = c.Tree + break + } + } + if next == nil { + return nil, false + } + current = next + } + return current, true +} +func (r reference) Send(t tree, path wire.TreePath, m wire.Message) error { + selected, ok := r.Select(t, path) + if !ok { + return errors.New("missing") + } + return selected.Own().Send(m) +} +func (r reference) AsAddressed(t tree) wire.AddressedWire { return referenceBridge{r, t} } + +type referenceBridge struct { + r reference + t tree +} + +func (b referenceBridge) Send(path []string, m wire.Message) error { + keys := wire.TreePath{} + for _, segment := range path { + if !utf8.ValidString(segment) { + return errors.New("invalid path") + } + keys = append(keys, []byte(segment)) + } + return b.r.Send(b.t, keys, m) +} + +// ---- Inputs ---- + +type declaration struct { + ID string `json:"id"` + Own *string `json:"own"` + Children [][2]string `json:"children"` +} +type step struct { + Op string `json:"op"` + Path []string `json:"path"` + Keep [][]string `json:"keep"` + Selections [][]string `json:"selections"` + Paths [][]string `json:"paths"` + Via string `json:"via"` + Key string `json:"key"` + To string `json:"to"` + Node string `json:"node"` + Mode string `json:"mode"` + Own *string `json:"own"` + Side string `json:"side"` +} +type testCase struct { + ID string `json:"id"` + Family string `json:"family"` + Root string `json:"root"` + Fault string `json:"fault"` + Relay bool `json:"relay"` + Mount bool `json:"mount"` + Steps []step `json:"steps"` +} +type inputs struct { + ServedAt []string `json:"servedAt"` + Declarations []declaration `json:"declarations"` + Cases []testCase `json:"cases"` +} + +func check(err error) { + if err != nil { + panic(err) + } +} +func key(h string) []byte { + b, err := hex.DecodeString(h) + check(err) + return b +} +func keys(path []string) wire.TreePath { + out := wire.TreePath{} + for _, h := range path { + out = append(out, key(h)) + } + return out +} +func segments(path []string) []string { + out := []string{} + for _, h := range path { + out = append(out, string(key(h))) + } + return out +} + +// ---- Instrumented primitives and structural editing ---- + +type primitive struct { + name string + instance int + h *harness +} + +func (p *primitive) Send(message wire.Message) error { return p.h.handle(p, message) } + +type refuser struct{ _ byte } + +func (*refuser) Send(wire.Message) error { return errors.New("refused") } + +type harness struct { + mu sync.Mutex + trace [][]any + unchanged bool + partsExact bool + expected *wire.Message + counts map[*primitive]int + instances map[string]int + shared map[string]*primitive + built map[string]tree + declarations map[string]declaration + t trees + deliver func(p *primitive, count int, message wire.Message) error +} + +func newHarness(t trees, in inputs) *harness { + h := &harness{unchanged: true, partsExact: true, counts: map[*primitive]int{}, instances: map[string]int{}, + shared: map[string]*primitive{}, built: map[string]tree{}, declarations: map[string]declaration{}, t: t} + for _, d := range in.Declarations { + h.declarations[d.ID] = d + } + return h +} +func (h *harness) push(entry ...any) { + h.mu.Lock() + h.trace = append(h.trace, entry) + h.mu.Unlock() +} +func (h *harness) length() int { + h.mu.Lock() + defer h.mu.Unlock() + return len(h.trace) +} +func (h *harness) setExpected(m wire.Message) { + h.mu.Lock() + h.expected = &m + h.mu.Unlock() +} +func (h *harness) handle(p *primitive, message wire.Message) error { + h.mu.Lock() + h.counts[p]++ + count := h.counts[p] + h.mu.Unlock() + return h.deliver(p, count, message) +} +func (h *harness) primitive(name string, fresh bool) wire.Wire { + h.mu.Lock() + defer h.mu.Unlock() + if p, ok := h.shared[name]; ok && !fresh { + return p + } + h.instances[name]++ + p := &primitive{name: name, instance: h.instances[name], h: h} + if !fresh { + h.shared[name] = p + } + return p +} + +// construct shows that neither the input nor the returned parts can change the tree. +func (h *harness) construct(own wire.Wire, children []child) (tree, error) { + type pair struct { + key string + t tree + } + want := []pair{} + input := []child{} + for _, c := range children { + want = append(want, pair{string(c.Key), c.Tree}) + input = append(input, child{Key: bytes.Clone(c.Key), Tree: c.Tree}) + } + t, err := h.t.Compose(own, input) + if err != nil { + return nil, err + } + for i := range input { + for j := range input[i].Key { + input[i].Key[j] = 'z' + } + input[i].Tree = nil + } + exact := func() bool { + gotOwn, got := t.Decompose() + if gotOwn != own || t.Own() != own || len(got) != len(want) || len(t.Children()) != len(want) { + return false + } + for _, w := range want { + if !slices.ContainsFunc(got, func(c child) bool { return string(c.Key) == w.key && c.Tree == w.t }) { + return false + } + } + return true + } + ok := exact() + returned := t.Children() + for i := range returned { + for j := range returned[i].Key { + returned[i].Key[j] = 'z' + } + returned[i].Tree = nil + } + ok = ok && exact() + h.mu.Lock() + h.partsExact = h.partsExact && ok + h.mu.Unlock() + return t, nil +} +func (h *harness) must(t tree, err error) tree { + check(err) + return t +} + +type loop struct{ own wire.Wire } + +func (l *loop) Own() wire.Wire { return l.own } +func (l *loop) Children() []child { return []child{{Key: []byte("again"), Tree: l}} } +func (l *loop) At(wire.TreePath) (tree, bool) { return nil, false } +func (l *loop) Decompose() (wire.Wire, []child) { return l.own, l.Children() } + +func (h *harness) build(id, fault, rootID string) (tree, error) { + if t, ok := h.built[id]; ok { + return t, nil + } + d := h.declarations[id] + children := []child{} + for _, c := range d.Children { + t, err := h.build(c[1], fault, rootID) + if err != nil { + return nil, err + } + children = append(children, child{Key: key(c[0]), Tree: t}) + } + if id == rootID { + switch fault { + case "duplicate": + children = append(children, child{Key: []byte("a"), Tree: h.must(h.build("leaf", "", ""))}) + case "missingChild": + children = append(children, child{Key: []byte("hole"), Tree: nil}) + case "cycle": + children = append(children, child{Key: []byte("loop"), Tree: &loop{own: &refuser{}}}) + } + } + var own wire.Wire = &refuser{} + if d.Own != nil { + own = h.primitive(*d.Own, false) + } + t, err := h.construct(own, children) + if err != nil { + return nil, err + } + h.built[id] = t + return t, nil +} + +type render struct { + Own any `json:"own"` + Children [][]any `json:"children"` +} + +func (h *harness) render(t tree) render { + var own any = "unknown" + switch p := t.Own().(type) { + case *refuser: + own = nil + case *primitive: + own = []any{p.name, p.instance} + } + children := [][]any{} + for _, c := range t.Children() { + children = append(children, []any{hex.EncodeToString(c.Key), h.render(c.Tree)}) + } + return render{Own: own, Children: children} +} +func (h *harness) rebuild(t tree, where []string, keep [][]string) tree { + for _, path := range keep { + if slices.Equal(path, where) { + return t + } + } + own, children := t.Decompose() + next := []child{} + for _, c := range children { + next = append(next, child{Key: c.Key, Tree: h.rebuild(c.Tree, append(slices.Clone(where), hex.EncodeToString(c.Key)), keep)}) + } + return h.must(h.construct(own, next)) +} +func (h *harness) replaceAt(t tree, path []string, f func(tree) tree) tree { + if len(path) == 0 { + return f(t) + } + own, children := t.Decompose() + found := false + next := []child{} + for _, c := range children { + if hex.EncodeToString(c.Key) == path[0] { + found = true + next = append(next, child{Key: c.Key, Tree: h.replaceAt(c.Tree, path[1:], f)}) + } else { + next = append(next, c) + } + } + if !found { + panic("edit path leaves the tree") + } + return h.must(h.construct(own, next)) +} +func (h *harness) fresh(own wire.Wire) wire.Wire { + if p, ok := own.(*primitive); ok { + return h.primitive(p.name, true) + } + return &refuser{} +} +func (h *harness) copy(t tree) tree { + next := []child{} + for _, c := range t.Children() { + next = append(next, child{Key: c.Key, Tree: h.copy(c.Tree)}) + } + return h.must(h.construct(h.fresh(t.Own()), next)) +} +func (h *harness) edit(t tree, s step, build func(id string) tree) tree { + switch s.Op { + case "rebuild": + return h.rebuild(t, []string{}, s.Keep) + case "replace": + return h.replaceAt(t, s.Path, func(tree) tree { return build(s.Node) }) + case "own": + return h.replaceAt(t, s.Path, func(n tree) tree { + var own wire.Wire = &refuser{} + if s.Own != nil { + own = h.fresh(n.Own()) + } + return h.must(h.construct(own, n.Children())) + }) + case "substitute": + return h.replaceAt(t, s.Path, func(n tree) tree { + if s.Mode == "copy" { + return h.copy(n) + } + return h.rebuild(n, []string{}, nil) + }) + case "omit", "rename", "add": + return h.replaceAt(t, s.Path, func(n tree) tree { + next := []child{} + for _, c := range n.Children() { + name := hex.EncodeToString(c.Key) + if s.Op == "omit" && name == s.Key { + continue + } + if s.Op == "rename" && name == s.Key { + c.Key = key(s.To) + } + next = append(next, c) + } + if s.Op == "add" { + next = append(next, child{Key: key(s.Key), Tree: build(s.Node)}) + } + return h.must(h.construct(n.Own(), next)) + }) + } + panic("unknown edit " + s.Op) +} + +// sameJSON compares encoded JSON payloads by value, since a carrier may re-encode them. +func sameJSON(a, b json.RawMessage) bool { + var x, y any + if json.Unmarshal(a, &x) != nil || json.Unmarshal(b, &y) != nil { + return bytes.Equal(a, b) + } + return reflect.DeepEqual(x, y) +} + +// chain applies each selection with the node's own At, and checks it against +// selecting the concatenation. +func (h *harness) chain(start tree, selections [][]string, fromRoot bool) (tree, bool) { + current := start + for _, selection := range selections { + next, ok := current.At(keys(selection)) + if !ok { + return nil, false + } + current = next + } + if fromRoot { + all := []string{} + for _, selection := range selections { + all = append(all, selection...) + } + if direct, ok := h.t.Select(start, keys(all)); !ok || direct != current { + h.push("selectionDiffers") + } + } + return current, true +} + +// derived sends through the realization: missing selection invokes nothing; +// a present node's refusal is its own. +func (h *harness) derived(base tree, ok bool, path []string, m wire.Message) { + if !ok { + h.push("missing") + return + } + _, present := h.t.Select(base, keys(path)) + before := h.length() + h.setExpected(m) + if err := h.t.Send(base, keys(path), m); err != nil { + if h.length() != before { + h.push("fallback") + } + if present { + h.push("refused") + } else { + h.push("missing") + } + return + } + if !present { + h.push("fabricated") + } +} + +type refuseWire struct{ _ byte } + +func (*refuseWire) Send([]string, wire.Message) error { return errors.New("not a reply target") } + +var replyless = &wire.ReturnAddress{Wire: &refuseWire{}} + +func event(data string) wire.Message { + encoded, err := json.Marshal(data) + check(err) + return wire.Message{Frame: wire.ProfileFrame{Version: 1, Kind: wire.ProfileEvent, Data: encoded}, Return: replyless} +} + +// ---- Local families: structure and the addressed bridge ---- + +func local(t trees, in inputs, test testCase) any { + h := newHarness(t, in) + h.deliver = func(p *primitive, count int, m wire.Message) error { + h.mu.Lock() + h.unchanged = h.unchanged && h.expected != nil && reflect.DeepEqual(m.Frame, h.expected.Frame) && m.Return == h.expected.Return + h.mu.Unlock() + h.push("delivered", p.name, count) + return nil + } + root, err := h.build(test.Root, test.Fault, test.Root) + if err != nil { + return map[string]any{"construction": "refused"} + } + if test.Fault != "" { + return map[string]any{"construction": "accepted"} + } + var view tree + haveView := false + sendOnly := true + for index, s := range test.Steps { + m := event(fmt.Sprintf("%s:%d", test.ID, index)) + switch s.Op { + case "structure": + if n, ok := t.Select(root, keys(s.Path)); ok { + h.push("structure", h.render(n)) + } else { + h.push("structure", "missing") + } + case "send": + start, ok := root, true + if s.Via == "view" { + start, ok = view, haveView + } + var base tree + if ok { + base, ok = h.chain(start, s.Selections, s.Via != "view") + } + h.derived(base, ok, s.Path, m) + case "same": + a, aok := t.Select(root, keys(s.Paths[0])) + b, bok := t.Select(root, keys(s.Paths[1])) + h.push("same", aok && bok && a.Own() == b.Own()) + case "view": + view, haveView = h.chain(root, s.Selections, true) + case "bridge", "bridgeInvalid": + bridge := t.AsAddressed(root) + _, receives := bridge.(interface { + Receive(wire.Receiver) (func(), error) + }) + _, closes := bridge.(interface{ Close(wire.Code, string) error }) + _, structural := bridge.(interface{ Own() wire.Wire }) + sendOnly = sendOnly && !receives && !closes && !structural + path := s.Path + if s.Op == "bridgeInvalid" { + path = []string{"\xff"} + } + before := h.length() + h.setExpected(m) + if err := bridge.Send(path, m); err != nil { + if h.length() != before { + h.push("fallback") + } + h.push("refused") + } + default: + root = h.edit(root, s, func(id string) tree { return h.must(h.build(id, "", "")) }) + } + } + if test.Family == "bridge" { + return map[string]any{"trace": h.trace, "sendOnly": sendOnly, "unchanged": h.unchanged} + } + return map[string]any{"trace": h.trace, "partsExact": h.partsExact, "unchanged": h.unchanged} +} + +// ---- Carrier family ---- + +// bindWire is test-only addressless access at one fixed addressed path (bitruntime#15). +type bindWire struct { + access wire.AddressedWire + path []string +} + +func (b *bindWire) Send(m wire.Message) error { return b.access.Send(slices.Clone(b.path), m) } + +// serve is test-only: exact dispatcher routes for every node whose keys are all +// UTF-8, each bound to that node's own Wire (bitruntime#15). A Go receiver +// returns nothing, so a refused request is answered through core.Respond. +func serve(d *dispatch.Dispatcher, t tree, prefix []string) func() { + var detach []func() + var visit func(n tree, path []string) + visit = func(n tree, path []string) { + own := n.Own() + release, err := d.Register(append(slices.Clone(prefix), path...), wire.Receiver{Message: func(_ []string, m wire.Message) { + if err := own.Send(m); err != nil && m.Frame.Kind == wire.ProfileRequest { + _ = core.Respond(m, nil, err) + } + }}) + check(err) + detach = append(detach, release) + for _, c := range n.Children() { + if !utf8.Valid(c.Key) { + continue + } + visit(c.Tree, append(slices.Clone(path), string(c.Key))) + } + } + visit(t, []string{}) + return func() { + for _, release := range detach { + release() + } + } +} + +func take[T any](ch chan T) T { + select { + case v := <-ch: + return v + case <-time.After(5 * time.Second): + panic("delivery deadline exceeded") + } +} + +type replies struct{ ch chan wire.Message } + +func (r replies) Send(path []string, m wire.Message) error { + if len(path) != 0 || m.Frame.Kind != wire.ProfileResponse { + return errors.New("unexpected reply") + } + r.ch <- m + return nil +} + +type call struct { + message wire.Message + replies chan wire.Message +} + +func runCarrier(t trees, in inputs, test testCase) (result any) { + var cleanups []func() + defer func() { + slices.Reverse(cleanups) + for _, cleanup := range cleanups { + cleanup() + } + }() + cleanup := func(f func()) { cleanups = append(cleanups, f) } + near, first := carrier.Pair(cleanup) + far := first + if test.Relay { + outgoing, target := carrier.Pair(cleanup) + detach, err := core.Forward(first, outgoing) + check(err) + cleanup(detach) + far = target + } + var access wire.AddressedWire = near + if test.Mount { + mounted := core.Mount(map[string]wire.Endpoint{"mounted": near}) + cleanup(func() { _ = mounted.Close(transports.CodeNormal, "done") }) + access = core.At(mounted, []string{"mounted"}) + } + + // The far side: an instrumented tree served on a dispatcher that borrows the endpoint. + h := newHarness(t, in) + signals := make(chan string, 8) + var hold bool + var held struct { + message wire.Message + name string + } + h.deliver = func(p *primitive, count int, m wire.Message) error { + h.mu.Lock() + expected := h.expected + ok := expected != nil && m.Frame.Kind == expected.Frame.Kind && m.Return != nil + if ok && m.Frame.Kind == wire.ProfileRequest { + ok = sameJSON(m.Frame.Params, expected.Frame.Params) + } + h.unchanged = h.unchanged && ok + if m.Frame.Kind == wire.ProfileCancel { + h.counts[p]-- + } + holding := hold && m.Frame.Kind == wire.ProfileRequest + if holding { + hold = false + held.message, held.name = m, p.name + } + h.mu.Unlock() + switch { + case m.Frame.Kind == wire.ProfileCancel: + h.push("cancelled", p.name) + signals <- "cancelled" + case m.Frame.Kind == wire.ProfileEvent: + h.push("delivered", p.name, count) + case holding: + h.push("held", p.name, count) + signals <- "held" + default: + h.push("delivered", p.name, count) + result, err := json.Marshal(p.name) + check(err) + return m.Return.Wire.Send(nil, wire.Message{Frame: wire.ProfileFrame{Version: 1, Kind: wire.ProfileResponse, ID: m.Frame.ID, Result: result}}) + } + return nil + } + farTree := h.must(h.build(test.Root, "", "")) + d, err := dispatch.NewDispatcher(far) + check(err) + unserve := serve(d, farTree, in.ServedAt) + + // The near side: the same declared structure, each own Wire bound to its far + // carrier path. A carrier path names a far position, and addressed access + // cannot show that two positions share a node, so every position is bound. + var mirror func(id string, path []string) tree + mirror = func(id string, path []string) tree { + dcl := h.declarations[id] + children := []child{} + for _, c := range dcl.Children { + k := key(c[0]) + if !utf8.Valid(k) { + continue + } + children = append(children, child{Key: k, Tree: mirror(c[1], append(slices.Clone(path), string(k)))}) + } + n, err := t.Compose(&bindWire{access: access, path: append(slices.Clone(in.ServedAt), path...)}, children) + check(err) + return n + } + nearTree := mirror(test.Root, []string{}) + + serial := 0 + request := func(label string) call { + serial++ + params, err := json.Marshal(label) + check(err) + c := call{replies: make(chan wire.Message, 2)} + c.message = wire.Message{Frame: wire.ProfileFrame{Version: 1, Kind: wire.ProfileRequest, ID: fmt.Sprintf("c:%d", serial), Params: params}, + Return: &wire.ReturnAddress{Wire: replies{c.replies}}} + return c + } + outcome := func(c call) { + reply := take(c.replies) + if reply.Frame.Error != nil { + h.push("error", reply.Frame.Error.Code) + } + } + var pending call + + for index, s := range test.Steps { + label := fmt.Sprintf("%s:%d", test.ID, index) + switch s.Op { + case "send", "hold", "cancel": + base, ok := h.chain(nearTree, s.Selections, true) + if ok { + _, ok = t.Select(base, keys(s.Path)) + } + if !ok { + h.push("missing") + continue + } + if s.Op == "cancel" { + cancel := wire.Message{Frame: wire.ProfileFrame{Version: 1, Kind: wire.ProfileCancel, ID: pending.message.Frame.ID}, Return: pending.message.Return} + h.setExpected(cancel) + check(t.Send(base, keys(s.Path), cancel)) + take(signals) + continue + } + c := request(label) + h.setExpected(c.message) + h.mu.Lock() + hold = s.Op == "hold" + h.mu.Unlock() + if err := t.Send(base, keys(s.Path), c.message); err != nil { + h.mu.Lock() + hold = false + h.mu.Unlock() + h.push("refused") + continue + } + if s.Op == "hold" { + pending = c + take(signals) + } else { + outcome(c) + } + case "sendAddressed": + c := request(label) + h.setExpected(c.message) + if err := access.Send(s.Path, c.message); err != nil { + h.push("refused") + continue + } + outcome(c) + case "release": + h.mu.Lock() + message, name := held.message, held.name + h.mu.Unlock() + result, err := json.Marshal(name) + check(err) + check(message.Return.Wire.Send(nil, wire.Message{Frame: wire.ProfileFrame{Version: 1, Kind: wire.ProfileResponse, ID: message.Frame.ID, Result: result}})) + var late string + check(json.Unmarshal(take(pending.replies).Frame.Result, &late)) + h.push("late", late) + case "structure": + if n, ok := t.Select(farTree, keys(s.Path)); ok { + h.push("structure", h.render(n)) + } else { + h.push("structure", "missing") + } + case "direct": + h.derived(farTree, true, s.Path, event(label)) + case "teardown": + unserve() + check(d.Close(transports.CodeNormal, "released")) + default: + if s.Side == "far" { + farTree = h.edit(farTree, s, func(id string) tree { return h.must(h.build(id, "", "")) }) + unserve() + unserve = serve(d, farTree, in.ServedAt) + } else { + nearTree = h.edit(nearTree, s, func(id string) tree { return mirror(id, segments(s.Path)) }) + } + } + } + + // The borrowed endpoint outlives every composition over it. + unserve() + _ = d.Close(transports.CodeNormal, "released") + borrowed := make(chan wire.Message, 1) + detach, err := far.Receive(wire.Receiver{Message: func(path []string, m wire.Message) { + if strings.Join(path, "/") == "borrowed" { + borrowed <- m + } + }}) + usable := false + if err == nil { + cleanup(detach) + if near.Send([]string{"borrowed"}, event("borrowed")) == nil { + select { + case m := <-borrowed: + usable = m.Frame.Kind == wire.ProfileEvent + case <-time.After(5 * time.Second): + } + } + } + h.mu.Lock() + defer h.mu.Unlock() + return map[string]any{"trace": h.trace, "unchanged": h.unchanged, "borrowedUsable": usable} +} + +func main() { + if len(os.Args) != 4 || !slices.Contains([]string{"reference", "production"}, os.Args[1]) || !slices.Contains([]string{"local", "carrier"}, os.Args[2]) { + panic("usage: wiretree reference|production local|carrier inputs.json") + } + var t trees = reference{} + if os.Args[1] == "production" { + t = production{} + } + data, err := os.ReadFile(os.Args[3]) + check(err) + var in inputs + check(json.Unmarshal(data, &in)) + if os.Args[2] == "carrier" { + carrier.Kind() + } + output := []map[string]any{} + for _, test := range in.Cases { + if (os.Args[2] == "carrier") != (test.Family == "carrier") { + continue + } + var observations any + if test.Family == "carrier" { + observations = runCarrier(t, in, test) + } else { + observations = local(t, in, test) + } + output = append(output, map[string]any{"id": test.ID, "observations": observations}) + } + check(json.NewEncoder(os.Stdout).Encode(output)) +} diff --git a/conformance/runtime/ts/wiretree.ts b/conformance/runtime/ts/wiretree.ts new file mode 100644 index 0000000..abd266a --- /dev/null +++ b/conformance/runtime/ts/wiretree.ts @@ -0,0 +1,547 @@ +// Full-tree driver for ../../wiretree/cases.json, run by scripts/conformance-runtime.mjs. +// Expectations are withheld: this file only interprets inputs and records what +// happened. Two realizations share one harness. "production" is bitruntime's +// compose, select, send and asAddressed; "reference" is a test-only structural +// interpreter that is never evidence about a runtime. Carrier cases always use +// bitruntime's carriers, dispatcher, forward, at and mount. Two small adapters +// below are test-only because bitruntime v0.2.0 has no public facility for them +// (bitruntime#15): bind, a Wire sending at a fixed AddressedWire path, and serve, +// which registers a tree's UTF-8 nodes on a dispatcher. +import { readFileSync } from 'node:fs'; +import { isDeepStrictEqual } from 'node:util'; +import type { AddressedWire, Child, DeixisNode, Endpoint, Key, Message, Parts, Path, ReturnAddress, TreePath, Wire, WireTree } from '@bitspark/bitwire'; +import { asAddressed, at, compose, forward, mount, select, send } from '@bitspark/bitruntime/core'; +import { createDispatcher, type Dispatcher } from '@bitspark/bitruntime/dispatch'; +import { CODE_NORMAL } from '@bitspark/bitruntime/transports'; +import { connected, kind } from './carrier.ts'; + +// ---- Realizations of the structural operations ---- +interface Trees { + compose(own: Wire, children: readonly Child[]): WireTree; + select(tree: WireTree, path: TreePath): WireTree | undefined; + send(tree: WireTree, path: TreePath, message: Message): void; + asAddressed(tree: WireTree): AddressedWire; +} +const production: Trees = { compose, select, send, asAddressed }; + +const hex = (key: Key): string => Buffer.from(key).toString('hex'); +const bytes = (text: string): Key => Uint8Array.from(Buffer.from(text, 'hex')); +const utf8 = new TextDecoder('utf-8', { fatal: true }); +const encoder = new TextEncoder(); + +/** Test-only interpreter of the contract: complete, exact, immutable, acyclic. */ +class Node implements WireTree { + readonly #own: Wire; + readonly #children: readonly Child[]; + constructor(own: Wire, children: readonly Child[]) { + const names = new Set(); + const copied: Child[] = []; + for (const [key, child] of children) { + if (!(key instanceof Uint8Array) || !child || typeof child.children !== 'function') throw new Error('invalid child'); + if (names.has(hex(key))) throw new Error('duplicate key'); + names.add(hex(key)); + copied.push(Object.freeze([Uint8Array.from(key), child] as const)); + } + const active = new Set(); + const visit = (node: WireTree): void => { + if (node instanceof Node) return; + if (active.has(node)) throw new Error('cycle'); + active.add(node); + for (const [, child] of node.children()) visit(child); + active.delete(node); + }; + for (const [, child] of copied) visit(child); + this.#own = own; + this.#children = Object.freeze(copied); + } + own(): Wire { return this.#own; } + children(): Child[] { return this.#children.map(([key, child]) => [Uint8Array.from(key), child] as const); } + at(path: TreePath): WireTree | undefined { return reference.select(this, path); } + decompose(): Parts { return { own: this.#own, children: this.children() }; } +} +const reference: Trees = { + compose: (own, children) => new Node(own, children), + select(tree, path) { + let current: WireTree | undefined = tree; + for (const key of path) current = current?.children().find(([candidate]) => hex(candidate) === hex(key))?.[1]; + return current; + }, + send(tree, path, message) { + const node = reference.select(tree, path); + if (!node) throw new Error('missing'); + node.own().send(message); + }, + asAddressed(tree) { + return { send(path: Path, message: Message) { + if (!path.every(segment => segment.isWellFormed())) throw new Error('invalid path'); + reference.send(tree, path.map(segment => encoder.encode(segment)), message); + } }; + }, +}; + +// ---- Deliberately unlawful realizations: each must fail the oracle ---- +const refusingNode = (): WireTree => new Node({ send() { throw new Error('refused'); } }, []); +const mutants: Record = { + /** A missing descendant is sent to its deepest present ancestor. */ + fallback: { ...reference, send(tree, path, message) { + let node = tree; + for (const key of path) node = node.children().find(([candidate]) => hex(candidate) === hex(key))?.[1] ?? node; + node.own().send(message); + } }, + /** The own capability is replaced by a forwarding wrapper. */ + 'wrapping-own': { ...reference, compose: (own, children) => new Node({ send: message => own.send(message) }, children) }, + /** UTF-8 keys are NFC-normalized, conflating distinct byte keys. */ + normalizing: { ...reference, compose: (own, children) => new Node(own, children.map(([key, child]) => { + let text: string; + try { text = utf8.decode(key); } catch { return [key, child] as const; } + return [encoder.encode(text.normalize('NFC')), child] as const; + })) }, + /** A missing path selects a fabricated refusing node. */ + fabricating: { ...reference, select: (tree, path) => reference.select(tree, path) ?? refusingNode() }, + /** The bridge encodes segments as Latin-1 when it can, so "ÿ" names the byte key ff. */ + 'latin1-bridge': { ...reference, asAddressed: tree => ({ send(path: Path, message: Message) { + reference.send(tree, path.map(segment => [...segment].every(c => c.charCodeAt(0) < 256) ? Uint8Array.from([...segment].map(c => c.charCodeAt(0))) : encoder.encode(segment)), message); + } }) }, + /** children() omits the empty key, so the child map is incomplete. */ + 'incomplete-children': { ...reference, compose: (own, children) => { + const node = new Node(own, children); + return Object.assign(Object.create(node), { + own: () => node.own(), at: (path: TreePath) => node.at(path), decompose: () => node.decompose(), + children: () => node.children().filter(([key]) => key.length > 0), + }) as WireTree; + } }, +}; + +// ---- Inputs ---- +interface Declaration { id: string; own: string | null; children: [string, string][] } +interface Step { + op: string; path?: string[]; keep?: string[][]; selections?: string[][]; paths?: string[][]; + via?: string; key?: string; to?: string; node?: string; mode?: string; own?: string | null; side?: string; +} +interface Case { id: string; family: string; root: string; fault?: string; relay?: boolean; mount?: boolean; steps: Step[] } +interface Inputs { servedAt: string[]; declarations: Declaration[]; cases: Case[] } +type Trace = unknown[][]; + +function mailbox() { + const values: T[] = []; + let waiting: ((value: T) => void) | undefined; + return { + put(value: T) { + if (waiting) { const resolve = waiting; waiting = undefined; resolve(value); } else values.push(value); + }, + take(): Promise { + if (values.length) return Promise.resolve(values.shift()!); + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { waiting = undefined; reject(new Error('delivery deadline exceeded')); }, 5000); + waiting = value => { clearTimeout(timer); resolve(value); }; + }); + }, + }; +} + +// ---- Instrumented primitives and structural editing ---- +class Harness { + readonly trace: Trace = []; + unchanged = true; + partsExact = true; + expected?: Message; + readonly names = new Map(); + readonly refusing = new Set(); + readonly counts = new Map(); + private readonly instances = new Map(); + private readonly shared = new Map(); + private readonly built = new Map(); + readonly declarations: Map; + readonly T: Trees; + readonly handle: (self: Wire, name: string, message: Message) => void; + constructor(T: Trees, inputs: Inputs, handle: (self: Wire, name: string, message: Message) => void) { + this.T = T; + this.handle = handle; + this.declarations = new Map(inputs.declarations.map(d => [d.id, d])); + } + /** One stateful primitive per name and case; fresh instances only when a step asks. */ + primitive(name: string, fresh = false): Wire { + if (!fresh && this.shared.has(name)) return this.shared.get(name)!; + const instance = (this.instances.get(name) ?? 0) + 1; + this.instances.set(name, instance); + const self: Wire = { send: (message: Message) => this.handle(self, name, message) }; + this.names.set(self, [name, instance]); + this.counts.set(self, 0); + if (!fresh) this.shared.set(name, self); + return self; + } + refuser(): Wire { + const self: Wire = { send() { throw new Error('refused'); } }; + this.refusing.add(self); + return self; + } + count(self: Wire): number { + const next = this.counts.get(self)! + 1; + this.counts.set(self, next); + return next; + } + /** Constructs, then shows that neither the input nor the returned parts can change the tree. */ + construct(own: Wire, children: readonly Child[]): WireTree { + const want = children.map(([key, child]) => [hex(key), child] as const); + const input: [Key, WireTree][] = children.map(([key, child]) => [Uint8Array.from(key), child]); + const tree = this.T.compose(own, input); + for (const [key] of input) key.fill(0x7a); + input.length = 0; + const exact = (): boolean => { + const parts = tree.decompose(); + const got = parts.children.map(([key, child]) => [hex(key), child] as const); + return parts.own === own && tree.own() === own && got.length === want.length + && want.every(([key, child]) => got.some(([k, c]) => k === key && c === child)) + && tree.children().every(([key, child]) => want.some(([k, c]) => k === hex(key) && c === child)); + }; + let ok = exact(); + const returned = tree.children() as [Key, WireTree][]; + for (const [key] of returned) key.fill(0x7a); + try { returned.length = 0; } catch { /* A frozen collection protects the tree too. */ } + ok &&= exact(); + this.partsExact &&= ok; + return tree; + } + build(id: string, fault = '', rootID = ''): WireTree { + const found = this.built.get(id); + if (found) return found; + const d = this.declarations.get(id)!; + const children: Child[] = d.children.map(([key, child]) => [bytes(key), this.build(child, fault, rootID)]); + if (id === rootID && fault === 'duplicate') children.push([encoder.encode('a'), this.build('leaf')]); + if (id === rootID && fault === 'missingChild') children.push([encoder.encode('hole'), undefined as unknown as WireTree]); + if (id === rootID && fault === 'cycle') { + const loop: WireTree = { + own: () => this.refuser(), + children: () => [[encoder.encode('again'), loop]], + at: () => undefined, + decompose: () => ({ own: this.refuser(), children: [[encoder.encode('again'), loop]] }), + }; + children.push([encoder.encode('loop'), loop]); + } + const tree = this.construct(d.own === null ? this.refuser() : this.primitive(d.own), children); + this.built.set(id, tree); + return tree; + } + render(tree: WireTree): unknown { + const own = tree.own(); + return { + own: this.refusing.has(own) ? null : this.names.get(own) ?? 'unknown', + children: tree.children().map(([key, child]) => [hex(key), this.render(child)]), + }; + } + rebuild(tree: WireTree, where: string[], keep: string[][]): WireTree { + if (keep.some(path => isDeepStrictEqual(path, where))) return tree; + const parts = tree.decompose(); + return this.construct(parts.own, parts.children.map(([key, child]) => [key, this.rebuild(child, [...where, hex(key)], keep)])); + } + replaceAt(tree: WireTree, path: string[], f: (tree: WireTree) => WireTree): WireTree { + if (!path.length) return f(tree); + const parts = tree.decompose(); + if (!parts.children.some(([key]) => hex(key) === path[0])) throw new Error('edit path leaves the tree'); + return this.construct(parts.own, parts.children.map(([key, child]) => [key, hex(key) === path[0] ? this.replaceAt(child, path.slice(1), f) : child])); + } + copy(tree: WireTree): WireTree { + const own = tree.own(); + const fresh = this.refusing.has(own) ? this.refuser() : this.primitive(this.names.get(own)![0], true); + return this.construct(fresh, tree.children().map(([key, child]) => [key, this.copy(child)])); + } + edit(tree: WireTree, s: Step, build: (id: string) => WireTree): WireTree { + switch (s.op) { + case 'rebuild': return this.rebuild(tree, [], s.keep!); + case 'replace': return this.replaceAt(tree, s.path!, () => build(s.node!)); + case 'own': return this.replaceAt(tree, s.path!, node => { + const own = node.own(); + return this.construct(s.own === null ? this.refuser() : this.primitive(this.names.get(own)![0], true), node.children()); + }); + case 'substitute': return this.replaceAt(tree, s.path!, node => s.mode === 'copy' ? this.copy(node) : this.rebuild(node, [], [])); + case 'omit': case 'rename': case 'add': return this.replaceAt(tree, s.path!, node => { + const next: Child[] = node.children() + .filter(([key]) => !(s.op === 'omit' && hex(key) === s.key)) + .map(([key, child]) => [s.op === 'rename' && hex(key) === s.key ? bytes(s.to!) : key, child]); + if (s.op === 'add') next.push([bytes(s.key!), build(s.node!)]); + return this.construct(node.own(), next); + }); + default: throw new Error('unknown edit ' + s.op); + } + } + verify(message: Message): void { + this.unchanged &&= !!this.expected && isDeepStrictEqual(message.frame, this.expected.frame) && message.return === this.expected.return; + } +} + +/** Applies a selection chain with each node's own at, and checks it against selecting the concatenation. */ +function chain(h: Harness, start: WireTree, selections: string[][], fromRoot: boolean): WireTree | undefined { + let node: WireTree | undefined = start; + for (const selection of selections) { + node = node?.at(selection.map(bytes)); + if (!node) return undefined; + } + if (fromRoot && h.T.select(start, selections.flat().map(bytes)) !== node) h.trace.push(['selectionDiffers']); + return node; +} + +/** Derived sending: missing selection invokes nothing; a present node's refusal is its own. */ +function derived(h: Harness, base: WireTree | undefined, path: string[], message: Message): void { + if (!base) { h.trace.push(['missing']); return; } + const present = h.T.select(base, path.map(bytes)) !== undefined; + const before = h.trace.length; + h.expected = message; + try { + h.T.send(base, path.map(bytes), message); + if (!present) h.trace.push(['fabricated']); + } catch { + if (h.trace.length !== before) h.trace.push(['fallback']); + h.trace.push([present ? 'refused' : 'missing']); + } +} + +const refuseWire: AddressedWire = { send() { throw new Error('not a reply target'); } }; +const event = (data: string): Message => ({ frame: { version: 1, kind: 'event', data }, return: { wire: refuseWire } }); + +// ---- Local families: structure and the addressed bridge ---- +function local(T: Trees, inputs: Inputs, test: Case): unknown { + const h: Harness = new Harness(T, inputs, (self, name, message) => { + h.verify(message); + h.trace.push(['delivered', name, h.count(self)]); + }); + let root: WireTree; + try { root = h.build(test.root, test.fault, test.root); } catch { return { construction: 'refused' }; } + if (test.fault) return { construction: 'accepted' }; + let view: WireTree | undefined; + let sendOnly = true; + for (const [index, s] of test.steps.entries()) { + const message = event(`${test.id}:${index}`); + switch (s.op) { + case 'structure': { + const node = T.select(root, s.path!.map(bytes)); + h.trace.push(['structure', node ? h.render(node) : 'missing']); + break; + } + case 'send': { + const start = s.via === 'view' ? view : root; + derived(h, start && chain(h, start, s.selections ?? [], s.via !== 'view'), s.path!, message); + break; + } + case 'same': { + const [a, b] = s.paths!.map(path => T.select(root, path.map(bytes))?.own()); + h.trace.push(['same', a !== undefined && a === b]); + break; + } + case 'view': view = chain(h, root, s.selections!, true); break; + case 'bridge': case 'bridgeInvalid': { + const bridge = T.asAddressed(root); + sendOnly &&= typeof bridge.send === 'function' + && ['receive', 'close', 'own', 'children', 'at', 'decompose'].every(name => !(name in bridge)); + const before = h.trace.length; + h.expected = message; + try { bridge.send(s.op === 'bridgeInvalid' ? ['\ud800'] : s.path!, message); } catch { + if (h.trace.length !== before) h.trace.push(['fallback']); + h.trace.push(['refused']); + } + break; + } + default: root = h.edit(root, s, id => h.build(id)); + } + } + return test.family === 'bridge' + ? { trace: h.trace, sendOnly, unchanged: h.unchanged } + : { trace: h.trace, partsExact: h.partsExact, unchanged: h.unchanged }; +} + +// ---- Carrier family ---- +/** Test-only: addressless send access at one fixed addressed path (bitruntime#15). */ +const bind = (access: AddressedWire, path: Path): Wire => Object.freeze({ send: (message: Message) => access.send(path, message) }); + +/** Test-only: exact dispatcher routes for every node whose keys are all UTF-8, each bound to that node (bitruntime#15). */ +function serve(dispatcher: Dispatcher, tree: WireTree, prefix: string[], current?: () => WireTree): () => void { + if (current) { + // Unlawful on purpose: routes by the tree current at delivery, so a captured + // cancellation reaches whatever node replaced the one that admitted it. + const bridge = (path: Path, message: Message) => reference.asAddressed(current()).send(path.slice(prefix.length), message); + return dispatcher.registerPrefix(prefix, { message: bridge }); + } + const detach: (() => void)[] = []; + const visit = (node: WireTree, path: string[]): void => { + const own = node.own(); + detach.push(dispatcher.register([...prefix, ...path], { message: (_path, message) => own.send(message) })); + for (const [key, child] of node.children()) { + let segment: string; + try { segment = utf8.decode(key); } catch { continue; } + visit(child, [...path, segment]); + } + }; + visit(tree, []); + return () => { for (const release of detach) release(); }; +} + +async function carrier(T: Trees, inputs: Inputs, test: Case, retargeting = false): Promise { + const cleanups: (() => void)[] = []; + try { + const [near, first] = await connected(release => cleanups.push(release)); + let far = first; + if (test.relay) { + const [outgoing, target] = await connected(release => cleanups.push(release)); + cleanups.push(forward(first, outgoing)); + far = target; + } + let access: AddressedWire = near; + if (test.mount) { + const mounted = mount(new Map([['mounted', near]])); + cleanups.push(() => mounted.close(CODE_NORMAL, 'done')); + access = at(mounted, ['mounted']); + } + + // The far side: an instrumented tree served on a dispatcher that borrows the endpoint. + let hold = false; + let held: { message: Message; name: string } | undefined; + const signals = mailbox(); + const h: Harness = new Harness(T, inputs, (self, name, message) => { + const frame = message.frame; + h.unchanged &&= !!h.expected && frame.kind === h.expected.frame.kind + && (frame.kind !== 'request' || isDeepStrictEqual(frame.params, (h.expected.frame as { params?: unknown }).params)) + && !!message.return; + if (frame.kind === 'cancel') { + h.trace.push(['cancelled', name]); + signals.put('cancelled'); + return; + } + const count = h.count(self); + if (frame.kind === 'event') { h.trace.push(['delivered', name, count]); return; } + if (frame.kind !== 'request') throw new Error('unexpected frame'); + if (hold) { + hold = false; + held = { message, name }; + h.trace.push(['held', name, count]); + signals.put('held'); + return; + } + h.trace.push(['delivered', name, count]); + message.return!.wire.send([], { frame: { version: 1, kind: 'response', id: frame.id, result: name } }); + }); + let farTree = h.build(test.root); + let dispatcher: Dispatcher | undefined = createDispatcher(far); + const current = retargeting ? () => farTree : undefined; + let unserve = serve(dispatcher, farTree, inputs.servedAt, current); + const reserve = () => { unserve(); unserve = serve(dispatcher!, farTree, inputs.servedAt, current); }; + + // The near side: the same declared structure, each own Wire bound to its far + // carrier path. A carrier path names a far position, and addressed access + // cannot show that two positions share a node, so every position is bound. + const mirror = (id: string, path: string[]): WireTree => { + const d = h.declarations.get(id)!; + const children: Child[] = []; + for (const [key, child] of d.children) { + let segment: string; + try { segment = utf8.decode(bytes(key)); } catch { continue; } + children.push([bytes(key), mirror(child, [...path, segment])]); + } + return T.compose(bind(access, [...inputs.servedAt, ...path]), children); + }; + let nearTree = mirror(test.root, []); + const segments = (path: string[]) => path.map(key => utf8.decode(bytes(key))); + + let serial = 0; + const request = (label: string) => { + const replies = mailbox(); + const original: ReturnAddress = { wire: { send(path: Path, message: Message) { + if (path.length || message.frame.kind !== 'response') throw new Error('unexpected reply'); + replies.put(message); + } } }; + const message: Message = { frame: { version: 1, kind: 'request', id: `c:${++serial}`, params: label }, return: original }; + return { message, replies }; + }; + const outcome = async (replies: ReturnType['replies']) => { + const reply = (await replies.take()).frame as { error?: { code: string } }; + if (reply.error) h.trace.push(['error', reply.error.code]); + }; + let pending: ReturnType | undefined; + + for (const [index, s] of test.steps.entries()) { + const label = `${test.id}:${index}`; + switch (s.op) { + case 'send': case 'hold': case 'cancel': { + const base = chain(h, nearTree, s.selections ?? [], true); + const target = base && T.select(base, s.path!.map(bytes)); + if (!target) { h.trace.push(['missing']); break; } + if (s.op === 'cancel') { + const cancel: Message = { frame: { version: 1, kind: 'cancel', id: (pending!.message.frame as { id: string }).id }, return: pending!.message.return }; + h.expected = cancel; + T.send(base, s.path!.map(bytes), cancel); + await signals.take(); + break; + } + const call = request(label); + h.expected = call.message; + if (s.op === 'hold') hold = true; + try { T.send(base, s.path!.map(bytes), call.message); } catch { hold = false; h.trace.push(['refused']); break; } + if (s.op === 'hold') { pending = call; await signals.take(); } else await outcome(call.replies); + break; + } + case 'sendAddressed': { + const call = request(label); + h.expected = call.message; + try { access.send(s.path!, call.message); } catch { h.trace.push(['refused']); break; } + await outcome(call.replies); + break; + } + case 'release': { + held!.message.return!.wire.send([], { frame: { version: 1, kind: 'response', id: (held!.message.frame as { id: string }).id, result: held!.name } }); + const reply = (await pending!.replies.take()).frame as { result?: unknown }; + h.trace.push(['late', reply.result]); + break; + } + case 'structure': { + const node = T.select(farTree, s.path!.map(bytes)); + h.trace.push(['structure', node ? h.render(node) : 'missing']); + break; + } + case 'direct': derived(h, farTree, s.path!, event(label)); break; + case 'teardown': unserve(); dispatcher!.close(); dispatcher = undefined; break; + default: + if (s.side === 'far') { + farTree = h.edit(farTree, s, id => h.build(id)); + reserve(); + } else { + nearTree = h.edit(nearTree, s, id => mirror(id, segments(s.path!))); + } + } + } + + // The borrowed endpoint outlives every composition over it. + unserve(); + dispatcher?.close(); + const borrowed = mailbox(); + cleanups.push(far.receive({ message(path, message) { if (path.join('/') === 'borrowed') borrowed.put(message); } })); + let borrowedUsable = false; + try { + near.send(['borrowed'], event('borrowed')); + borrowedUsable = (await borrowed.take()).frame.kind === 'event'; + } catch { /* not usable */ } + return { trace: h.trace, unchanged: h.unchanged, borrowedUsable }; + } finally { + for (const cleanup of cleanups.reverse()) cleanup(); + } +} + +const [realization = '', scope, inputPath] = process.argv.slice(2); +const mutant = realization.startsWith('mutant:') ? realization.slice('mutant:'.length) : undefined; +if (!(['reference', 'production'].includes(realization) || (mutant && (mutant in mutants || mutant === 'retargeting-serve'))) + || !['local', 'carrier'].includes(scope ?? '') || !inputPath) { + throw new Error('Usage: wiretree.ts reference|production|mutant: local|carrier inputs.json'); +} +const T = realization === 'production' ? production : mutant && mutant in mutants ? mutants[mutant]! : reference; +const inputs = JSON.parse(readFileSync(inputPath, 'utf8')) as Inputs; +if (scope === 'carrier') kind(); +const output = []; +for (const test of inputs.cases) { + if ((scope === 'carrier') !== (test.family === 'carrier')) continue; + let observations: unknown; + try { + observations = scope === 'carrier' ? await carrier(T, inputs, test, mutant === 'retargeting-serve') : local(T, inputs, test); + } catch (error) { + if (!mutant) throw error; + observations = { failed: String(error) }; // A mutant may break the harness; that is a failed case too. + } + output.push({ id: test.id, observations }); +} +process.stdout.write(JSON.stringify(output) + '\n'); diff --git a/conformance/trees/README.md b/conformance/trees/README.md index ff1d239..5e2f5c3 100644 --- a/conformance/trees/README.md +++ b/conformance/trees/README.md @@ -17,6 +17,10 @@ construction is deliberately scoped test infrastructure; it is not a shipped production tree runtime, carrier implementation or proof of downstream adoption. bitruntime owns production construction and derived operators. +The [full-tree family](../wiretree/README.md) extends these observations with +independent cases for reconstruction, retained state, the addressed bridge and +composition across real carriers, run against released bitruntime. + The older [declared cases](../declared/README.md) exercise retained owner parts and addressed forwarding. Their pinned release names and observations remain historical evidence and do not substitute for these structural laws. diff --git a/conformance/wiretree/README.md b/conformance/wiretree/README.md new file mode 100644 index 0000000..02cad34 --- /dev/null +++ b/conformance/wiretree/README.md @@ -0,0 +1,145 @@ +# Full-tree composition across carriers + +These independent cases exercise the 0.3 contract of +[decision 0012](../../docs/decisions/0012-explicit-data-and-wire-trees.md) where +it matters to consumers: `WireTree = DeixisNode` built, selected, +decomposed and rebuilt locally, exposed through the unchanged `bitwire/1` +addressed carrier, and composed across real carriers. They are separately +identified from the historical [declared cases](../declared/README.md), which +stay byte-identical with their gap ledgers. +[`disposition.json`](disposition.json) maps every historical observation onto +these cases or onto an explicit historical addressed limitation. + +Run them with the released-runtime gate: + +```console +node scripts/conformance-runtime.mjs +``` + +The oracle is [`cases.json`](cases.json). Its expectations were written from the +contract, decision 0012 and the `bitwire/1` profile. They were not recorded from +an implementation. The runner gives each driver its inputs with every expectation +withheld and keys already converted to hex. It compares complete observations +itself: missing, extra, duplicate and mismatched rows fail. +`node --test scripts/wiretree.test.mjs`, part of `node scripts/check.mjs`, checks +the notation, the withheld inputs, the comparison and the disposition's +coverage. It needs no network. + +## Three kinds of evidence + +| Family | What it establishes | How it runs | +| --- | --- | --- | +| `structure`, 18 cases | Own capability identity, exact byte keys (empty, binary, the literal `a/b`, both spellings of é), complete children, missing versus a present node whose own refuses, selection chains, both reconstruction directions, complete cuts, retained and reset primitive state, substitution, alteration, and construction refusing duplicate keys, missing children and cycles | No carrier. Every construction also checks that neither the caller's input nor returned parts can change the tree. | +| `bridge`, 1 case | The explicit mapping to the unchanged carrier surface: the exact UTF-8 image, refusal of ill-formed segments before any primitive, unreachable binary keys, send-only access | `AsAddressed`/`asAddressed`, no carrier | +| `carrier`, 7 cases | A far tree served on an endpoint and a near tree whose own Wires are bound to carrier paths: routing, reconstruction on either side, a relay through `Forward`, access through an addressed `Mount`, a late reply and a requester's cancellation after the far node was replaced, teardown | On the local pair, and on real WebSockets with the client sending and with the server sending | + +Two realizations run every case in Go and TypeScript. **production** uses +bitruntime's released `Compose`/`compose`, `Select`/`select`, `Send`/`send` and +`AsAddressed`/`asAddressed`. **reference** is a test-only interpreter that shows +the oracle can be met. It is never evidence about a runtime. Carriers, +dispatchers, `Forward`, `Mount` and `At` are always bitruntime's. + +## The carrier model + +The far side serves its tree through a bitruntime dispatcher that borrows the +endpoint. It registers an exact route for every node whose keys are UTF-8, and +each route is bound to that node's own Wire. The near side declares the same +structure. Each of its own Wires sends at the corresponding far path, so the +near tree's `own`, `children`, selection and reconstruction are local. The +carrier sees only paths. + +Four observations follow, and the cases state each one: + +- **The served tree needs a nonempty prefix.** `bitwire/1` refuses a request at + a peer root's empty path. The far tree is served under `servedAt` (`["t"]`), + and an addressed request at `[]` is refused at admission. +- **A carrier path names a position, not a node.** Addressed access cannot + reveal that two far positions share a node. The near tree therefore binds each + position separately. When the far side replaces one of two positions that + shared a node, the other position keeps reaching the original node. +- **Missing and refusing stay distinct, as profile outcomes.** A path absent + from the near tree is missing locally and sends nothing. A far path with no + node answers `method_not_found`. A far node whose own refuses answers + `internal`. Both codes come from the + [pinned profile](https://github.com/Bitspark/nightseam/blob/5cc9723a24646c40ed1861f892b2b23eb6d785d7/docs/wire/profile.md). + A binary key outside the UTF-8 image cannot be named over the carrier at all. +- **Captured cancellation survives replacement.** The dispatcher captures each + request's traversal on its invocation. After the far node that admitted a + request is replaced and re-served, the requester's cancel frame, sent through + the same near tree position, reaches the original node and not its + replacement. A late reply from the original node reaches the original return + capability. + +Two adapters in the drivers are **test-only**. bitruntime v0.2.0 has no public +facility for either, and +[bitruntime#15](https://github.com/Bitspark/bitruntime/issues/15) tracks them: + +- `bind`: an addressless `Wire` that sends at one fixed `AddressedWire` path; +- `serve`: exact dispatcher registration of a tree's UTF-8 nodes, each bound to + its node. + +Everything the adapters call is public bitruntime API. The carrier results are +therefore evidence that bitruntime's carriers, dispatcher and trees compose +lawfully. They are not evidence of a shipped serving or binding API. + +## Unlawful realizations are rejected + +The TypeScript driver also runs deliberately unlawful realizations. The runner +requires each one to fail at least one case: + +| Realization | Violation | Rejected by, among others | +| --- | --- | --- | +| `fallback` | A missing descendant is sent to its nearest ancestor | `missing-never-falls-back` | +| `wrapping-own` | The own capability is replaced by a forwarding wrapper | every identity and reconstruction case | +| `normalizing` | UTF-8 keys are NFC-normalized | construction of the two spellings of é | +| `fabricating` | A missing path selects a fabricated refusing node | `refusing-versus-missing` | +| `latin1-bridge` | The bridge names byte key `ff` with `"ÿ"` | `bridge-exact-utf8-image` | +| `incomplete-children` | `children()` omits the empty key | every `structure` render | +| `retargeting-serve` | The far side routes by the tree current at delivery | `carrier-cancel-across-replacement` | + +## Disposition of the declared cases + +[`disposition.json`](disposition.json) pins the historical fixture and both gap +ledgers by SHA-256. A changed historical file fails `scripts/check.mjs` until +someone revisits the disposition. The mapping: + +- **37 of the 39 cases** have current `structure`, `bridge` or `carrier` + counterparts. Several meanings are sharpened: a path that the addressed + interpretation answered with a refusal is now *missing*, while a present node + whose own refuses stays *refused*. +- **10 cases** keep a historical addressed part. Two of them have nothing else: + `guard-around-composite` and `guarded-child-complete-access`. The reasons, + recorded as limitations: + - *suffix delivery* (6 cases): an opaque child received the rest of the path. + In a full tree that path is missing, and suffix delivery survives only as + AddressedWire prefix binding. + - *path-observing interception* (4 cases): guards saw descendant paths, but an + own Wire never sees a path. + - *addressed views* (1 case): at(guard, [k]) produced an addressed view that + repeated the guard's check when composed again. +- **The 19 recorded gaps:** + - The origin-bearing construction gap (16 cases) and the conflicting and + missing-child gaps are met structurally by public `Compose`/`compose`. + - Invalid segments changed meaning: no tree key is invalid, and the bridge + refuses ill-formed segments. + - The ledger entries stay accurate about the addressed `Mount`, which is a + child-only routing operator, not tree construction. +- **The two recorded limitations:** + - Endpoint-typed children are dissolved: tree children are nodes with a + send-only own. + - Owner-retained parts are superseded by public decomposition. A caller that + must not see structure receives the bridge instead. + +## Limits + +Only Go and TypeScript run. The other six languages have declarations, not +runtimes. The schedules are serial, and the carriers are connected, ordered +and fault-free. Concurrency, faults, backpressure and closure codes belong to +the carrier contract +([#54](https://github.com/Bitspark/bitwire/issues/54)). Invocation retirement +is [#20](https://github.com/Bitspark/bitwire/issues/20), and received-context +evidence is [#55](https://github.com/Bitspark/bitwire/issues/55). Remote +structural discovery is not specified. A near tree is declared, never inferred +from an opaque router. Consumer acceptance belongs to +[bitsystem3#11](https://github.com/Bitspark/bitsystem3/issues/11) and +[bittree#53](https://github.com/Bitspark/bittree/issues/53). diff --git a/conformance/wiretree/cases.json b/conformance/wiretree/cases.json new file mode 100644 index 0000000..c3128b5 --- /dev/null +++ b/conformance/wiretree/cases.json @@ -0,0 +1,790 @@ +{ + "schemaVersion": 1, + "contract": "bitwire v0.3.0, decision 0012: Wire.send(message), WireTree = DeixisNode, AddressedWire", + "profile": "bitwire/1 = nightseam v0.6.0 at 5cc9723a24646c40ed1861f892b2b23eb6d785d7; error codes from its docs/wire/profile.md", + "notation": { + "key": "A JSON string is the exact UTF-8 encoding of that string; {\"hex\": \"...\"} is exact bytes. Keys are never normalized.", + "addressedPath": "An array of Unicode-scalar strings, the unchanged bitwire/1 carrier path.", + "own": "A name creates one stateful instrumented primitive per case, shared by every node declaring it; null is a primitive that refuses every send.", + "render": "{\"own\": [name, instance] | null, \"children\": [[key, render], ...]}; children compare as a set of exact keys. {\"$render\": name} refers to renders.", + "same": "Whether the two selected nodes hold the identical own capability, which is what shared primitive state depends on.", + "servedAt": "Carrier cases serve the far tree under this nonempty addressed prefix, because bitwire/1 refuses a request at a peer root's empty path. A tree path p is carrier path servedAt ++ utf8(p)." + }, + "servedAt": ["t"], + "declarations": [ + { "id": "root", "own": "root", "children": [ + ["", "blank"], ["a", "branch"], ["a/b", "slash"], ["alias", "leaf"], ["empty", "empty"], + ["é", "acute"], ["é", "combining"], [{ "hex": "ff" }, "binary"] + ] }, + { "id": "rroot", "own": null, "children": [ + ["", "rblank"], ["a", "rbranch"], ["a/b", "slash"], ["alias", "leaf"], ["empty", "empty"], + ["é", "acute"], ["é", "combining"] + ] }, + { "id": "blank", "own": "blank", "children": [["", "blankLeaf"]] }, + { "id": "rblank", "own": null, "children": [["", "blankLeaf"]] }, + { "id": "branch", "own": "branch", "children": [["b", "leaf"]] }, + { "id": "rbranch", "own": null, "children": [["b", "leaf"]] }, + { "id": "empty", "own": null }, + { "id": "leaf", "own": "leaf" }, + { "id": "blankLeaf", "own": "blank-leaf" }, + { "id": "slash", "own": "slash" }, + { "id": "acute", "own": "acute" }, + { "id": "combining", "own": "combining" }, + { "id": "binary", "own": "binary" }, + { "id": "replacement", "own": "replacement" }, + { "id": "extra", "own": "extra" } + ], + "renders": { + "root": { "own": ["root", 1], "children": [ + ["", { "own": ["blank", 1], "children": [["", { "own": ["blank-leaf", 1], "children": [] }]] }], + ["a", { "own": ["branch", 1], "children": [["b", { "own": ["leaf", 1], "children": [] }]] }], + ["a/b", { "own": ["slash", 1], "children": [] }], + ["alias", { "own": ["leaf", 1], "children": [] }], + ["empty", { "own": null, "children": [] }], + ["é", { "own": ["acute", 1], "children": [] }], + ["é", { "own": ["combining", 1], "children": [] }], + [{ "hex": "ff" }, { "own": ["binary", 1], "children": [] }] + ] }, + "rroot": { "own": null, "children": [ + ["", { "own": null, "children": [["", { "own": ["blank-leaf", 1], "children": [] }]] }], + ["a", { "own": null, "children": [["b", { "own": ["leaf", 1], "children": [] }]] }], + ["a/b", { "own": ["slash", 1], "children": [] }], + ["alias", { "own": ["leaf", 1], "children": [] }], + ["empty", { "own": null, "children": [] }], + ["é", { "own": ["acute", 1], "children": [] }], + ["é", { "own": ["combining", 1], "children": [] }] + ] } + }, + "cases": [ + { + "id": "own-and-descendants", + "family": "structure", + "root": "root", + "steps": [ + { "op": "structure", "path": [] }, + { "op": "send", "path": [] }, + { "op": "send", "path": ["a"] }, + { "op": "send", "path": ["a", "b"] }, + { "op": "send", "path": ["alias"] }, + { "op": "send", "path": [""] }, + { "op": "send", "path": ["", ""] }, + { "op": "send", "path": ["a/b"] }, + { "op": "send", "path": ["é"] }, + { "op": "send", "path": ["é"] }, + { "op": "send", "path": [{ "hex": "ff" }] }, + { "op": "send", "path": ["empty"] }, + { "op": "send", "path": ["alias", "x"] }, + { "op": "send", "path": ["a", "b", "x", "y"] }, + { "op": "send", "path": ["ÿ"] } + ], + "expected": { + "trace": [ + ["structure", { "$render": "root" }], + ["delivered", "root", 1], + ["delivered", "branch", 1], + ["delivered", "leaf", 1], + ["delivered", "leaf", 2], + ["delivered", "blank", 1], + ["delivered", "blank-leaf", 1], + ["delivered", "slash", 1], + ["delivered", "acute", 1], + ["delivered", "combining", 1], + ["delivered", "binary", 1], + ["refused"], + ["missing"], + ["missing"], + ["missing"] + ], + "partsExact": true, + "unchanged": true + } + }, + { + "id": "refusing-own-and-descendants", + "family": "structure", + "root": "rroot", + "steps": [ + { "op": "structure", "path": [] }, + { "op": "send", "path": [] }, + { "op": "send", "path": ["a"] }, + { "op": "send", "path": ["a", "b"] }, + { "op": "send", "path": ["alias"] }, + { "op": "send", "path": [""] }, + { "op": "send", "path": ["", ""] }, + { "op": "send", "path": ["a/b"] }, + { "op": "send", "path": ["é"] }, + { "op": "send", "path": ["é"] }, + { "op": "send", "path": ["empty"] }, + { "op": "send", "path": ["missing"] }, + { "op": "send", "path": ["alias", "x"] }, + { "op": "send", "path": ["a", "b", "x", "y"] } + ], + "expected": { + "trace": [ + ["structure", { "$render": "rroot" }], + ["refused"], + ["refused"], + ["delivered", "leaf", 1], + ["delivered", "leaf", 2], + ["refused"], + ["delivered", "blank-leaf", 1], + ["delivered", "slash", 1], + ["delivered", "acute", 1], + ["delivered", "combining", 1], + ["refused"], + ["missing"], + ["missing"], + ["missing"] + ], + "partsExact": true, + "unchanged": true + } + }, + { + "id": "nested-selection-agrees", + "family": "structure", + "root": "root", + "steps": [ + { "op": "send", "path": [], "selections": [[]] }, + { "op": "send", "path": [], "selections": [[], ["a"], [], ["b"]] }, + { "op": "send", "path": ["b"], "selections": [["a"]] }, + { "op": "send", "path": [], "selections": [["a", "b"]] }, + { "op": "send", "path": [], "selections": [["a"]] }, + { "op": "send", "path": [""], "selections": [[""]] }, + { "op": "send", "path": [], "selections": [[""]] }, + { "op": "send", "path": ["x"], "selections": [["alias"]] }, + { "op": "send", "path": [], "selections": [["alias"], ["x"], []] }, + { "op": "send", "path": [], "selections": [["missing"]] }, + { "op": "send", "path": [], "selections": [["a"], ["missing"]] }, + { "op": "send", "path": [], "selections": [["empty"], []] }, + { "op": "send", "path": [], "selections": [[{ "hex": "ff" }]] }, + { "op": "same", "paths": [["a", "b"], ["alias"]] } + ], + "expected": { + "trace": [ + ["delivered", "root", 1], + ["delivered", "leaf", 1], + ["delivered", "leaf", 2], + ["delivered", "leaf", 3], + ["delivered", "branch", 1], + ["delivered", "blank-leaf", 1], + ["delivered", "blank", 1], + ["missing"], + ["missing"], + ["missing"], + ["missing"], + ["refused"], + ["delivered", "binary", 1], + ["same", true] + ], + "partsExact": true, + "unchanged": true + } + }, + { + "id": "missing-never-falls-back", + "family": "structure", + "root": "root", + "steps": [ + { "op": "send", "path": ["missing"] }, + { "op": "send", "path": ["a", "missing"] }, + { "op": "send", "path": ["empty"] }, + { "op": "send", "path": ["empty", "x"] }, + { "op": "send", "path": ["", "missing"] }, + { "op": "send", "path": [], "selections": [["missing"]] }, + { "op": "send", "path": [], "selections": [["a"], ["missing"]] }, + { "op": "send", "path": [] }, + { "op": "send", "path": ["a"] } + ], + "expected": { + "trace": [ + ["missing"], + ["missing"], + ["refused"], + ["missing"], + ["missing"], + ["missing"], + ["missing"], + ["delivered", "root", 1], + ["delivered", "branch", 1] + ], + "partsExact": true, + "unchanged": true + } + }, + { + "id": "refusing-versus-missing", + "family": "structure", + "root": "root", + "steps": [ + { "op": "structure", "path": ["empty"] }, + { "op": "structure", "path": ["missing"] }, + { "op": "send", "path": ["empty"] }, + { "op": "send", "path": ["missing"] }, + { "op": "structure", "path": [] } + ], + "expected": { + "trace": [ + ["structure", { "own": null, "children": [] }], + ["structure", "missing"], + ["refused"], + ["missing"], + ["structure", { "$render": "root" }] + ], + "partsExact": true, + "unchanged": true + } + }, + { + "id": "root-cut-reconstruction", + "family": "structure", + "root": "root", + "steps": [ + { "op": "send", "path": ["a", "b"] }, + { "op": "rebuild", "keep": [[""], ["a"], ["a/b"], ["alias"], ["empty"], ["é"], ["é"], [{ "hex": "ff" }]] }, + { "op": "send", "path": ["alias"] }, + { "op": "send", "path": [] }, + { "op": "send", "path": ["a"] }, + { "op": "structure", "path": [] } + ], + "expected": { + "trace": [ + ["delivered", "leaf", 1], + ["delivered", "leaf", 2], + ["delivered", "root", 1], + ["delivered", "branch", 1], + ["structure", { "$render": "root" }] + ], + "partsExact": true, + "unchanged": true + } + }, + { + "id": "complete-cuts-agree", + "family": "structure", + "root": "root", + "steps": [ + { "op": "send", "path": ["a", "b"] }, + { "op": "rebuild", "keep": [] }, + { "op": "send", "path": [] }, + { "op": "rebuild", "keep": [["a"]] }, + { "op": "send", "path": ["alias"] }, + { "op": "rebuild", "keep": [[""], ["empty"]] }, + { "op": "send", "path": ["", ""] }, + { "op": "rebuild", "keep": [[]] }, + { "op": "send", "path": ["a"] }, + { "op": "rebuild", "keep": [["a", "b"]] }, + { "op": "send", "path": ["a", "b"] }, + { "op": "send", "path": [{ "hex": "ff" }] }, + { "op": "same", "paths": [["a", "b"], ["alias"]] }, + { "op": "structure", "path": [] } + ], + "expected": { + "trace": [ + ["delivered", "leaf", 1], + ["delivered", "root", 1], + ["delivered", "leaf", 2], + ["delivered", "blank-leaf", 1], + ["delivered", "branch", 1], + ["delivered", "leaf", 3], + ["delivered", "binary", 1], + ["same", true], + ["structure", { "$render": "root" }] + ], + "partsExact": true, + "unchanged": true + } + }, + { + "id": "refusing-complete-cuts-agree", + "family": "structure", + "root": "rroot", + "steps": [ + { "op": "send", "path": ["a", "b"] }, + { "op": "rebuild", "keep": [] }, + { "op": "send", "path": ["alias"] }, + { "op": "rebuild", "keep": [["a"]] }, + { "op": "send", "path": ["", ""] }, + { "op": "rebuild", "keep": [[""], ["empty"]] }, + { "op": "send", "path": ["a", "b"] }, + { "op": "rebuild", "keep": [[]] }, + { "op": "send", "path": [] }, + { "op": "structure", "path": [] } + ], + "expected": { + "trace": [ + ["delivered", "leaf", 1], + ["delivered", "leaf", 2], + ["delivered", "blank-leaf", 1], + ["delivered", "leaf", 3], + ["refused"], + ["structure", { "$render": "rroot" }] + ], + "partsExact": true, + "unchanged": true + } + }, + { + "id": "children-alone-lose-own", + "family": "structure", + "root": "root", + "steps": [ + { "op": "send", "path": [] }, + { "op": "own", "path": [], "own": null }, + { "op": "send", "path": [] }, + { "op": "send", "path": ["alias"] }, + { "op": "structure", "path": ["a"] } + ], + "expected": { + "trace": [ + ["delivered", "root", 1], + ["refused"], + ["delivered", "leaf", 1], + ["structure", { "own": ["branch", 1], "children": [["b", { "own": ["leaf", 1], "children": [] }]] }] + ], + "partsExact": true, + "unchanged": true + } + }, + { + "id": "fresh-own-resets-state", + "family": "structure", + "root": "root", + "steps": [ + { "op": "send", "path": [] }, + { "op": "send", "path": [] }, + { "op": "own", "path": [], "own": "fresh" }, + { "op": "send", "path": [] }, + { "op": "send", "path": ["a", "b"] }, + { "op": "structure", "path": ["a"] } + ], + "expected": { + "trace": [ + ["delivered", "root", 1], + ["delivered", "root", 2], + ["delivered", "root", 1], + ["delivered", "leaf", 1], + ["structure", { "own": ["branch", 1], "children": [["b", { "own": ["leaf", 1], "children": [] }]] }] + ], + "partsExact": true, + "unchanged": true + } + }, + { + "id": "shared-child-state", + "family": "structure", + "root": "root", + "steps": [ + { "op": "send", "path": ["a", "b"] }, + { "op": "send", "path": ["alias"] }, + { "op": "rebuild", "keep": [] }, + { "op": "send", "path": ["a", "b"] }, + { "op": "send", "path": ["alias"] }, + { "op": "same", "paths": [["a", "b"], ["alias"]] }, + { "op": "structure", "path": [] } + ], + "expected": { + "trace": [ + ["delivered", "leaf", 1], + ["delivered", "leaf", 2], + ["delivered", "leaf", 3], + ["delivered", "leaf", 4], + ["same", true], + ["structure", { "$render": "root" }] + ], + "partsExact": true, + "unchanged": true + } + }, + { + "id": "equivalent-substitution", + "family": "structure", + "root": "root", + "steps": [ + { "op": "send", "path": ["a", "b"] }, + { "op": "substitute", "path": ["a"], "mode": "parts" }, + { "op": "send", "path": ["a", "b"] }, + { "op": "send", "path": ["alias"] }, + { "op": "send", "path": ["a"] }, + { "op": "same", "paths": [["a", "b"], ["alias"]] }, + { "op": "structure", "path": ["a"] } + ], + "expected": { + "trace": [ + ["delivered", "leaf", 1], + ["delivered", "leaf", 2], + ["delivered", "leaf", 3], + ["delivered", "branch", 1], + ["same", true], + ["structure", { "own": ["branch", 1], "children": [["b", { "own": ["leaf", 1], "children": [] }]] }] + ], + "partsExact": true, + "unchanged": true + } + }, + { + "id": "copied-subtree-breaks-sharing", + "family": "structure", + "root": "root", + "steps": [ + { "op": "send", "path": ["alias"] }, + { "op": "send", "path": ["a"] }, + { "op": "substitute", "path": ["a"], "mode": "copy" }, + { "op": "send", "path": ["a", "b"] }, + { "op": "send", "path": ["a"] }, + { "op": "send", "path": ["alias"] }, + { "op": "same", "paths": [["a", "b"], ["alias"]] }, + { "op": "structure", "path": ["a"] } + ], + "expected": { + "trace": [ + ["delivered", "leaf", 1], + ["delivered", "branch", 1], + ["delivered", "leaf", 1], + ["delivered", "branch", 1], + ["delivered", "leaf", 2], + ["same", false], + ["structure", { "own": ["branch", 2], "children": [["b", { "own": ["leaf", 2], "children": [] }]] }] + ], + "partsExact": true, + "unchanged": true + } + }, + { + "id": "altered-children-detected", + "family": "structure", + "root": "root", + "steps": [ + { "op": "send", "path": ["a", "b"] }, + { "op": "omit", "path": [], "key": "a" }, + { "op": "send", "path": ["a", "b"] }, + { "op": "send", "path": ["alias"] }, + { "op": "rename", "path": [], "key": "alias", "to": "renamed" }, + { "op": "send", "path": ["alias"] }, + { "op": "send", "path": ["renamed"] }, + { "op": "add", "path": [], "key": "extra", "node": "extra" }, + { "op": "send", "path": ["extra"] }, + { "op": "send", "path": [] }, + { "op": "structure", "path": [] } + ], + "expected": { + "trace": [ + ["delivered", "leaf", 1], + ["missing"], + ["delivered", "leaf", 2], + ["missing"], + ["delivered", "leaf", 3], + ["delivered", "extra", 1], + ["delivered", "root", 1], + ["structure", { "own": ["root", 1], "children": [ + ["", { "own": ["blank", 1], "children": [["", { "own": ["blank-leaf", 1], "children": [] }]] }], + ["a/b", { "own": ["slash", 1], "children": [] }], + ["empty", { "own": null, "children": [] }], + ["extra", { "own": ["extra", 1], "children": [] }], + ["renamed", { "own": ["leaf", 1], "children": [] }], + ["é", { "own": ["acute", 1], "children": [] }], + ["é", { "own": ["combining", 1], "children": [] }], + [{ "hex": "ff" }, { "own": ["binary", 1], "children": [] }] + ] }] + ], + "partsExact": true, + "unchanged": true + } + }, + { + "id": "selected-subtree-keeps-identity", + "family": "structure", + "root": "root", + "steps": [ + { "op": "view", "selections": [["a"], ["b"]] }, + { "op": "replace", "path": ["a", "b"], "node": "replacement" }, + { "op": "send", "path": [], "via": "view" }, + { "op": "send", "path": ["a", "b"] }, + { "op": "send", "path": ["alias"] }, + { "op": "structure", "path": ["a"] } + ], + "expected": { + "trace": [ + ["delivered", "leaf", 1], + ["delivered", "replacement", 1], + ["delivered", "leaf", 2], + ["structure", { "own": ["branch", 1], "children": [["b", { "own": ["replacement", 1], "children": [] }]] }] + ], + "partsExact": true, + "unchanged": true + } + }, + { + "id": "duplicate-keys-refused", + "family": "structure", + "root": "root", + "fault": "duplicate", + "steps": [{ "op": "send", "path": ["a"] }], + "expected": { "construction": "refused" } + }, + { + "id": "missing-child-refused", + "family": "structure", + "root": "root", + "fault": "missingChild", + "steps": [{ "op": "send", "path": ["hole"] }], + "expected": { "construction": "refused" } + }, + { + "id": "cycle-refused", + "family": "structure", + "root": "root", + "fault": "cycle", + "steps": [], + "expected": { "construction": "refused" } + }, + { + "id": "bridge-exact-utf8-image", + "family": "bridge", + "root": "root", + "steps": [ + { "op": "bridge", "path": [] }, + { "op": "bridge", "path": ["a", "b"] }, + { "op": "bridge", "path": [""] }, + { "op": "bridge", "path": ["", ""] }, + { "op": "bridge", "path": ["a/b"] }, + { "op": "bridge", "path": ["é"] }, + { "op": "bridge", "path": ["é"] }, + { "op": "bridge", "path": ["ÿ"] }, + { "op": "bridge", "path": ["empty"] }, + { "op": "bridge", "path": ["missing"] }, + { "op": "bridge", "path": ["alias", "x"] }, + { "op": "bridgeInvalid" }, + { "op": "bridge", "path": [] } + ], + "expected": { + "trace": [ + ["delivered", "root", 1], + ["delivered", "leaf", 1], + ["delivered", "blank", 1], + ["delivered", "blank-leaf", 1], + ["delivered", "slash", 1], + ["delivered", "acute", 1], + ["delivered", "combining", 1], + ["refused"], + ["refused"], + ["refused"], + ["refused"], + ["refused"], + ["delivered", "root", 2] + ], + "sendOnly": true, + "unchanged": true + } + }, + { + "id": "carrier-routing", + "family": "carrier", + "root": "root", + "steps": [ + { "op": "send", "path": [] }, + { "op": "send", "path": ["a"] }, + { "op": "send", "path": ["a", "b"] }, + { "op": "send", "path": ["alias"] }, + { "op": "send", "path": [""] }, + { "op": "send", "path": ["", ""] }, + { "op": "send", "path": ["a/b"] }, + { "op": "send", "path": ["é"] }, + { "op": "send", "path": ["é"] }, + { "op": "send", "path": ["empty"] }, + { "op": "send", "path": [{ "hex": "ff" }] }, + { "op": "send", "path": ["alias", "x"] }, + { "op": "sendAddressed", "path": ["t", "missing"] }, + { "op": "sendAddressed", "path": ["t", "alias", "x"] }, + { "op": "sendAddressed", "path": ["t", "ÿ"] }, + { "op": "sendAddressed", "path": ["missing"] }, + { "op": "sendAddressed", "path": [] }, + { "op": "sendAddressed", "path": ["t", "a", "b"] }, + { "op": "structure", "path": [] } + ], + "expected": { + "trace": [ + ["delivered", "root", 1], + ["delivered", "branch", 1], + ["delivered", "leaf", 1], + ["delivered", "leaf", 2], + ["delivered", "blank", 1], + ["delivered", "blank-leaf", 1], + ["delivered", "slash", 1], + ["delivered", "acute", 1], + ["delivered", "combining", 1], + ["error", "internal"], + ["missing"], + ["missing"], + ["error", "method_not_found"], + ["error", "method_not_found"], + ["error", "method_not_found"], + ["error", "method_not_found"], + ["refused"], + ["delivered", "leaf", 3], + ["structure", { "$render": "root" }] + ], + "unchanged": true, + "borrowedUsable": true + } + }, + { + "id": "carrier-reconstruction", + "family": "carrier", + "root": "root", + "steps": [ + { "op": "send", "path": ["a", "b"] }, + { "op": "rebuild", "side": "near", "keep": [] }, + { "op": "send", "path": [], "selections": [["a"], ["b"]] }, + { "op": "rebuild", "side": "far", "keep": [] }, + { "op": "send", "path": [] }, + { "op": "send", "path": ["alias"] }, + { "op": "rebuild", "side": "near", "keep": [["a"]] }, + { "op": "rebuild", "side": "far", "keep": [["a"]] }, + { "op": "send", "path": ["", ""] }, + { "op": "structure", "path": [] } + ], + "expected": { + "trace": [ + ["delivered", "leaf", 1], + ["delivered", "leaf", 2], + ["delivered", "root", 1], + ["delivered", "leaf", 3], + ["delivered", "blank-leaf", 1], + ["structure", { "$render": "root" }] + ], + "unchanged": true, + "borrowedUsable": true + } + }, + { + "id": "carrier-relay-reconstruction", + "family": "carrier", + "root": "root", + "relay": true, + "steps": [ + { "op": "send", "path": ["a", "b"] }, + { "op": "rebuild", "side": "near", "keep": [] }, + { "op": "rebuild", "side": "far", "keep": [] }, + { "op": "send", "path": [], "selections": [["a"], ["b"]] }, + { "op": "send", "path": [] }, + { "op": "send", "path": ["empty"] }, + { "op": "sendAddressed", "path": ["t", "missing"] } + ], + "expected": { + "trace": [ + ["delivered", "leaf", 1], + ["delivered", "leaf", 2], + ["delivered", "root", 1], + ["error", "internal"], + ["error", "method_not_found"] + ], + "unchanged": true, + "borrowedUsable": true + } + }, + { + "id": "carrier-mount-reconstruction", + "family": "carrier", + "root": "rroot", + "mount": true, + "steps": [ + { "op": "send", "path": ["a", "b"] }, + { "op": "rebuild", "side": "near", "keep": [] }, + { "op": "rebuild", "side": "far", "keep": [] }, + { "op": "send", "path": [], "selections": [["a"], ["b"]] }, + { "op": "send", "path": ["", ""] }, + { "op": "send", "path": [] } + ], + "expected": { + "trace": [ + ["delivered", "leaf", 1], + ["delivered", "leaf", 2], + ["delivered", "blank-leaf", 1], + ["error", "internal"] + ], + "unchanged": true, + "borrowedUsable": true + } + }, + { + "id": "carrier-late-reply-across-replacement", + "family": "carrier", + "root": "root", + "steps": [ + { "op": "hold", "path": ["a", "b"] }, + { "op": "rebuild", "side": "near", "keep": [] }, + { "op": "rebuild", "side": "far", "keep": [] }, + { "op": "replace", "side": "far", "path": ["a", "b"], "node": "replacement" }, + { "op": "replace", "side": "far", "path": ["alias"], "node": "replacement" }, + { "op": "replace", "side": "near", "path": ["a", "b"], "node": "replacement" }, + { "op": "replace", "side": "near", "path": ["alias"], "node": "replacement" }, + { "op": "send", "path": ["alias"] }, + { "op": "send", "path": ["a", "b"] }, + { "op": "release" }, + { "op": "send", "path": ["a"] } + ], + "expected": { + "trace": [ + ["held", "leaf", 1], + ["delivered", "replacement", 1], + ["delivered", "replacement", 2], + ["late", "leaf"], + ["delivered", "branch", 1] + ], + "unchanged": true, + "borrowedUsable": true + } + }, + { + "id": "carrier-cancel-across-replacement", + "family": "carrier", + "root": "root", + "steps": [ + { "op": "hold", "path": ["a", "b"] }, + { "op": "rebuild", "side": "far", "keep": [] }, + { "op": "replace", "side": "far", "path": ["a", "b"], "node": "replacement" }, + { "op": "replace", "side": "near", "path": ["a", "b"], "node": "replacement" }, + { "op": "cancel", "path": ["a", "b"] }, + { "op": "send", "path": ["a", "b"] }, + { "op": "send", "path": ["alias"] } + ], + "expected": { + "trace": [ + ["held", "leaf", 1], + ["cancelled", "leaf"], + ["delivered", "replacement", 1], + ["delivered", "leaf", 2] + ], + "unchanged": true, + "borrowedUsable": true + } + }, + { + "id": "carrier-teardown-keeps-borrowed", + "family": "carrier", + "root": "root", + "steps": [ + { "op": "send", "path": ["a", "b"] }, + { "op": "teardown" }, + { "op": "direct", "path": ["a", "b"] }, + { "op": "direct", "path": ["", ""] }, + { "op": "direct", "path": ["missing"] }, + { "op": "direct", "path": ["empty"] }, + { "op": "send", "path": ["a", "b"] } + ], + "expected": { + "trace": [ + ["delivered", "leaf", 1], + ["delivered", "leaf", 2], + ["delivered", "blank-leaf", 1], + ["missing"], + ["refused"], + ["error", "method_not_found"] + ], + "unchanged": true, + "borrowedUsable": true + } + } + ] +} diff --git a/conformance/wiretree/disposition.json b/conformance/wiretree/disposition.json new file mode 100644 index 0000000..4a1e888 --- /dev/null +++ b/conformance/wiretree/disposition.json @@ -0,0 +1,164 @@ +{ + "schemaVersion": 1, + "subject": "Disposition of decision 0006's 39 declared-access cases, their 19 recorded production gaps and two limitations under decision 0012. The historical files are unchanged; their digests pin what was disposed.", + "historical": { + "conformance/declared/cases.json": "2c3ae3605581e30749d6c0225e8619bd797887be1d540499fb9fb6904c361dd6", + "conformance/declared/production-gaps.json": "8c7262e7b72a9508d3d90d9b328a85749764f587bb9cd90f6ad67440c5ccd1f6", + "conformance/runtime/production-gaps.json": "b8becd0099bec9c3e283a7d7dc2e3951e6d3f52e0543568bfa5273e2a4c6eea4" + }, + "kinds": { + "structural": "A requirement of the full tree itself: construction, selection, own capability identity, exact keys, missing versus refusing nodes, decomposition and reconstruction. Evidence runs with no carrier.", + "bridge": "A requirement of the explicit mapping from a tree to the unchanged addressed carrier surface (AsAddressed/asAddressed): the exact UTF-8 image, refusal before any primitive, send-only access.", + "carrier": "A requirement of full trees composed across bitruntime carriers: a tree served on an endpoint and a declared tree whose own Wires are bound to carrier paths, on the local pair and on real WebSockets in both directions, including replies, cancellation, relays, addressed mounts and teardown.", + "historical-addressed": "An observation that depended on decision 0006's addressed interpretation. It stays valid evidence about AddressedWire operators and is not a WireTree requirement." + }, + "limitations": { + "suffix-delivery": "An opaque child access received the remaining relative path, so [alias, x] reached leaf with [x]. In 0.3 every node's own Wire is addressless and a tree path names nodes only: a path beyond a node without that child is missing. Suffix delivery survives only as AddressedWire prefix binding (bitruntime At/at, Mount/mount), which decision 0012 says is not structural selection.", + "path-observing-interception": "A guard wrapped addressed access, saw every relative path and could refuse before any destination. An own Wire never sees a path. Interception inside a tree is a wrapped own primitive, whose retained state the structural cases cover; interception that inspects descendant paths exists only around addressed access.", + "addressed-views": "at(guard, [k]) produced an addressed view that repeated the guard's check when composed again. Structural selection returns the selected node itself, so a rebuilt tree retains child identity and nothing is wrapped twice.", + "owner-only-parts": "Decision 0006 kept construction parts with their owner and gave callers send-only access. Decision 0012 makes decomposition part of the full tree; a caller that must not see structure receives an AddressedWire bridge or a single own Wire instead.", + "addressed-mount": "Mount/mount is bitruntime's addressed routing operator: child-only, typed by a native map of Endpoints and validated on send. Its recorded gaps remain accurate observations of that operator. Tree construction is Compose/compose." + }, + "cases": [ + { "id": "origin-and-descendants", "gap": "origin-bearing-construction", "requirements": [ + { "kind": "structural", "cases": ["own-and-descendants"], "note": "Own value at [] beside complete children, empty keys, the literal slash key and both Unicode spellings. Adds the binary key and a present refusing node." }, + { "kind": "carrier", "cases": ["carrier-routing"] }, + { "kind": "historical-addressed", "limitation": "suffix-delivery", "note": "[alias, x] and [a, b, x, y] were delivered to leaf with a suffix; in the full tree both are missing." } + ] }, + { "id": "nested-selection-agrees", "gap": "origin-bearing-construction", "requirements": [ + { "kind": "structural", "cases": ["nested-selection-agrees"], "note": "Nested and concatenated selection reach the same node; selection returns that node itself." }, + { "kind": "historical-addressed", "limitation": "suffix-delivery", "note": "Selections through [alias] then [x] were delivered to leaf with [x]; in the full tree they are missing." } + ] }, + { "id": "missing-never-falls-back", "gap": "origin-bearing-construction", "requirements": [ + { "kind": "structural", "cases": ["missing-never-falls-back"], "note": "Meaning sharpened: missing paths are missing, not refused, and [empty] is a present node whose own refuses. No own value is invoked as fallback." } + ] }, + { "id": "empty-branch-versus-missing", "gap": "origin-bearing-construction", "requirements": [ + { "kind": "structural", "cases": ["refusing-versus-missing"], "note": "Meaning sharpened: the two sends were indistinguishable refusals; selection now distinguishes a present refusing node from a missing one." } + ] }, + { "id": "root-cut-reconstruction", "gap": "origin-bearing-construction", "requirements": [ + { "kind": "structural", "cases": ["root-cut-reconstruction"] } + ] }, + { "id": "complete-cuts-agree", "gap": "origin-bearing-construction", "requirements": [ + { "kind": "structural", "cases": ["complete-cuts-agree"] }, + { "kind": "carrier", "cases": ["carrier-reconstruction"] } + ] }, + { "id": "children-alone-lose-origin", "gap": "origin-bearing-construction", "requirements": [ + { "kind": "structural", "cases": ["children-alone-lose-own"] } + ] }, + { "id": "fresh-origin-resets-parent-state", "gap": "origin-bearing-construction", "requirements": [ + { "kind": "structural", "cases": ["fresh-own-resets-state"] } + ] }, + { "id": "equivalent-substitution", "gap": "origin-bearing-construction", "requirements": [ + { "kind": "structural", "cases": ["equivalent-substitution"] } + ] }, + { "id": "copied-subtree-breaks-sharing", "gap": "origin-bearing-construction", "requirements": [ + { "kind": "structural", "cases": ["copied-subtree-breaks-sharing"] } + ] }, + { "id": "altered-children-detected", "gap": "origin-bearing-construction", "requirements": [ + { "kind": "structural", "cases": ["altered-children-detected"], "note": "Meaning sharpened: an omitted or renamed child is missing rather than refused." } + ] }, + { "id": "invalid-path-never-reaches-origin", "gap": "origin-bearing-construction", "requirements": [ + { "kind": "bridge", "cases": ["bridge-exact-utf8-image"], "note": "Tree paths are bytes and have no invalid form; an ill-formed addressed segment is refused by the bridge before any own Wire." } + ] }, + { "id": "selection-retains-original-binding", "gap": "origin-bearing-construction", "requirements": [ + { "kind": "structural", "cases": ["selected-subtree-keeps-identity"] } + ] }, + { "id": "forwarded-reconstruction", "gap": "origin-bearing-construction", "requirements": [ + { "kind": "carrier", "cases": ["carrier-relay-reconstruction"] } + ] }, + { "id": "mounted-carrier-reconstruction", "gap": "origin-bearing-construction", "requirements": [ + { "kind": "carrier", "cases": ["carrier-mount-reconstruction"] } + ] }, + { "id": "pending-across-reconstruction", "gap": "origin-bearing-construction", "requirements": [ + { "kind": "carrier", "cases": ["carrier-late-reply-across-replacement", "carrier-cancel-across-replacement"], "note": "The captured cancellation is now the requester's cancel frame through tree access, routed by the far dispatcher's captured traversal; the late reply returns to the original return capability." } + ] }, + { "id": "mount-routing", "requirements": [ + { "kind": "structural", "cases": ["refusing-own-and-descendants"], "note": "Meaning sharpened: [missing] is missing, while [], [a], [\"\"] and [empty] are present nodes whose own refuses." }, + { "kind": "historical-addressed", "limitation": "suffix-delivery" } + ] }, + { "id": "mount-nested-selection", "requirements": [ + { "kind": "structural", "cases": ["nested-selection-agrees", "refusing-own-and-descendants"] }, + { "kind": "historical-addressed", "limitation": "suffix-delivery", "note": "[alias] then [x, y] reached leaf with a suffix." } + ] }, + { "id": "mount-complete-cuts-agree", "requirements": [ + { "kind": "structural", "cases": ["refusing-complete-cuts-agree"] } + ] }, + { "id": "mount-empty-branch-versus-missing", "requirements": [ + { "kind": "structural", "cases": ["refusing-versus-missing"] } + ] }, + { "id": "mount-altered-children-detected", "requirements": [ + { "kind": "structural", "cases": ["altered-children-detected"] } + ] }, + { "id": "conflicting-children", "gap": "conflicting-segments-accepted", "requirements": [ + { "kind": "structural", "cases": ["duplicate-keys-refused"] } + ] }, + { "id": "invalid-key", "gap": "invalid-segments-accepted", "requirements": [ + { "kind": "structural", "cases": ["own-and-descendants"], "note": "Meaning changed: a key outside the UTF-8 image is a valid tree key, reachable structurally." }, + { "kind": "bridge", "cases": ["bridge-exact-utf8-image"], "note": "It is unreachable through the bitwire/1 bridge, which never decodes or normalizes." }, + { "kind": "carrier", "cases": ["carrier-routing"] } + ] }, + { "id": "missing-child-value", "gap": "missing-children-accepted", "requirements": [ + { "kind": "structural", "cases": ["missing-child-refused"] } + ] }, + { "id": "cyclic-declaration", "requirements": [ + { "kind": "structural", "cases": ["cycle-refused"], "note": "The historical case refused a cyclic description in the test harness. Construction itself now refuses a child graph that contains a cycle." } + ] }, + { "id": "mount-invalid-path", "requirements": [ + { "kind": "bridge", "cases": ["bridge-exact-utf8-image"] } + ] }, + { "id": "mount-shared-child-state", "requirements": [ + { "kind": "structural", "cases": ["shared-child-state"] } + ] }, + { "id": "mount-equivalent-substitution", "requirements": [ + { "kind": "structural", "cases": ["equivalent-substitution"] } + ] }, + { "id": "mount-copied-subtree-breaks-sharing", "requirements": [ + { "kind": "structural", "cases": ["copied-subtree-breaks-sharing"] } + ] }, + { "id": "mount-forwarded-reconstruction", "requirements": [ + { "kind": "carrier", "cases": ["carrier-relay-reconstruction"] } + ] }, + { "id": "mount-mounted-carrier", "requirements": [ + { "kind": "carrier", "cases": ["carrier-mount-reconstruction"] } + ] }, + { "id": "mount-selection-retains-original-binding", "requirements": [ + { "kind": "structural", "cases": ["selected-subtree-keeps-identity"] } + ] }, + { "id": "mount-pending-across-reconstruction", "requirements": [ + { "kind": "carrier", "cases": ["carrier-late-reply-across-replacement", "carrier-cancel-across-replacement"] } + ] }, + { "id": "mount-teardown-keeps-borrowed", "requirements": [ + { "kind": "carrier", "cases": ["carrier-teardown-keeps-borrowed"], "note": "Closing the serving dispatcher releases its routes, never the borrowed endpoint or the served tree." }, + { "kind": "historical-addressed", "limitation": "suffix-delivery", "note": "The direct send to leaf with [x]." } + ] }, + { "id": "guard-around-composite", "requirements": [ + { "kind": "historical-addressed", "limitation": "path-observing-interception" } + ] }, + { "id": "guard-state-survives-reconstruction", "requirements": [ + { "kind": "structural", "cases": ["root-cut-reconstruction", "shared-child-state"], "note": "Retained primitive state survives reconstruction because reconstruction keeps the capability." }, + { "kind": "historical-addressed", "limitation": "path-observing-interception" } + ] }, + { "id": "fresh-guard-resets-state", "requirements": [ + { "kind": "structural", "cases": ["fresh-own-resets-state"] }, + { "kind": "historical-addressed", "limitation": "path-observing-interception" } + ] }, + { "id": "guarded-child-complete-access", "requirements": [ + { "kind": "historical-addressed", "limitation": "path-observing-interception" }, + { "kind": "historical-addressed", "limitation": "suffix-delivery" } + ] }, + { "id": "rebuilding-from-guarded-views-repeats-checks", "requirements": [ + { "kind": "structural", "cases": ["equivalent-substitution", "nested-selection-agrees"], "note": "Rebuilding from selected children retains their identity." }, + { "kind": "historical-addressed", "limitation": "addressed-views" } + ] } + ], + "gaps": [ + { "id": "origin-bearing-construction", "disposition": "met-structurally", "cases": ["own-and-descendants", "refusing-own-and-descendants", "complete-cuts-agree", "children-alone-lose-own"], "note": "Public Compose/compose(own, children) takes the own value. The ledger entries remain true of addressed Mount, which is child-only by design.", "limitation": "addressed-mount" }, + { "id": "conflicting-segments-accepted", "disposition": "met-structurally", "cases": ["duplicate-keys-refused"], "limitation": "addressed-mount" }, + { "id": "invalid-segments-accepted", "disposition": "meaning-changed", "cases": ["own-and-descendants", "bridge-exact-utf8-image"], "note": "No tree key is invalid. The bridge carries the exact UTF-8 image and refuses ill-formed segments before any primitive.", "limitation": "addressed-mount" }, + { "id": "missing-children-accepted", "disposition": "met-structurally", "cases": ["missing-child-refused"], "limitation": "addressed-mount" } + ], + "ledgerLimitations": [ + { "id": "endpoint-typed-children", "disposition": "dissolved", "note": "Tree children are DeixisNode values whose own is a send-only Wire; construction needs no receive attachment or closure authority.", "cases": ["own-and-descendants"] }, + { "id": "owner-retained-parts", "disposition": "superseded", "note": "Decision 0012 makes decomposition public; restricted callers receive the bridge.", "cases": ["bridge-exact-utf8-image"], "limitation": "owner-only-parts" } + ] +} diff --git a/docs/composition.md b/docs/composition.md index 3ba3682..f28a4cd 100644 --- a/docs/composition.md +++ b/docs/composition.md @@ -65,6 +65,29 @@ profile-defined correlation, context and live-reference obligations. invocation-lifecycle paths. Replacing it with a primitive Wire would erase those operations and requires a separate explicit lifecycle design. +## Full trees across carriers + +A tree crosses a carrier as two trees. The far side serves its nodes on an +endpoint; the near side declares the same structure, and each of its own Wires +sends at the corresponding far path. Structure stays local on both sides: +the carrier carries paths and messages, never children or own capabilities. + +- `bitwire/1` refuses a request at a peer root's empty path, so a served tree + sits under a nonempty prefix. +- A carrier path names a far position. Addressed access cannot show that two + positions share a node, so the near side binds each position. +- Missing and refusing remain distinct: a near path that does not exist sends + nothing; a far path without a node answers `method_not_found`; a far node + whose own refuses answers `internal`. +- A far side that serves through an invocation-aware dispatcher keeps each + admitted request's cancellation with the node that admitted it, even after + that node is replaced. + +The [full-tree cases](../conformance/wiretree/README.md) check these +observations against released bitruntime. The adapters that bind a Wire to a +carrier path and serve a tree on a dispatcher are test-only there; production +facilities for them belong to bitruntime. + ## Ownership and evidence Bitwire owns the declarations, laws, protocol and independent expectations. diff --git a/scripts/README.md b/scripts/README.md index fb0d2b7..eec58ee 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -10,15 +10,18 @@ The command uses shell-free child processes and resolves the checkout from its own location. It runs identically on Windows and Linux: 1. Check repository-relative Markdown links against tracked and unignored files. -2. Check that no published package depends on or imports Nightseam or bitruntime, +2. Run the script tests, including `wiretree.test.mjs`: the full-tree case + notation, withheld driver inputs, observation comparison and the disposition + of every historical declared case. +3. Check that no published package depends on or imports Nightseam or bitruntime, in any language ([decisions 0007](../docs/decisions/0007-using-bitwire-never-requires-nightseam.md) and [0010](../docs/decisions/0010-bitwire-holds-the-contract-and-bitruntime-implements-it.md)). Test-only conformance under `conformance/` is exempt. -3. Check Go formatting, then run `go vet` and `go test` to compile the declarations. -4. Check and build the TypeScript declarations using the pinned compiler. -5. Run `scripts/composition.mjs`, comparing Go and TypeScript reference composition +4. Check Go formatting, then run `go vet` and `go test` to compile the declarations. +5. Check and build the TypeScript declarations using the pinned compiler. +6. Run `scripts/composition.mjs`, comparing Go and TypeScript reference composition observations to a shared independent oracle for the 0.2 contract. -6. Run `scripts/trees.mjs`, comparing Go and TypeScript test-only tree interpreters +7. Run `scripts/trees.mjs`, comparing Go and TypeScript test-only tree interpreters against the independent 0.3 full-structure observations. The link check checks local destinations and heading fragments, not remote URLs. @@ -34,6 +37,8 @@ conformance CI job. `node scripts/conformance-runtime.mjs` runs the same independent cases against the pinned bitruntime Go module and TypeScript release package in its own CI job; see [bitruntime runtime conformance](../conformance/README.md#bitruntime-runtime-conformance). +That job includes the [full-tree cases](../conformance/wiretree/README.md) and +their deliberately unlawful realizations. `node scripts/smoke-packed.mjs` installs npm and Go artifacts outside the checkout. These are separate from declaration checks. The [release procedure](../RELEASING.md) describes rehearsal and public registry diff --git a/scripts/check.mjs b/scripts/check.mjs index 46029c5..621daa5 100644 --- a/scripts/check.mjs +++ b/scripts/check.mjs @@ -15,7 +15,7 @@ if (!existsSync(compiler)) { run(process.execPath, ['scripts/links.mjs']); run(process.execPath, ['scripts/independence.mjs']); -run(process.execPath, ['--test', 'scripts/registry-readiness.test.mjs', 'scripts/publish-extra.test.mjs', 'scripts/conformance-results.test.mjs', 'scripts/independence.test.mjs']); +run(process.execPath, ['--test', 'scripts/registry-readiness.test.mjs', 'scripts/publish-extra.test.mjs', 'scripts/conformance-results.test.mjs', 'scripts/independence.test.mjs', 'scripts/wiretree.test.mjs']); const files = execFileSync('git', ['ls-files', '--cached', '--others', '--exclude-standard', '-z'], { cwd: root, encoding: 'utf8', }).split('\0').filter(path => path.endsWith('.go')); diff --git a/scripts/conformance-runtime.mjs b/scripts/conformance-runtime.mjs index 48f69c4..7d169f9 100644 --- a/scripts/conformance-runtime.mjs +++ b/scripts/conformance-runtime.mjs @@ -11,6 +11,7 @@ import { tmpdir } from 'node:os'; import { basename, dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { compareCases, compareProduction, declaredInputs, lifecycleInputs, nightseamCompositionExpected } from './conformance-results.mjs'; +import { compareWiretree, validateDisposition, wiretreeFailures, wiretreeInputs } from './wiretree-lib.mjs'; const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const module = join(root, 'conformance/runtime/go'); @@ -45,6 +46,8 @@ const files = { composition: 'conformance/reference/expected.json', trees: 'conformance/trees/expected.json', gaps: 'conformance/runtime/production-gaps.json', + wiretree: 'conformance/wiretree/cases.json', + disposition: 'conformance/wiretree/disposition.json', }; const digest = (algorithm, data, encoding = 'hex') => createHash(algorithm).update(data).digest(encoding); const sha256 = path => digest('sha256', bytes(path)); @@ -55,6 +58,13 @@ const declared = read(files.declared); const composition = nightseamCompositionExpected(read(files.composition)); const trees = read(files.trees); const gaps = read(files.gaps); +const wiretree = read(files.wiretree); +const disposition = validateDisposition(read(files.disposition), wiretree, bytes); +// Deliberately unlawful TypeScript realizations; each must fail at least one case. +const mutants = [ + ['fallback', 'local'], ['wrapping-own', 'local'], ['normalizing', 'local'], ['fabricating', 'local'], + ['latin1-bridge', 'local'], ['incomplete-children', 'local'], ['retargeting-serve', 'carrier'], +]; const carriers = [['local', '0'], ['peer', '0'], ['peer', '1']]; const scratch = mkdtempSync(join(tmpdir(), 'bitwire-runtime-')); @@ -168,6 +178,8 @@ try { writeFileSync(lifecycleInput, JSON.stringify(lifecycleInputs(lifecycle))); const declaredInput = join(scratch, 'declared-inputs.json'); writeFileSync(declaredInput, JSON.stringify(declaredInputs(declared))); + const wiretreeInput = join(scratch, 'wiretree-inputs.json'); + writeFileSync(wiretreeInput, JSON.stringify(wiretreeInputs(wiretree))); const total = declared.cases.length; const toolchain = []; @@ -177,11 +189,12 @@ try { let driver, facilities; if (language === 'go') { verifyModules(); - const programs = Object.fromEntries(['lifecycle', 'composition', 'declared', 'trees'].map(name => [name, build(name)])); + const programs = Object.fromEntries(['lifecycle', 'composition', 'declared', 'trees', 'wiretree'].map(name => [name, build(name)])); driver = (name, arguments_ = [], env = {}) => run(programs[name], arguments_, env); facilities = { lifecycle: 'core.Invocation', composition: 'NewPair/peers, dispatch, At, Mount and Forward', production: 'core.Mount/At/Forward', trees: 'core.Compose/Select/Send/AsAddressed', + carriers: 'NewPair/peers, dispatch.NewDispatcher, Forward, Mount and At', }; toolchain.push(`${run('go', ['env', 'GOVERSION']).trim()}${race ? ' with race detector' : ' (race detector unavailable locally)'}`); } else { @@ -192,6 +205,7 @@ try { facilities = { lifecycle: 'Invocation', composition: 'pair/Peer, createDispatcher, at, mount and forward', production: 'mount/at/forward', trees: 'compose/select/send/asAddressed', + carriers: 'pair/Peer, createDispatcher, forward, mount and at', }; const typescript = run(process.execPath, [compiler, '--version'], {}, target).trim().replace(/^Version /, ''); toolchain.push(`node ${process.version} with type stripping, checked by TypeScript ${typescript}`); @@ -224,12 +238,37 @@ try { assert.deepEqual(JSON.parse(driver('trees')), trees, `${language}/trees: full tree observations differ from bitwire's oracle`); pass(`${language}/trees`, `${Object.keys(trees).length}/${Object.keys(trees).length} observations through ${facilities.trees}`); + + // Full trees: structural and bridge cases without a carrier, then carrier + // composition on each carrier. The reference is a test-only interpreter; + // production is bitruntime's tree operations. bind and serve are test-only + // adapters in both, because bitruntime has no public facility for them yet. + for (const realization of ['reference', 'production']) { + const trees_ = realization === 'production' ? facilities.trees : 'the test-only interpreter'; + const label = `${language}/wiretree/${realization}`; + const local = compareWiretree(wiretree, JSON.parse(driver('wiretree', [realization, 'local', wiretreeInput])), ['structure', 'bridge'], label); + pass(label, `${local}/${local} structural and bridge cases through ${trees_}`); + for (const [carrier, reverse] of carriers) { + const at = `${label}/${carrier}/${reverse}`; + const count = compareWiretree(wiretree, JSON.parse(driver('wiretree', [realization, 'carrier', wiretreeInput], carrierEnv(carrier, reverse))), ['carrier'], at); + pass(at, `${count}/${count} carrier cases through ${trees_} over ${facilities.carriers} (test-only bind and serve)`); + } + } + if (language === 'ts') { + for (const [mutant, scope] of mutants) { + const actual = JSON.parse(driver('wiretree', [`mutant:${mutant}`, scope, wiretreeInput], scope === 'carrier' ? carrierEnv('local', '0') : {})); + const failed = wiretreeFailures(wiretree, actual, scope === 'carrier' ? ['carrier'] : ['structure', 'bridge']); + assert.ok(failed.length > 0, `the unlawful realization ${mutant} passes every case`); + pass(`ts/wiretree/mutant/${mutant}`, `rejected by ${failed.length} case${failed.length === 1 ? '' : 's'}, including ${failed[0]}`); + } + } } console.log('\nReport'); console.log(` bitwire contract: ${pin.bitwireVersion} (github.com/Bitspark/bitwire at ${pin.bitwireRevision}; npm @bitspark/bitwire ${pin.npm.bitwireVersion})`); console.log(` bitwire cases: checkout ${checkout}${modified ? ' with local changes to the case files' : ''}`); for (const [name, path] of Object.entries(files)) console.log(` ${sha256(path)} ${path}${name === 'gaps' ? ' (bitruntime gap ledger)' : ''}`); + console.log(` Disposition: all ${disposition.declared} historical declared cases, ${disposition.gaps} recorded gaps (${disposition.gapCases} cases) and ${disposition.limitations} limitations mapped to current cases or historical addressed limitations`); if (languages.includes('go')) console.log(` Go: ${pin.module} ${pin.version} (${pin.revision}), ${pin.profile}`); if (languages.includes('ts')) { console.log(` TypeScript: ${pin.npm.package} ${pin.npm.version} (${pin.npm.tag} at ${pin.npm.revision}), ${pin.profile}`); @@ -240,7 +279,8 @@ try { console.log(' Carriers: local = the local pair (Go core.NewPair, TypeScript pair); peer/0 = WebSocket client sends;'); console.log(' peer/1 = WebSocket server sends (Go engine/websocket; TypeScript engine Peer over ws)'); for (const line of results) console.log(` ${line}`); - console.log('Historical 0.2 addressed evidence (lifecycle, declared, composition) and 0.3 structural evidence (trees).'); + console.log('Historical 0.2 addressed evidence (lifecycle, declared, composition); 0.3 structural evidence (trees, wiretree structure/bridge);'); + console.log('0.3 carrier composition evidence (wiretree carrier), with test-only bind and serve adapters.'); } finally { if (args.includes('--keep-scratch')) console.log(`Retained scratch: ${scratch}`); else { diff --git a/scripts/wiretree-lib.mjs b/scripts/wiretree-lib.mjs new file mode 100644 index 0000000..636c5f5 --- /dev/null +++ b/scripts/wiretree-lib.mjs @@ -0,0 +1,213 @@ +// The full-tree family: validation of conformance/wiretree/cases.json, driver +// inputs with every expectation withheld, and comparison of driver observations +// with the independently authored oracle. Keys reach drivers only as lowercase +// hex of their exact bytes, so no driver parses the case notation. +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import { isDeepStrictEqual } from 'node:util'; + +const encoder = new TextEncoder(); +const families = new Set(['structure', 'bridge', 'carrier']); +const operations = { + structure: new Set(['structure', 'send', 'same', 'rebuild', 'own', 'substitute', 'omit', 'rename', 'add', 'replace', 'view']), + bridge: new Set(['bridge', 'bridgeInvalid']), + carrier: new Set(['send', 'sendAddressed', 'structure', 'rebuild', 'replace', 'hold', 'release', 'cancel', 'teardown', 'direct']), +}; +const faults = new Set(['duplicate', 'missingChild', 'cycle']); + +function scalar(text) { + assert.equal(typeof text, 'string'); + assert.ok(text.isWellFormed(), `not a Unicode scalar string: ${JSON.stringify(text)}`); + return text; +} + +/** Exact key bytes as lowercase hex: a string is its UTF-8; {hex} is bytes. */ +export function keyHex(key) { + if (typeof key === 'string') return Buffer.from(encoder.encode(scalar(key))).toString('hex'); + assert.ok(key && typeof key === 'object' && Object.keys(key).join() === 'hex', `invalid key ${JSON.stringify(key)}`); + assert.match(key.hex, /^(?:[0-9a-f]{2})*$/, `invalid hex key ${key.hex}`); + return key.hex; +} +const pathHex = path => { + assert.ok(Array.isArray(path), 'a tree path is an array of keys'); + return path.map(keyHex); +}; +const addressed = path => { + assert.ok(Array.isArray(path), 'an addressed path is an array of strings'); + return path.map(scalar); +}; + +export function validateWiretree(fixture) { + assert.equal(fixture.schemaVersion, 1); + assert.ok(Array.isArray(fixture.servedAt) && fixture.servedAt.length > 0, 'servedAt must be a nonempty addressed prefix'); + addressed(fixture.servedAt); + const declarations = new Map(); + for (const d of fixture.declarations) { + assert.ok(typeof d.id === 'string' && !declarations.has(d.id), `duplicate or unnamed declaration ${d.id}`); + assert.ok(d.own === null || (typeof d.own === 'string' && d.own.length > 0), `${d.id}: own is a name or null`); + declarations.set(d.id, d); + } + for (const d of fixture.declarations) { + const keys = (d.children ?? []).map(([key, child]) => { + assert.ok(declarations.has(child), `${d.id}: unknown child ${child}`); + return keyHex(key); + }); + assert.equal(new Set(keys).size, keys.length, `${d.id}: duplicate byte key`); + } + assert.ok(Array.isArray(fixture.cases) && fixture.cases.length > 0); + const ids = new Set(); + for (const test of fixture.cases) { + assert.ok(typeof test.id === 'string' && !ids.has(test.id), `duplicate case ${test.id}`); + ids.add(test.id); + assert.ok(families.has(test.family), `${test.id}: unknown family ${test.family}`); + assert.ok(declarations.has(test.root), `${test.id}: unknown root`); + assert.ok(test.fault === undefined || (test.family === 'structure' && faults.has(test.fault)), `${test.id}: unknown fault`); + assert.ok((!test.relay && !test.mount) || test.family === 'carrier', `${test.id}: relay and mount are carrier options`); + for (const step of test.steps) { + assert.ok(operations[test.family].has(step.op), `${test.id}: ${step.op} is not a ${test.family} operation`); + if (step.node !== undefined) assert.ok(declarations.has(step.node), `${test.id}: unknown node ${step.node}`); + if (test.family === 'carrier' && ['rebuild', 'replace'].includes(step.op)) { + assert.ok(['near', 'far'].includes(step.side), `${test.id}: ${step.op} names its side`); + } + } + assert.ok(test.expected && typeof test.expected === 'object', `${test.id}: no expectation`); + } + return declarations; +} + +function stepInput(step) { + const out = { ...step }; + for (const field of ['path', 'keep', 'selections', 'paths']) { + if (step[field] === undefined) continue; + if (field === 'path' && ['bridge', 'sendAddressed'].includes(step.op)) out.path = addressed(step.path); + else if (field === 'path') out.path = pathHex(step.path); + else out[field] = step[field].map(pathHex); + } + for (const field of ['key', 'to']) if (step[field] !== undefined) out[field] = keyHex(step[field]); + return out; +} + +/** What a driver receives: structure and steps, never an expectation. */ +export function wiretreeInputs(fixture) { + validateWiretree(fixture); + return { + servedAt: fixture.servedAt, + declarations: fixture.declarations.map(({ id, own, children }) => ({ + id, own, children: (children ?? []).map(([key, child]) => [keyHex(key), child]), + })), + cases: fixture.cases.map(({ id, family, root, fault, relay, mount, steps }) => ({ + id, family, root, ...(fault ? { fault } : {}), ...(relay ? { relay } : {}), ...(mount ? { mount } : {}), + steps: steps.map(stepInput), + })), + }; +} + +/** Children are a map, not a list: order is canonicalized by exact key bytes. */ +function canonicalRender(render, renders, keyed) { + if (render === 'missing' || render === 'unknown') return render; + if (render && typeof render === 'object' && Object.keys(render).join() === '$render') { + assert.ok(Object.hasOwn(renders, render.$render), `unknown render ${render.$render}`); + return canonicalRender(renders[render.$render], renders, keyed); + } + assert.ok(render && typeof render === 'object' && Array.isArray(render.children), `invalid render ${JSON.stringify(render)}`); + const children = render.children.map(([key, child]) => [keyed ? keyHex(key) : key, canonicalRender(child, renders, keyed)]); + children.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)); + return { own: render.own, children }; +} +function canonical(observation, renders, keyed) { + if (!observation || typeof observation !== 'object' || !Array.isArray(observation.trace)) return observation; + return { + ...observation, + trace: observation.trace.map(entry => entry[0] === 'structure' + ? ['structure', canonicalRender(entry[1], renders, keyed)] : entry), + }; +} + +export function wiretreeExpected(fixture) { + validateWiretree(fixture); + return new Map(fixture.cases.map(test => [test.id, canonical(test.expected, fixture.renders ?? {}, true)])); +} + +/** Compares the cases of the given families; missing, extra and duplicate rows fail. */ +export function compareWiretree(fixture, actual, selected, label) { + const expected = wiretreeExpected(fixture); + const cases = fixture.cases.filter(test => selected.includes(test.family)); + assert.ok(Array.isArray(actual), `${label}: expected an observations array`); + for (const row of actual) assert.deepEqual(Object.keys(row).sort(), ['id', 'observations'], `${label}: invalid result record`); + const rows = new Map(actual.map(row => [row.id, row.observations])); + assert.equal(rows.size, actual.length, `${label}: duplicate observations`); + assert.equal(actual.length, cases.length, `${label}: missing or extra observations`); + for (const test of cases) { + assert.ok(rows.has(test.id), `${label}: missing ${test.id}`); + assert.deepEqual(canonical(rows.get(test.id), {}, false), expected.get(test.id), `${label}: ${test.id}`); + } + return cases.length; +} + +/** The cases of the given families that an implementation fails; used to show unlawful realizations are rejected. */ +export function wiretreeFailures(fixture, actual, selected) { + const expected = wiretreeExpected(fixture); + const rows = new Map(actual.map(row => [row.id, row.observations])); + return fixture.cases.filter(test => selected.includes(test.family)) + .filter(test => !isDeepStrictEqual(canonical(rows.get(test.id), {}, false), expected.get(test.id))) + .map(test => test.id); +} + +const sha256 = bytes => createHash('sha256').update(bytes).digest('hex'); + +/** + * Every historical declared case, recorded gap and limitation is disposed + * exactly once, and every current case it names exists. The historical files + * are pinned by digest: changing them reopens the disposition. + */ +export function validateDisposition(disposition, fixture, read) { + assert.equal(disposition.schemaVersion, 1); + for (const [path, digest] of Object.entries(disposition.historical)) { + assert.equal(sha256(read(path)), digest, `${path} changed; the disposition must be revisited`); + } + const declared = JSON.parse(read('conformance/declared/cases.json')); + const ledgers = ['conformance/declared/production-gaps.json', 'conformance/runtime/production-gaps.json'].map(path => JSON.parse(read(path))); + const current = new Map(fixture.cases.map(test => [test.id, test])); + const kinds = new Set(Object.keys(disposition.kinds)); + const limitations = new Set(Object.keys(disposition.limitations)); + const familyOf = { structural: 'structure', bridge: 'bridge', carrier: 'carrier' }; + const cited = new Set(); + const names = cases => { + for (const id of cases) { + assert.ok(current.has(id), `unknown current case ${id}`); + cited.add(id); + } + }; + + const disposed = new Map(disposition.cases.map(row => [row.id, row])); + assert.equal(disposed.size, disposition.cases.length, 'a declared case is disposed twice'); + assert.deepEqual([...disposed.keys()].sort(), declared.cases.map(test => test.id).sort(), 'every declared case is disposed exactly once'); + const gapCases = new Map(); + for (const ledger of ledgers) for (const gap of ledger.gaps) for (const id of gap.cases) gapCases.set(id, gap.id); + for (const row of disposition.cases) { + assert.equal(row.gap, gapCases.get(row.id), `${row.id}: gap membership differs from the ledgers`); + assert.ok(row.requirements.length > 0, `${row.id}: no requirement`); + for (const requirement of row.requirements) { + assert.ok(kinds.has(requirement.kind), `${row.id}: unknown kind ${requirement.kind}`); + if (requirement.kind === 'historical-addressed') { + assert.ok(limitations.has(requirement.limitation), `${row.id}: unknown limitation ${requirement.limitation}`); + assert.equal(requirement.cases, undefined, `${row.id}: a historical limitation names no current case`); + } else { + assert.ok(requirement.cases?.length > 0, `${row.id}: ${requirement.kind} names no current case`); + names(requirement.cases); + for (const id of requirement.cases) assert.equal(current.get(id).family, familyOf[requirement.kind], `${row.id}: ${id} is not ${requirement.kind}`); + } + } + } + + for (const ledger of ledgers) { + assert.deepEqual(ledger.gaps.map(gap => gap.id).sort(), disposition.gaps.map(gap => gap.id).sort(), 'every recorded gap is disposed'); + assert.deepEqual(ledger.limitations.map(item => item.id).sort(), disposition.ledgerLimitations.map(item => item.id).sort(), 'every recorded limitation is disposed'); + } + for (const item of [...disposition.gaps, ...disposition.ledgerLimitations]) { + names(item.cases); + if (item.limitation) assert.ok(limitations.has(item.limitation), `${item.id}: unknown limitation`); + } + const uncited = fixture.cases.map(test => test.id).filter(id => !cited.has(id)); + return { declared: declared.cases.length, gaps: disposition.gaps.length, gapCases: gapCases.size, limitations: disposition.ledgerLimitations.length, uncited }; +} diff --git a/scripts/wiretree.test.mjs b/scripts/wiretree.test.mjs new file mode 100644 index 0000000..6a682f2 --- /dev/null +++ b/scripts/wiretree.test.mjs @@ -0,0 +1,69 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; +import { compareWiretree, keyHex, validateDisposition, wiretreeExpected, wiretreeFailures, wiretreeInputs } from './wiretree-lib.mjs'; + +const root = new URL('../', import.meta.url); +const read = path => readFileSync(new URL(path, root)); +const fixture = JSON.parse(read('conformance/wiretree/cases.json')); +const disposition = JSON.parse(read('conformance/wiretree/disposition.json')); +const conforming = families => fixture.cases.filter(item => families.includes(item.family)) + .map(item => ({ id: item.id, observations: structuredClone(wiretreeExpected(fixture).get(item.id)) })); + +test('keys denote exact bytes and never normalize', () => { + assert.equal(keyHex(''), ''); + assert.equal(keyHex('a/b'), '612f62'); + assert.equal(keyHex('é'), 'c3a9'); + assert.equal(keyHex('é'), '65cc81'); + assert.equal(keyHex({ hex: 'ff' }), 'ff'); + assert.throws(() => keyHex('\ud800')); + assert.throws(() => keyHex({ hex: 'f' })); + assert.throws(() => keyHex({ hex: 'FF' })); +}); + +test('drivers receive inputs, never the oracle', () => { + const inputs = wiretreeInputs(fixture); + assert.equal(inputs.cases.length, fixture.cases.length); + const text = JSON.stringify(inputs); + assert.ok(!text.includes('"expected"') && !text.includes('"$render"') && !text.includes('"renders"')); + for (const item of inputs.cases) assert.ok(!('expected' in item)); + const binary = inputs.declarations.find(d => d.id === 'root').children.find(([, child]) => child === 'binary'); + assert.deepEqual(binary, ['ff', 'binary']); +}); + +test('missing, extra, duplicate and incorrect observations fail the gate', () => { + const families = ['structure', 'bridge']; + assert.equal(compareWiretree(fixture, conforming(families).reverse(), families, 'valid'), 19); + assert.throws(() => compareWiretree(fixture, conforming(families).slice(1), families, 'missing')); + assert.throws(() => compareWiretree(fixture, [...conforming(families), conforming(families)[0]], families, 'duplicate')); + assert.throws(() => compareWiretree(fixture, [...conforming(families), conforming(['carrier'])[0]], families, 'extra')); + const wrong = conforming(families); + wrong[3].observations.trace[0] = ['refused']; + assert.throws(() => compareWiretree(fixture, wrong, families, 'missing reported as refused')); + assert.deepEqual(wiretreeFailures(fixture, wrong, families), [wrong[3].id]); +}); + +test('child order has no meaning, but child keys and owns do', () => { + const rows = conforming(['structure']); + const row = rows.find(item => item.id === 'own-and-descendants'); + row.observations.trace[0][1].children.reverse(); + compareWiretree(fixture, rows, ['structure'], 'reordered'); + row.observations.trace[0][1].children[0][0] = '00'; + assert.throws(() => compareWiretree(fixture, rows, ['structure'], 'renamed key')); +}); + +test('every historical case, gap and limitation is disposed against a current case', () => { + const result = validateDisposition(disposition, fixture, read); + assert.deepEqual(result, { declared: 39, gaps: 4, gapCases: 19, limitations: 2, uncited: [] }); + const dropped = structuredClone(disposition); + dropped.cases.pop(); + assert.throws(() => validateDisposition(dropped, fixture, read)); + const unknown = structuredClone(disposition); + unknown.cases[0].requirements[0].cases = ['not-a-case']; + assert.throws(() => validateDisposition(unknown, fixture, read)); + const misfiled = structuredClone(disposition); + misfiled.cases[0].requirements[0].kind = 'carrier'; + assert.throws(() => validateDisposition(misfiled, fixture, read)); + const edited = path => path === 'conformance/declared/cases.json' ? Buffer.concat([read(path), Buffer.from(' ')]) : read(path); + assert.throws(() => validateDisposition(disposition, fixture, edited), /changed/); +}); From ac8897051ce0810fd554e536873cba94955e7fc9 Mon Sep 17 00:00:00 2001 From: Julian Matschinske Date: Sat, 26 Sep 2026 19:54:47 +0200 Subject: [PATCH 2/2] conformance: resolve review of the full-tree family. - The mounted-carrier case now reaches an own value at [] through the mount; the relay also carries the doubly empty path; the late reply arrives after the serving composition is torn down, as in the historical cases. - A U+FFFD child and a fixture-defined ill-formed segment (UTF-16 units and UTF-8 bytes) make lossy bridges observable in both languages; a lossy-bridge mutant is added. - Refusing primitives record their own invocation; missing is no invocation plus refusal; bridge refusals without a primitive are "unreached". - A driver-implemented acyclic child is accepted, so cycle refusal is not vacuous. - Exact parts check children() and decompose() by count and contents; the selection law covers chains that fail part-way; Go checks every structural method on the bridge; every event has its own return capability; borrowed endpoints are checked after the dispatcher, forwarding and mount are released. - Each mutant must fail the case aimed at it. - The carrier model moves from docs/composition.md to the family README as a test model; internal is documented as the serving adapter's choice. - The contract states that derived sending on a missing path is refused. - Disposition notes cover every sharpened meaning and name the carrier gap members met only through the test-only adapters. --- CHANGELOG.md | 22 +- conformance/README.md | 4 +- conformance/runtime/go/wiretree/main.go | 213 +- conformance/runtime/ts/wiretree.ts | 120 +- conformance/wiretree/README.md | 253 +- conformance/wiretree/cases.json | 3176 +++++++++++++++++++---- conformance/wiretree/disposition.json | 697 ++++- docs/composition.md | 26 +- docs/wire/contract.md | 3 +- scripts/conformance-runtime.mjs | 18 +- scripts/wiretree-lib.mjs | 12 +- scripts/wiretree.test.mjs | 2 +- 12 files changed, 3701 insertions(+), 845 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3600b1..fc0ef63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,15 +3,19 @@ ## Unreleased - Add the full-tree conformance family (`conformance/wiretree`) for decision - 0012: 18 structural, 1 bridge and 7 carrier cases with independently authored - expectations, run in Go and TypeScript against released bitruntime v0.2.0 on - the local pair and real WebSockets in both directions. It separates - structural, bridge and carrier evidence, and rejects seven deliberately - unlawful realizations. A disposition maps all 39 historical declared cases, - their 19 recorded gaps and two limitations to current cases or explicit - historical addressed limitations; the historical files are unchanged and - pinned by digest. The adapters that bind a Wire to a carrier path and serve a - tree on a dispatcher are test-only until bitruntime provides them. + 0012: 19 structural, 1 bridge and 7 carrier cases with independently authored + expectations, run in Go and TypeScript against released bitruntime v0.2.0. + Structural and bridge cases run without a carrier; carrier cases run on the + local pair and on real WebSockets in both directions, through test-only + adapters that bind a Wire to a carrier path and serve a tree on a dispatcher + until bitruntime provides them. Eight deliberately unlawful TypeScript + realizations are each rejected by the case aimed at them. A disposition maps + all 39 historical declared cases, their 19 recorded gaps and two limitations + to current cases or explicit historical addressed limitations; the historical + files are unchanged and pinned by digest. +- Clarify in the contract that derived sending on a missing tree path is + refused and never reports admission. bitruntime v0.2.0 already + behaves this way; no declaration changes. ## 0.3.0 — 2026-09-26 diff --git a/conformance/README.md b/conformance/README.md index 4ebc3bb..5e4061a 100644 --- a/conformance/README.md +++ b/conformance/README.md @@ -42,7 +42,7 @@ the case files' SHA-256s and every family's result in each language. | Declared, reference | The 39 [declared](declared/cases.json) cases through the test-only interpreter over bitruntime's carriers | the same three | 39/39 on each | 39/39 on each | | Declared, production | The same cases through bitruntime's child-only addressed mount, with its selection and forwarding | the same three | 20 conform; 19 match bitruntime's own [gap ledger](runtime/production-gaps.json) | the same | | Trees | The [0.3 observations](trees/expected.json) through bitruntime's tree construction, selection, sending and addressed bridge | none, structural | 14/14 | 14/14 | -| Full trees | The [full-tree cases](wiretree/README.md): 18 structural, 1 bridge, 7 carrier; test-only reference and bitruntime's tree operations | none for structure and bridge; the three carriers for carrier cases, with test-only bind and serve adapters | 26/26 in each realization | 26/26 in each realization | +| Full trees | The [full-tree cases](wiretree/README.md): 19 structural, 1 bridge, 7 carrier; test-only reference and bitruntime's tree operations | none for structure and bridge; the three carriers for carrier cases, with test-only bind and serve adapters | 27/27 in each realization | 27/27 in each realization | The Go drivers use `core.Invocation`, `core.NewPair`, the WebSocket engine, `dispatch`, `core.At`, `core.Mount`, `core.Forward`, `core.Compose`, @@ -65,7 +65,7 @@ every refusal in these fixtures happens before a carrier or forwarder. Lifecycle, composition and declared results remain evidence about the 0.2 addressed contract (`AddressedWire` in 0.3); trees is the 0.3 structural contract. The full-tree family separates structural, bridge and carrier -evidence, rejects seven deliberately unlawful TypeScript realizations, and +evidence, rejects eight deliberately unlawful TypeScript realizations, and [disposes](wiretree/disposition.json) every historical declared case and recorded gap. diff --git a/conformance/runtime/go/wiretree/main.go b/conformance/runtime/go/wiretree/main.go index 2eebadb..56048bb 100644 --- a/conformance/runtime/go/wiretree/main.go +++ b/conformance/runtime/go/wiretree/main.go @@ -174,15 +174,17 @@ type step struct { Mode string `json:"mode"` Own *string `json:"own"` Side string `json:"side"` + UTF8 string `json:"utf8"` } type testCase struct { - ID string `json:"id"` - Family string `json:"family"` - Root string `json:"root"` - Fault string `json:"fault"` - Relay bool `json:"relay"` - Mount bool `json:"mount"` - Steps []step `json:"steps"` + ID string `json:"id"` + Family string `json:"family"` + Root string `json:"root"` + Fault string `json:"fault"` + Relay bool `json:"relay"` + Mount bool `json:"mount"` + Foreign bool `json:"foreign"` + Steps []step `json:"steps"` } type inputs struct { ServedAt []string `json:"servedAt"` @@ -225,9 +227,16 @@ type primitive struct { func (p *primitive) Send(message wire.Message) error { return p.h.handle(p, message) } -type refuser struct{ _ byte } +// refuser is a present node's refusing own; it records its invocation itself, +// so a refusal is observed where it happens. +type refuser struct{ h *harness } -func (*refuser) Send(wire.Message) error { return errors.New("refused") } +func (r *refuser) Send(wire.Message) error { + if r.h != nil { + r.h.push("refused") + } + return errors.New("refused") +} type harness struct { mu sync.Mutex @@ -310,9 +319,8 @@ func (h *harness) construct(own wire.Wire, children []child) (tree, error) { } input[i].Tree = nil } - exact := func() bool { - gotOwn, got := t.Decompose() - if gotOwn != own || t.Own() != own || len(got) != len(want) || len(t.Children()) != len(want) { + same := func(got []child) bool { + if len(got) != len(want) { return false } for _, w := range want { @@ -322,13 +330,19 @@ func (h *harness) construct(own wire.Wire, children []child) (tree, error) { } return true } + exact := func() bool { + gotOwn, got := t.Decompose() + return gotOwn == own && t.Own() == own && same(got) && same(t.Children()) + } ok := exact() - returned := t.Children() - for i := range returned { - for j := range returned[i].Key { - returned[i].Key[j] = 'z' + _, decomposed := t.Decompose() + for _, returned := range [][]child{t.Children(), decomposed} { + for i := range returned { + for j := range returned[i].Key { + returned[i].Key[j] = 'z' + } + returned[i].Tree = nil } - returned[i].Tree = nil } ok = ok && exact() h.mu.Lock() @@ -348,7 +362,22 @@ func (l *loop) Children() []child { return []child{{Key: []byte("a func (l *loop) At(wire.TreePath) (tree, bool) { return nil, false } func (l *loop) Decompose() (wire.Wire, []child) { return l.own, l.Children() } +// foreign is an acyclic DeixisNode implemented by the driver, not the realization. +type foreign struct { + own wire.Wire + children []child +} + +func (f *foreign) Own() wire.Wire { return f.own } +func (f *foreign) Children() []child { return copyChildren(f.children) } +func (f *foreign) At(path wire.TreePath) (tree, bool) { return reference{}.Select(f, path) } +func (f *foreign) Decompose() (wire.Wire, []child) { return f.own, f.Children() } + func (h *harness) build(id, fault, rootID string) (tree, error) { + return h.buildWith(id, fault, rootID, false) +} + +func (h *harness) buildWith(id, fault, rootID string, withForeign bool) (tree, error) { if t, ok := h.built[id]; ok { return t, nil } @@ -370,8 +399,12 @@ func (h *harness) build(id, fault, rootID string) (tree, error) { case "cycle": children = append(children, child{Key: []byte("loop"), Tree: &loop{own: &refuser{}}}) } + if withForeign { + inner := &foreign{own: h.primitive("inner", false)} + children = append(children, child{Key: []byte("foreign"), Tree: &foreign{own: h.primitive("foreign", false), children: []child{{Key: []byte("inner"), Tree: inner}}}}) + } } - var own wire.Wire = &refuser{} + var own wire.Wire = &refuser{h: h} if d.Own != nil { own = h.primitive(*d.Own, false) } @@ -439,7 +472,7 @@ func (h *harness) fresh(own wire.Wire) wire.Wire { if p, ok := own.(*primitive); ok { return h.primitive(p.name, true) } - return &refuser{} + return &refuser{h: h} } func (h *harness) copy(t tree) tree { next := []child{} @@ -456,7 +489,7 @@ func (h *harness) edit(t tree, s step, build func(id string) tree) tree { return h.replaceAt(t, s.Path, func(tree) tree { return build(s.Node) }) case "own": return h.replaceAt(t, s.Path, func(n tree) tree { - var own wire.Wire = &refuser{} + var own wire.Wire = &refuser{h: h} if s.Own != nil { own = h.fresh(n.Own()) } @@ -500,31 +533,37 @@ func sameJSON(a, b json.RawMessage) bool { return reflect.DeepEqual(x, y) } -// chain applies each selection with the node's own At, and checks it against -// selecting the concatenation. -func (h *harness) chain(start tree, selections [][]string, fromRoot bool) (tree, bool) { - current := start +// chain applies each selection with the node's own At. From the root, the chain +// followed by path must select exactly what the concatenation selects, +// including when a selection in the chain fails. +func (h *harness) chain(start tree, selections [][]string, path []string, fromRoot bool) (tree, bool) { + current, ok := start, true for _, selection := range selections { - next, ok := current.At(keys(selection)) - if !ok { - return nil, false + if current, ok = current.At(keys(selection)); !ok { + break } - current = next } if fromRoot { all := []string{} for _, selection := range selections { all = append(all, selection...) } - if direct, ok := h.t.Select(start, keys(all)); !ok || direct != current { + direct, directOK := h.t.Select(start, keys(append(all, path...))) + var chained tree + chainedOK := false + if ok { + chained, chainedOK = h.t.Select(current, keys(path)) + } + if directOK != chainedOK || directOK && direct != chained { h.push("selectionDiffers") } } - return current, true + return current, ok } -// derived sends through the realization: missing selection invokes nothing; -// a present node's refusal is its own. +// derived sends through the realization. A present node's own primitive +// records its admission or refusal itself. A missing path invokes no primitive +// and the send is refused; anything else is recorded as the violation it is. func (h *harness) derived(base tree, ok bool, path []string, m wire.Message) { if !ok { h.push("missing") @@ -533,19 +572,19 @@ func (h *harness) derived(base tree, ok bool, path []string, m wire.Message) { _, present := h.t.Select(base, keys(path)) before := h.length() h.setExpected(m) - if err := h.t.Send(base, keys(path), m); err != nil { - if h.length() != before { - h.push("fallback") - } - if present { - h.push("refused") - } else { - h.push("missing") - } - return - } - if !present { - h.push("fabricated") + refused := h.t.Send(base, keys(path), m) != nil + invoked := h.length() != before + switch { + case present && !invoked && refused: + h.push("refusedWithoutOwn") + case present && !invoked: + h.push("admittedWithoutOwn") + case !present && invoked: + h.push("fallback") + case !present && refused: + h.push("missing") + case !present: + h.push("missingAdmitted") } } @@ -553,12 +592,11 @@ type refuseWire struct{ _ byte } func (*refuseWire) Send([]string, wire.Message) error { return errors.New("not a reply target") } -var replyless = &wire.ReturnAddress{Wire: &refuseWire{}} - +// event carries its own return capability, so identity preservation is per message. func event(data string) wire.Message { encoded, err := json.Marshal(data) check(err) - return wire.Message{Frame: wire.ProfileFrame{Version: 1, Kind: wire.ProfileEvent, Data: encoded}, Return: replyless} + return wire.Message{Frame: wire.ProfileFrame{Version: 1, Kind: wire.ProfileEvent, Data: encoded}, Return: &wire.ReturnAddress{Wire: &refuseWire{}}} } // ---- Local families: structure and the addressed bridge ---- @@ -572,7 +610,7 @@ func local(t trees, in inputs, test testCase) any { h.push("delivered", p.name, count) return nil } - root, err := h.build(test.Root, test.Fault, test.Root) + root, err := h.buildWith(test.Root, test.Fault, test.Root, test.Foreign) if err != nil { return map[string]any{"construction": "refused"} } @@ -598,7 +636,7 @@ func local(t trees, in inputs, test testCase) any { } var base tree if ok { - base, ok = h.chain(start, s.Selections, s.Via != "view") + base, ok = h.chain(start, s.Selections, s.Path, s.Via != "view") } h.derived(base, ok, s.Path, m) case "same": @@ -606,26 +644,35 @@ func local(t trees, in inputs, test testCase) any { b, bok := t.Select(root, keys(s.Paths[1])) h.push("same", aok && bok && a.Own() == b.Own()) case "view": - view, haveView = h.chain(root, s.Selections, true) + view, haveView = h.chain(root, s.Selections, nil, true) case "bridge", "bridgeInvalid": bridge := t.AsAddressed(root) _, receives := bridge.(interface { Receive(wire.Receiver) (func(), error) }) _, closes := bridge.(interface{ Close(wire.Code, string) error }) - _, structural := bridge.(interface{ Own() wire.Wire }) - sendOnly = sendOnly && !receives && !closes && !structural + _, owns := bridge.(interface{ Own() wire.Wire }) + _, enumerates := bridge.(interface{ Children() []child }) + _, selects := bridge.(interface { + At(wire.TreePath) (tree, bool) + }) + _, decomposes := bridge.(interface{ Decompose() (wire.Wire, []child) }) + sendOnly = sendOnly && !receives && !closes && !owns && !enumerates && !selects && !decomposes path := s.Path if s.Op == "bridgeInvalid" { - path = []string{"\xff"} + // The ill-formed segment comes from the fixture: bytes, where Go strings are bytes. + path = []string{string(key(s.UTF8))} } before := h.length() h.setExpected(m) - if err := bridge.Send(path, m); err != nil { - if h.length() != before { - h.push("fallback") + refused := bridge.Send(path, m) != nil + if h.length() == before { + // A refusal the bridge makes without reaching any primitive. + if refused { + h.push("unreached") + } else { + h.push("admittedWithoutOwn") } - h.push("refused") } default: root = h.edit(root, s, func(id string) tree { return h.must(h.build(id, "", "")) }) @@ -712,17 +759,20 @@ func runCarrier(t trees, in inputs, test testCase) (result any) { cleanup := func(f func()) { cleanups = append(cleanups, f) } near, first := carrier.Pair(cleanup) far := first + carriers := [][2]wire.Endpoint{{near, first}} + var compositions []func() if test.Relay { outgoing, target := carrier.Pair(cleanup) detach, err := core.Forward(first, outgoing) check(err) - cleanup(detach) + compositions = append(compositions, detach) + carriers = append(carriers, [2]wire.Endpoint{outgoing, target}) far = target } var access wire.AddressedWire = near if test.Mount { mounted := core.Mount(map[string]wire.Endpoint{"mounted": near}) - cleanup(func() { _ = mounted.Close(transports.CodeNormal, "done") }) + compositions = append(compositions, func() { _ = mounted.Close(transports.CodeNormal, "done") }) access = core.At(mounted, []string{"mounted"}) } @@ -815,7 +865,7 @@ func runCarrier(t trees, in inputs, test testCase) (result any) { label := fmt.Sprintf("%s:%d", test.ID, index) switch s.Op { case "send", "hold", "cancel": - base, ok := h.chain(nearTree, s.Selections, true) + base, ok := h.chain(nearTree, s.Selections, s.Path, true) if ok { _, ok = t.Select(base, keys(s.Path)) } @@ -888,24 +938,35 @@ func runCarrier(t trees, in inputs, test testCase) (result any) { } } - // The borrowed endpoint outlives every composition over it. + // Every borrowed endpoint outlives each composition over it: the dispatcher, + // the relay's forwarding and the addressed mount. unserve() _ = d.Close(transports.CodeNormal, "released") - borrowed := make(chan wire.Message, 1) - detach, err := far.Receive(wire.Receiver{Message: func(path []string, m wire.Message) { - if strings.Join(path, "/") == "borrowed" { - borrowed <- m - } - }}) - usable := false - if err == nil { - cleanup(detach) - if near.Send([]string{"borrowed"}, event("borrowed")) == nil { - select { - case m := <-borrowed: - usable = m.Frame.Kind == wire.ProfileEvent - case <-time.After(5 * time.Second): + for _, release := range compositions { + release() + } + usable := true + for _, pair := range carriers { + borrowed := make(chan wire.Message, 1) + detach, err := pair[1].Receive(wire.Receiver{Message: func(path []string, m wire.Message) { + if strings.Join(path, "/") == "borrowed" { + borrowed <- m } + }}) + if err != nil { + usable = false + continue + } + cleanup(detach) + if pair[0].Send([]string{"borrowed"}, event("borrowed")) != nil { + usable = false + continue + } + select { + case m := <-borrowed: + usable = usable && m.Frame.Kind == wire.ProfileEvent + case <-time.After(5 * time.Second): + usable = false } } h.mu.Lock() diff --git a/conformance/runtime/ts/wiretree.ts b/conformance/runtime/ts/wiretree.ts index abd266a..c5698f8 100644 --- a/conformance/runtime/ts/wiretree.ts +++ b/conformance/runtime/ts/wiretree.ts @@ -102,6 +102,10 @@ const mutants: Record = { 'latin1-bridge': { ...reference, asAddressed: tree => ({ send(path: Path, message: Message) { reference.send(tree, path.map(segment => [...segment].every(c => c.charCodeAt(0) < 256) ? Uint8Array.from([...segment].map(c => c.charCodeAt(0))) : encoder.encode(segment)), message); } }) }, + /** The bridge encodes segments without checking them, so a lone surrogate becomes U+FFFD. */ + 'lossy-bridge': { ...reference, asAddressed: tree => ({ send(path: Path, message: Message) { + reference.send(tree, path.map(segment => encoder.encode(segment)), message); + } }) }, /** children() omits the empty key, so the child map is incomplete. */ 'incomplete-children': { ...reference, compose: (own, children) => { const node = new Node(own, children); @@ -117,8 +121,9 @@ interface Declaration { id: string; own: string | null; children: [string, strin interface Step { op: string; path?: string[]; keep?: string[][]; selections?: string[][]; paths?: string[][]; via?: string; key?: string; to?: string; node?: string; mode?: string; own?: string | null; side?: string; + utf16?: string[]; utf8?: string; } -interface Case { id: string; family: string; root: string; fault?: string; relay?: boolean; mount?: boolean; steps: Step[] } +interface Case { id: string; family: string; root: string; fault?: string; foreign?: boolean; relay?: boolean; mount?: boolean; steps: Step[] } interface Inputs { servedAt: string[]; declarations: Declaration[]; cases: Case[] } type Trace = unknown[][]; @@ -170,8 +175,9 @@ class Harness { if (!fresh) this.shared.set(name, self); return self; } + /** A present node's refusal is observed where it happens: the refusing primitive records it. */ refuser(): Wire { - const self: Wire = { send() { throw new Error('refused'); } }; + const self: Wire = { send: () => { this.trace.push(['refused']); throw new Error('refused'); } }; this.refusing.add(self); return self; } @@ -187,22 +193,33 @@ class Harness { const tree = this.T.compose(own, input); for (const [key] of input) key.fill(0x7a); input.length = 0; + const same = (got: readonly Child[]): boolean => got.length === want.length + && want.every(([key, child]) => got.some(([k, c]) => hex(k) === key && c === child)); const exact = (): boolean => { const parts = tree.decompose(); - const got = parts.children.map(([key, child]) => [hex(key), child] as const); - return parts.own === own && tree.own() === own && got.length === want.length - && want.every(([key, child]) => got.some(([k, c]) => k === key && c === child)) - && tree.children().every(([key, child]) => want.some(([k, c]) => k === hex(key) && c === child)); + return parts.own === own && tree.own() === own && same(parts.children) && same(tree.children()); }; let ok = exact(); - const returned = tree.children() as [Key, WireTree][]; - for (const [key] of returned) key.fill(0x7a); - try { returned.length = 0; } catch { /* A frozen collection protects the tree too. */ } + for (const returned of [tree.children(), tree.decompose().children] as [Key, WireTree][][]) { + for (const [key] of returned) key.fill(0x7a); + try { returned.length = 0; } catch { /* A frozen collection protects the tree too. */ } + } ok &&= exact(); this.partsExact &&= ok; return tree; } - build(id: string, fault = '', rootID = ''): WireTree { + /** An acyclic DeixisNode implemented by the driver rather than the realization. */ + foreign(name: string, children: Child[]): WireTree { + const own = this.primitive(name); + const node: WireTree = { + own: () => own, + children: () => children.map(([key, child]) => [Uint8Array.from(key), child] as const), + at: path => reference.select(node, path), + decompose: () => ({ own, children: node.children() }), + }; + return node; + } + build(id: string, fault = '', rootID = '', foreign = false): WireTree { const found = this.built.get(id); if (found) return found; const d = this.declarations.get(id)!; @@ -218,6 +235,7 @@ class Harness { }; children.push([encoder.encode('loop'), loop]); } + if (id === rootID && foreign) children.push([encoder.encode('foreign'), this.foreign('foreign', [[encoder.encode('inner'), this.foreign('inner', [])]])]); const tree = this.construct(d.own === null ? this.refuser() : this.primitive(d.own), children); this.built.set(id, tree); return tree; @@ -269,34 +287,43 @@ class Harness { } } -/** Applies a selection chain with each node's own at, and checks it against selecting the concatenation. */ -function chain(h: Harness, start: WireTree, selections: string[][], fromRoot: boolean): WireTree | undefined { +/** + * Applies a selection chain with each node's own at. From the root, the chain + * followed by path must select exactly what the concatenation selects, including + * when a selection in the chain fails. + */ +function chain(h: Harness, start: WireTree, selections: string[][], path: string[], fromRoot: boolean): WireTree | undefined { let node: WireTree | undefined = start; for (const selection of selections) { node = node?.at(selection.map(bytes)); - if (!node) return undefined; + if (!node) break; + } + if (fromRoot && h.T.select(start, [...selections.flat(), ...path].map(bytes)) !== (node && h.T.select(node, path.map(bytes)))) { + h.trace.push(['selectionDiffers']); } - if (fromRoot && h.T.select(start, selections.flat().map(bytes)) !== node) h.trace.push(['selectionDiffers']); return node; } -/** Derived sending: missing selection invokes nothing; a present node's refusal is its own. */ +/** + * Derived sending. A present node's own primitive records its admission or + * refusal itself. A missing path invokes no primitive and the send is refused; + * anything else is recorded as the violation it is. + */ function derived(h: Harness, base: WireTree | undefined, path: string[], message: Message): void { if (!base) { h.trace.push(['missing']); return; } const present = h.T.select(base, path.map(bytes)) !== undefined; const before = h.trace.length; h.expected = message; - try { - h.T.send(base, path.map(bytes), message); - if (!present) h.trace.push(['fabricated']); - } catch { - if (h.trace.length !== before) h.trace.push(['fallback']); - h.trace.push([present ? 'refused' : 'missing']); - } + let refused = false; + try { h.T.send(base, path.map(bytes), message); } catch { refused = true; } + const invoked = h.trace.length !== before; + if (present && !invoked) h.trace.push([refused ? 'refusedWithoutOwn' : 'admittedWithoutOwn']); + if (!present && invoked) h.trace.push(['fallback']); + if (!present && !invoked) h.trace.push([refused ? 'missing' : 'missingAdmitted']); } -const refuseWire: AddressedWire = { send() { throw new Error('not a reply target'); } }; -const event = (data: string): Message => ({ frame: { version: 1, kind: 'event', data }, return: { wire: refuseWire } }); +// Each event carries its own return capability, so identity preservation is per message. +const event = (data: string): Message => ({ frame: { version: 1, kind: 'event', data }, return: { wire: { send() { throw new Error('not a reply target'); } } } }); // ---- Local families: structure and the addressed bridge ---- function local(T: Trees, inputs: Inputs, test: Case): unknown { @@ -305,7 +332,7 @@ function local(T: Trees, inputs: Inputs, test: Case): unknown { h.trace.push(['delivered', name, h.count(self)]); }); let root: WireTree; - try { root = h.build(test.root, test.fault, test.root); } catch { return { construction: 'refused' }; } + try { root = h.build(test.root, test.fault, test.root, test.foreign); } catch { return { construction: 'refused' }; } if (test.fault) return { construction: 'accepted' }; let view: WireTree | undefined; let sendOnly = true; @@ -319,7 +346,7 @@ function local(T: Trees, inputs: Inputs, test: Case): unknown { } case 'send': { const start = s.via === 'view' ? view : root; - derived(h, start && chain(h, start, s.selections ?? [], s.via !== 'view'), s.path!, message); + derived(h, start && chain(h, start, s.selections ?? [], s.path!, s.via !== 'view'), s.path!, message); break; } case 'same': { @@ -327,17 +354,19 @@ function local(T: Trees, inputs: Inputs, test: Case): unknown { h.trace.push(['same', a !== undefined && a === b]); break; } - case 'view': view = chain(h, root, s.selections!, true); break; + case 'view': view = chain(h, root, s.selections!, [], true); break; case 'bridge': case 'bridgeInvalid': { const bridge = T.asAddressed(root); sendOnly &&= typeof bridge.send === 'function' && ['receive', 'close', 'own', 'children', 'at', 'decompose'].every(name => !(name in bridge)); + // The ill-formed segment comes from the fixture: UTF-16 code units here. + const path = s.op === 'bridgeInvalid' ? [String.fromCharCode(...s.utf16!.map(unit => parseInt(unit, 16)))] : s.path!; const before = h.trace.length; h.expected = message; - try { bridge.send(s.op === 'bridgeInvalid' ? ['\ud800'] : s.path!, message); } catch { - if (h.trace.length !== before) h.trace.push(['fallback']); - h.trace.push(['refused']); - } + let refused = false; + try { bridge.send(path, message); } catch { refused = true; } + // A refusal the bridge makes without reaching any primitive. + if (h.trace.length === before) h.trace.push([refused ? 'unreached' : 'admittedWithoutOwn']); break; } default: root = h.edit(root, s, id => h.build(id)); @@ -379,15 +408,18 @@ async function carrier(T: Trees, inputs: Inputs, test: Case, retargeting = false try { const [near, first] = await connected(release => cleanups.push(release)); let far = first; + const carriers: [Endpoint, Endpoint][] = [[near, first]]; + const compositions: (() => void)[] = []; if (test.relay) { const [outgoing, target] = await connected(release => cleanups.push(release)); - cleanups.push(forward(first, outgoing)); + compositions.push(forward(first, outgoing)); + carriers.push([outgoing, target]); far = target; } let access: AddressedWire = near; if (test.mount) { const mounted = mount(new Map([['mounted', near]])); - cleanups.push(() => mounted.close(CODE_NORMAL, 'done')); + compositions.push(() => mounted.close(CODE_NORMAL, 'done')); access = at(mounted, ['mounted']); } @@ -460,7 +492,7 @@ async function carrier(T: Trees, inputs: Inputs, test: Case, retargeting = false const label = `${test.id}:${index}`; switch (s.op) { case 'send': case 'hold': case 'cancel': { - const base = chain(h, nearTree, s.selections ?? [], true); + const base = chain(h, nearTree, s.selections ?? [], s.path!, true); const target = base && T.select(base, s.path!.map(bytes)); if (!target) { h.trace.push(['missing']); break; } if (s.op === 'cancel') { @@ -507,16 +539,20 @@ async function carrier(T: Trees, inputs: Inputs, test: Case, retargeting = false } } - // The borrowed endpoint outlives every composition over it. + // Every borrowed endpoint outlives each composition over it: the dispatcher, + // the relay's forwarding and the addressed mount. unserve(); dispatcher?.close(); - const borrowed = mailbox(); - cleanups.push(far.receive({ message(path, message) { if (path.join('/') === 'borrowed') borrowed.put(message); } })); - let borrowedUsable = false; - try { - near.send(['borrowed'], event('borrowed')); - borrowedUsable = (await borrowed.take()).frame.kind === 'event'; - } catch { /* not usable */ } + for (const release of compositions) release(); + let borrowedUsable = true; + for (const [sender, receiver] of carriers) { + const borrowed = mailbox(); + try { + cleanups.push(receiver.receive({ message(path, message) { if (path.join('/') === 'borrowed') borrowed.put(message); } })); + sender.send(['borrowed'], event('borrowed')); + borrowedUsable &&= (await borrowed.take()).frame.kind === 'event'; + } catch { borrowedUsable = false; } + } return { trace: h.trace, unchanged: h.unchanged, borrowedUsable }; } finally { for (const cleanup of cleanups.reverse()) cleanup(); diff --git a/conformance/wiretree/README.md b/conformance/wiretree/README.md index 02cad34..80e5321 100644 --- a/conformance/wiretree/README.md +++ b/conformance/wiretree/README.md @@ -2,13 +2,13 @@ These independent cases exercise the 0.3 contract of [decision 0012](../../docs/decisions/0012-explicit-data-and-wire-trees.md) where -it matters to consumers: `WireTree = DeixisNode` built, selected, -decomposed and rebuilt locally, exposed through the unchanged `bitwire/1` -addressed carrier, and composed across real carriers. They are separately -identified from the historical [declared cases](../declared/README.md), which -stay byte-identical with their gap ledgers. -[`disposition.json`](disposition.json) maps every historical observation onto -these cases or onto an explicit historical addressed limitation. +it matters to consumers. They cover `WireTree = DeixisNode` built, +selected, decomposed and rebuilt locally, then exposed through the unchanged +`bitwire/1` addressed carrier, then composed across real carriers. They are +identified separately from the historical +[declared cases](../declared/README.md), which stay byte-identical with their gap +ledgers. [`disposition.json`](disposition.json) maps every historical observation +onto these cases or onto an explicit historical addressed limitation. Run them with the released-runtime gate: @@ -17,129 +17,174 @@ node scripts/conformance-runtime.mjs ``` The oracle is [`cases.json`](cases.json). Its expectations were written from the -contract, decision 0012 and the `bitwire/1` profile. They were not recorded from -an implementation. The runner gives each driver its inputs with every expectation -withheld and keys already converted to hex. It compares complete observations -itself: missing, extra, duplicate and mismatched rows fail. -`node --test scripts/wiretree.test.mjs`, part of `node scripts/check.mjs`, checks -the notation, the withheld inputs, the comparison and the disposition's -coverage. It needs no network. +[contract](../../docs/wire/contract.md), decision 0012 and the `bitwire/1` +profile. They were not recorded from an implementation. + +The runner gives each driver its inputs with every expectation withheld and with +keys already in hex. It compares complete observations itself: missing, extra, +duplicate and mismatched rows fail. + +`node --test scripts/wiretree.test.mjs` needs no network and is part of +`node scripts/check.mjs`. It checks the notation, the withheld inputs, the +comparison and the disposition's coverage. + +## What the cases observe + +| Observation | Meaning | +| --- | --- | +| `["delivered", name, n]` | The named primitive admitted a message. It is that instance's nth admission. | +| `["refused"]` | The selected node's own primitive was invoked and refused. The primitive records this itself. | +| `["missing"]` | Selection found no node, no primitive was invoked, and the derived send was refused. | +| `["unreached"]` | The addressed bridge refused without invoking any primitive. | +| `["error", code]` | Across a carrier, the request was answered with that `bitwire/1` error code. | +| `partsExact` | Every construction returned exactly its own value and children. This held through `children()` and `decompose()`, and after the caller's input, keys and returned parts were altered. | +| `unchanged` | Every delivered message kept its frame and its own return capability. | +| `sendOnly` | The bridge exposed no receive, close, own, children, selection or decomposition. | +| `borrowedUsable` | After the dispatcher, the relay's forwarding and the addressed mount were all released, every carrier still delivered. | + +A driver records any other outcome under its own name, such as `fallback`, +`refusedWithoutOwn` or `selectionDiffers`. That fails the case. ## Three kinds of evidence | Family | What it establishes | How it runs | | --- | --- | --- | -| `structure`, 18 cases | Own capability identity, exact byte keys (empty, binary, the literal `a/b`, both spellings of é), complete children, missing versus a present node whose own refuses, selection chains, both reconstruction directions, complete cuts, retained and reset primitive state, substitution, alteration, and construction refusing duplicate keys, missing children and cycles | No carrier. Every construction also checks that neither the caller's input nor returned parts can change the tree. | -| `bridge`, 1 case | The explicit mapping to the unchanged carrier surface: the exact UTF-8 image, refusal of ill-formed segments before any primitive, unreachable binary keys, send-only access | `AsAddressed`/`asAddressed`, no carrier | -| `carrier`, 7 cases | A far tree served on an endpoint and a near tree whose own Wires are bound to carrier paths: routing, reconstruction on either side, a relay through `Forward`, access through an addressed `Mount`, a late reply and a requester's cancellation after the far node was replaced, teardown | On the local pair, and on real WebSockets with the client sending and with the server sending | +| `structure`, 19 cases | Covers, among others:
  • own capability identity;
  • exact byte keys: empty, binary, U+FFFD, the literal `a/b` and both spellings of é;
  • complete children, and missing versus a present node whose own refuses;
  • the selection law, including chains that fail part-way;
  • both reconstruction directions and complete cuts;
  • retained and reset primitive state, substitution and alteration;
  • an acyclic child implemented outside the runtime;
  • construction refusing duplicate keys, missing children and cycles.
| No carrier | +| `bridge`, 1 case | The explicit mapping to the unchanged carrier surface:
  • the exact UTF-8 image;
  • refusal of an ill-formed segment before any primitive, including one that lossy decoding would turn into U+FFFD or the byte `ff`;
  • unreachable binary keys;
  • send-only access.
| `AsAddressed`/`asAddressed`, no carrier | +| `carrier`, 7 cases | Full trees composed across bitruntime carriers, as described in [the test model](#the-carrier-test-model) below | The local pair; real WebSockets with the client sending; real WebSockets with the server sending | -Two realizations run every case in Go and TypeScript. **production** uses -bitruntime's released `Compose`/`compose`, `Select`/`select`, `Send`/`send` and -`AsAddressed`/`asAddressed`. **reference** is a test-only interpreter that shows -the oracle can be met. It is never evidence about a runtime. Carriers, -dispatchers, `Forward`, `Mount` and `At` are always bitruntime's. +Two realizations run every case in Go and TypeScript: +- **production** uses bitruntime's released `Compose`/`compose`, + `Select`/`select`, `Send`/`send` and `AsAddressed`/`asAddressed`; +- **reference** is a test-only interpreter that shows the oracle can be met. It + is never evidence about a runtime. -## The carrier model +Carriers, dispatchers, `Forward`, `Mount` and `At` are always bitruntime's. + +## The carrier test model + +This section describes how the drivers compose trees across carriers. **It is a +test model, not part of the contract.** Remote structural discovery is not +specified, and bitruntime has no public facility yet for either adapter below. The far side serves its tree through a bitruntime dispatcher that borrows the -endpoint. It registers an exact route for every node whose keys are UTF-8, and -each route is bound to that node's own Wire. The near side declares the same -structure. Each of its own Wires sends at the corresponding far path, so the -near tree's `own`, `children`, selection and reconstruction are local. The +endpoint. There is one exact route for every node whose keys are UTF-8, and each +route is bound to that node's own Wire. The near side declares the same +structure, and each of its own Wires sends at the corresponding far path. So the +near tree's `own`, `children`, selection and reconstruction are local, and the carrier sees only paths. -Four observations follow, and the cases state each one: - -- **The served tree needs a nonempty prefix.** `bitwire/1` refuses a request at - a peer root's empty path. The far tree is served under `servedAt` (`["t"]`), - and an addressed request at `[]` is refused at admission. -- **A carrier path names a position, not a node.** Addressed access cannot - reveal that two far positions share a node. The near tree therefore binds each - position separately. When the far side replaces one of two positions that - shared a node, the other position keeps reaching the original node. -- **Missing and refusing stay distinct, as profile outcomes.** A path absent - from the near tree is missing locally and sends nothing. A far path with no - node answers `method_not_found`. A far node whose own refuses answers - `internal`. Both codes come from the - [pinned profile](https://github.com/Bitspark/nightseam/blob/5cc9723a24646c40ed1861f892b2b23eb6d785d7/docs/wire/profile.md). - A binary key outside the UTF-8 image cannot be named over the carrier at all. -- **Captured cancellation survives replacement.** The dispatcher captures each - request's traversal on its invocation. After the far node that admitted a - request is replaced and re-served, the requester's cancel frame, sent through - the same near tree position, reaches the original node and not its - replacement. A late reply from the original node reaches the original return - capability. - -Two adapters in the drivers are **test-only**. bitruntime v0.2.0 has no public -facility for either, and -[bitruntime#15](https://github.com/Bitspark/bitruntime/issues/15) tracks them: +Two adapters in the drivers are **test-only**. Everything they call is public +bitruntime API: - `bind`: an addressless `Wire` that sends at one fixed `AddressedWire` path; - `serve`: exact dispatcher registration of a tree's UTF-8 nodes, each bound to its node. -Everything the adapters call is public bitruntime API. The carrier results are -therefore evidence that bitruntime's carriers, dispatcher and trees compose -lawfully. They are not evidence of a shipped serving or binding API. +[bitruntime#15](https://github.com/Bitspark/bitruntime/issues/15) tracks +production facilities for both. The carrier results are therefore evidence that +bitruntime's carriers, dispatcher and trees compose lawfully. They are not +evidence of a shipped serving or binding API. + +The cases state what follows from this model: + +- **The served tree has a nonempty prefix.** `bitwire/1` refuses a request at a + peer root's empty path, so the far tree is served under `servedAt` (`["t"]`). + An addressed request at `[]` is refused at admission. +- **A carrier path names a position, not a node.** Addressed access cannot + reveal that two far positions share a node, so the near tree binds each + position. If the far side replaces one of two positions that shared a node, + the other position still reaches the original. +- **Missing and refusing stay distinct.** + - A near path that does not exist is missing, and nothing is sent. + - A far path without a route is answered `method_not_found` by the + dispatcher. That is the profile's code for "no handler". + - A far node whose own refuses records its refusal, and the request is + answered `internal`. That is the profile's code for a handler that failed + with a non-public error. It arises because `serve` lets the refusal fail the + request: in TypeScript the error is thrown from the receiver and the carrier + answers it; in Go receivers return nothing, so `serve` answers through + `core.Respond`. + - A binary key cannot be named over the carrier at all. +- **Captured cancellation survives replacement.** + - The dispatcher captures each request's traversal on its invocation. After + the far node that admitted a request is replaced and re-served, the + requester's cancel frame, sent through the same near position, reaches the + original node and not its replacement. + - A late reply from the original node reaches the original return capability, + even after the serving composition was torn down. + +`method_not_found` and `internal` are defined in the pinned +[profile](https://github.com/Bitspark/nightseam/blob/5cc9723a24646c40ed1861f892b2b23eb6d785d7/docs/wire/profile.md). ## Unlawful realizations are rejected The TypeScript driver also runs deliberately unlawful realizations. The runner -requires each one to fail at least one case: +requires each to fail the case aimed at it: -| Realization | Violation | Rejected by, among others | +| Realization | Violation | Must fail | | --- | --- | --- | -| `fallback` | A missing descendant is sent to its nearest ancestor | `missing-never-falls-back` | -| `wrapping-own` | The own capability is replaced by a forwarding wrapper | every identity and reconstruction case | -| `normalizing` | UTF-8 keys are NFC-normalized | construction of the two spellings of é | -| `fabricating` | A missing path selects a fabricated refusing node | `refusing-versus-missing` | -| `latin1-bridge` | The bridge names byte key `ff` with `"ÿ"` | `bridge-exact-utf8-image` | -| `incomplete-children` | `children()` omits the empty key | every `structure` render | -| `retargeting-serve` | The far side routes by the tree current at delivery | `carrier-cancel-across-replacement` | +| `fallback` | A missing descendant is sent to its nearest ancestor. | `missing-never-falls-back` | +| `wrapping-own` | The own capability is replaced by a forwarding wrapper. | `root-cut-reconstruction` | +| `normalizing` | UTF-8 keys are NFC-normalized. | `own-and-descendants` | +| `fabricating` | A missing path selects a fabricated refusing node. | `refusing-versus-missing` | +| `latin1-bridge` | The bridge names byte key `ff` with `"ÿ"`. | `bridge-exact-utf8-image` | +| `lossy-bridge` | The bridge encodes a lone surrogate as U+FFFD. | `bridge-exact-utf8-image` | +| `incomplete-children` | `children()` omits the empty key. | `own-and-descendants` | +| `retargeting-serve` | The far side routes by the tree that is current at delivery. | `carrier-cancel-across-replacement` | ## Disposition of the declared cases [`disposition.json`](disposition.json) pins the historical fixture and both gap -ledgers by SHA-256. A changed historical file fails `scripts/check.mjs` until -someone revisits the disposition. The mapping: - -- **37 of the 39 cases** have current `structure`, `bridge` or `carrier` - counterparts. Several meanings are sharpened: a path that the addressed - interpretation answered with a refusal is now *missing*, while a present node - whose own refuses stays *refused*. -- **10 cases** keep a historical addressed part. Two of them have nothing else: - `guard-around-composite` and `guarded-child-complete-access`. The reasons, - recorded as limitations: - - *suffix delivery* (6 cases): an opaque child received the rest of the path. - In a full tree that path is missing, and suffix delivery survives only as - AddressedWire prefix binding. - - *path-observing interception* (4 cases): guards saw descendant paths, but an - own Wire never sees a path. - - *addressed views* (1 case): at(guard, [k]) produced an addressed view that - repeated the guard's check when composed again. -- **The 19 recorded gaps:** - - The origin-bearing construction gap (16 cases) and the conflicting and - missing-child gaps are met structurally by public `Compose`/`compose`. - - Invalid segments changed meaning: no tree key is invalid, and the bridge - refuses ill-formed segments. - - The ledger entries stay accurate about the addressed `Mount`, which is a - child-only routing operator, not tree construction. -- **The two recorded limitations:** - - Endpoint-typed children are dissolved: tree children are nodes with a - send-only own. - - Owner-retained parts are superseded by public decomposition. A caller that - must not see structure receives the bridge instead. +ledgers by SHA-256. A changed historical file fails `scripts/check.mjs` until the +disposition is revisited. + +**37 of the 39 cases have current counterparts:** `structure`, `bridge` or +`carrier` cases. +- Several meanings are sharpened. A path the addressed interpretation answered + with a refusal is now *missing*, while a present node whose own refuses stays + *refused*. +- Each row whose meaning changed says so. + +**10 cases keep a historical addressed part:** +- *suffix delivery* (6 cases): an opaque child received the rest of the path. In + a full tree that path is missing, and suffix delivery survives only as + AddressedWire prefix binding. +- *path-observing interception* (4 cases): guards saw descendant paths, which an + own Wire never does. +- *addressed views* (1 case). + +Two of these cases have nothing else: `guard-around-composite` and +`guarded-child-complete-access`. + +**The 19 recorded gaps:** +- The origin-bearing construction gap (16 cases) and the conflicting and + missing-child gaps are met structurally by public `Compose`/`compose`. The + origin-bearing gap's carrier members are met through the test-only adapters + above. +- The invalid-segment gap changed meaning: no tree key is invalid, and the bridge + refuses ill-formed segments. +- The ledger entries stay accurate about the addressed `Mount`, which is a + child-only routing operator, not tree construction. + +**The two recorded limitations:** +- Endpoint-typed children are dissolved: tree children are nodes with a + send-only own. +- Owner-retained parts are superseded by public decomposition. A caller that must + not see structure receives the bridge instead. ## Limits -Only Go and TypeScript run. The other six languages have declarations, not -runtimes. The schedules are serial, and the carriers are connected, ordered -and fault-free. Concurrency, faults, backpressure and closure codes belong to -the carrier contract -([#54](https://github.com/Bitspark/bitwire/issues/54)). Invocation retirement -is [#20](https://github.com/Bitspark/bitwire/issues/20), and received-context -evidence is [#55](https://github.com/Bitspark/bitwire/issues/55). Remote -structural discovery is not specified. A near tree is declared, never inferred -from an opaque router. Consumer acceptance belongs to -[bitsystem3#11](https://github.com/Bitspark/bitsystem3/issues/11) and -[bittree#53](https://github.com/Bitspark/bittree/issues/53). +- **Languages:** only Go and TypeScript run. The other six languages have + declarations, not runtimes. The unlawful realizations are TypeScript only. +- **Schedules:** they are serial, and the carriers are connected, ordered and + fault-free. +- **Carrier behavior:** concurrency, faults, backpressure and closure codes + belong to the carrier contract, + [#54](https://github.com/Bitspark/bitwire/issues/54). +- **Invocation retirement:** [#20](https://github.com/Bitspark/bitwire/issues/20). +- **Received-context evidence:** + [#55](https://github.com/Bitspark/bitwire/issues/55). +- **Consumer acceptance:** + [bitsystem3#11](https://github.com/Bitspark/bitsystem3/issues/11) and + [bittree#53](https://github.com/Bitspark/bittree/issues/53). diff --git a/conformance/wiretree/cases.json b/conformance/wiretree/cases.json index c3128b5..b8a6a2d 100644 --- a/conformance/wiretree/cases.json +++ b/conformance/wiretree/cases.json @@ -8,52 +8,327 @@ "own": "A name creates one stateful instrumented primitive per case, shared by every node declaring it; null is a primitive that refuses every send.", "render": "{\"own\": [name, instance] | null, \"children\": [[key, render], ...]}; children compare as a set of exact keys. {\"$render\": name} refers to renders.", "same": "Whether the two selected nodes hold the identical own capability, which is what shared primitive state depends on.", - "servedAt": "Carrier cases serve the far tree under this nonempty addressed prefix, because bitwire/1 refuses a request at a peer root's empty path. A tree path p is carrier path servedAt ++ utf8(p)." + "servedAt": "Carrier cases serve the far tree under this nonempty addressed prefix, because bitwire/1 refuses a request at a peer root's empty path. A tree path p is carrier path servedAt ++ utf8(p).", + "delivered": "[\"delivered\", name, count]: the named primitive admitted a message; count is that primitive instance's own number of admissions, from 1.", + "instances": "Instances of a name are numbered from 1 in creation order within a case; a fresh or copied primitive takes the next number.", + "refused": "[\"refused\"]: the selected node's own primitive was invoked and refused; the primitive records this itself. [\"missing\"]: selection found no node, no primitive was invoked, and the derived send was refused. [\"unreached\"]: the addressed bridge refused without invoking any primitive.", + "illFormed": "bridgeInvalid sends one segment that is not a Unicode scalar string: the UTF-16 code units in utf16 where strings are UTF-16, or the bytes in utf8 where strings are bytes. A lossy conversion of either would reach the U+FFFD or ff key.", + "foreign": "foreign: true adds, under key foreign, a child implemented by the driver rather than the realization: own primitive foreign with one child inner (own inner)." }, - "servedAt": ["t"], + "servedAt": [ + "t" + ], "declarations": [ - { "id": "root", "own": "root", "children": [ - ["", "blank"], ["a", "branch"], ["a/b", "slash"], ["alias", "leaf"], ["empty", "empty"], - ["é", "acute"], ["é", "combining"], [{ "hex": "ff" }, "binary"] - ] }, - { "id": "rroot", "own": null, "children": [ - ["", "rblank"], ["a", "rbranch"], ["a/b", "slash"], ["alias", "leaf"], ["empty", "empty"], - ["é", "acute"], ["é", "combining"] - ] }, - { "id": "blank", "own": "blank", "children": [["", "blankLeaf"]] }, - { "id": "rblank", "own": null, "children": [["", "blankLeaf"]] }, - { "id": "branch", "own": "branch", "children": [["b", "leaf"]] }, - { "id": "rbranch", "own": null, "children": [["b", "leaf"]] }, - { "id": "empty", "own": null }, - { "id": "leaf", "own": "leaf" }, - { "id": "blankLeaf", "own": "blank-leaf" }, - { "id": "slash", "own": "slash" }, - { "id": "acute", "own": "acute" }, - { "id": "combining", "own": "combining" }, - { "id": "binary", "own": "binary" }, - { "id": "replacement", "own": "replacement" }, - { "id": "extra", "own": "extra" } + { + "id": "root", + "own": "root", + "children": [ + ["", "blank"], + ["a", "branch"], + ["a/b", "slash"], + ["alias", "leaf"], + ["empty", "empty"], + ["é", "acute"], + ["é", "combining"], + [ + { + "hex": "ff" + }, + "binary" + ], + ["�", "replacementChar"] + ] + }, + { + "id": "rroot", + "own": null, + "children": [ + ["", "rblank"], + ["a", "rbranch"], + ["a/b", "slash"], + ["alias", "leaf"], + ["empty", "empty"], + ["é", "acute"], + ["é", "combining"] + ] + }, + { + "id": "blank", + "own": "blank", + "children": [ + ["", "blankLeaf"] + ] + }, + { + "id": "rblank", + "own": null, + "children": [ + ["", "blankLeaf"] + ] + }, + { + "id": "branch", + "own": "branch", + "children": [ + ["b", "leaf"] + ] + }, + { + "id": "rbranch", + "own": null, + "children": [ + ["b", "leaf"] + ] + }, + { + "id": "empty", + "own": null + }, + { + "id": "leaf", + "own": "leaf" + }, + { + "id": "blankLeaf", + "own": "blank-leaf" + }, + { + "id": "slash", + "own": "slash" + }, + { + "id": "acute", + "own": "acute" + }, + { + "id": "combining", + "own": "combining" + }, + { + "id": "binary", + "own": "binary" + }, + { + "id": "replacement", + "own": "replacement" + }, + { + "id": "extra", + "own": "extra" + }, + { + "id": "replacementChar", + "own": "fffd" + } ], "renders": { - "root": { "own": ["root", 1], "children": [ - ["", { "own": ["blank", 1], "children": [["", { "own": ["blank-leaf", 1], "children": [] }]] }], - ["a", { "own": ["branch", 1], "children": [["b", { "own": ["leaf", 1], "children": [] }]] }], - ["a/b", { "own": ["slash", 1], "children": [] }], - ["alias", { "own": ["leaf", 1], "children": [] }], - ["empty", { "own": null, "children": [] }], - ["é", { "own": ["acute", 1], "children": [] }], - ["é", { "own": ["combining", 1], "children": [] }], - [{ "hex": "ff" }, { "own": ["binary", 1], "children": [] }] - ] }, - "rroot": { "own": null, "children": [ - ["", { "own": null, "children": [["", { "own": ["blank-leaf", 1], "children": [] }]] }], - ["a", { "own": null, "children": [["b", { "own": ["leaf", 1], "children": [] }]] }], - ["a/b", { "own": ["slash", 1], "children": [] }], - ["alias", { "own": ["leaf", 1], "children": [] }], - ["empty", { "own": null, "children": [] }], - ["é", { "own": ["acute", 1], "children": [] }], - ["é", { "own": ["combining", 1], "children": [] }] - ] } + "root": { + "own": [ + "root", + 1 + ], + "children": [ + [ + "", + { + "own": [ + "blank", + 1 + ], + "children": [ + [ + "", + { + "own": [ + "blank-leaf", + 1 + ], + "children": [] + } + ] + ] + } + ], + [ + "a", + { + "own": [ + "branch", + 1 + ], + "children": [ + [ + "b", + { + "own": [ + "leaf", + 1 + ], + "children": [] + } + ] + ] + } + ], + [ + "a/b", + { + "own": [ + "slash", + 1 + ], + "children": [] + } + ], + [ + "alias", + { + "own": [ + "leaf", + 1 + ], + "children": [] + } + ], + [ + "empty", + { + "own": null, + "children": [] + } + ], + [ + "é", + { + "own": [ + "acute", + 1 + ], + "children": [] + } + ], + [ + "é", + { + "own": [ + "combining", + 1 + ], + "children": [] + } + ], + [ + { + "hex": "ff" + }, + { + "own": [ + "binary", + 1 + ], + "children": [] + } + ], + [ + "�", + { + "own": [ + "fffd", + 1 + ], + "children": [] + } + ] + ] + }, + "rroot": { + "own": null, + "children": [ + [ + "", + { + "own": null, + "children": [ + [ + "", + { + "own": [ + "blank-leaf", + 1 + ], + "children": [] + } + ] + ] + } + ], + [ + "a", + { + "own": null, + "children": [ + [ + "b", + { + "own": [ + "leaf", + 1 + ], + "children": [] + } + ] + ] + } + ], + [ + "a/b", + { + "own": [ + "slash", + 1 + ], + "children": [] + } + ], + [ + "alias", + { + "own": [ + "leaf", + 1 + ], + "children": [] + } + ], + [ + "empty", + { + "own": null, + "children": [] + } + ], + [ + "é", + { + "own": [ + "acute", + 1 + ], + "children": [] + } + ], + [ + "é", + { + "own": [ + "combining", + 1 + ], + "children": [] + } + ] + ] + } }, "cases": [ { @@ -61,39 +336,162 @@ "family": "structure", "root": "root", "steps": [ - { "op": "structure", "path": [] }, - { "op": "send", "path": [] }, - { "op": "send", "path": ["a"] }, - { "op": "send", "path": ["a", "b"] }, - { "op": "send", "path": ["alias"] }, - { "op": "send", "path": [""] }, - { "op": "send", "path": ["", ""] }, - { "op": "send", "path": ["a/b"] }, - { "op": "send", "path": ["é"] }, - { "op": "send", "path": ["é"] }, - { "op": "send", "path": [{ "hex": "ff" }] }, - { "op": "send", "path": ["empty"] }, - { "op": "send", "path": ["alias", "x"] }, - { "op": "send", "path": ["a", "b", "x", "y"] }, - { "op": "send", "path": ["ÿ"] } + { + "op": "structure", + "path": [] + }, + { + "op": "send", + "path": [] + }, + { + "op": "send", + "path": [ + "a" + ] + }, + { + "op": "send", + "path": ["a", "b"] + }, + { + "op": "send", + "path": [ + "alias" + ] + }, + { + "op": "send", + "path": [ + "" + ] + }, + { + "op": "send", + "path": ["", ""] + }, + { + "op": "send", + "path": [ + "a/b" + ] + }, + { + "op": "send", + "path": [ + "é" + ] + }, + { + "op": "send", + "path": [ + "é" + ] + }, + { + "op": "send", + "path": [ + { + "hex": "ff" + } + ] + }, + { + "op": "send", + "path": [ + "empty" + ] + }, + { + "op": "send", + "path": ["alias", "x"] + }, + { + "op": "send", + "path": [ + "a", + "b", + "x", + "y" + ] + }, + { + "op": "send", + "path": [ + "ÿ" + ] + } ], "expected": { "trace": [ - ["structure", { "$render": "root" }], - ["delivered", "root", 1], - ["delivered", "branch", 1], - ["delivered", "leaf", 1], - ["delivered", "leaf", 2], - ["delivered", "blank", 1], - ["delivered", "blank-leaf", 1], - ["delivered", "slash", 1], - ["delivered", "acute", 1], - ["delivered", "combining", 1], - ["delivered", "binary", 1], - ["refused"], - ["missing"], - ["missing"], - ["missing"] + [ + "structure", + { + "$render": "root" + } + ], + [ + "delivered", + "root", + 1 + ], + [ + "delivered", + "branch", + 1 + ], + [ + "delivered", + "leaf", + 1 + ], + [ + "delivered", + "leaf", + 2 + ], + [ + "delivered", + "blank", + 1 + ], + [ + "delivered", + "blank-leaf", + 1 + ], + [ + "delivered", + "slash", + 1 + ], + [ + "delivered", + "acute", + 1 + ], + [ + "delivered", + "combining", + 1 + ], + [ + "delivered", + "binary", + 1 + ], + [ + "refused" + ], + [ + "missing" + ], + [ + "missing" + ], + [ + "missing" + ] ], "partsExact": true, "unchanged": true @@ -104,37 +502,143 @@ "family": "structure", "root": "rroot", "steps": [ - { "op": "structure", "path": [] }, - { "op": "send", "path": [] }, - { "op": "send", "path": ["a"] }, - { "op": "send", "path": ["a", "b"] }, - { "op": "send", "path": ["alias"] }, - { "op": "send", "path": [""] }, - { "op": "send", "path": ["", ""] }, - { "op": "send", "path": ["a/b"] }, - { "op": "send", "path": ["é"] }, - { "op": "send", "path": ["é"] }, - { "op": "send", "path": ["empty"] }, - { "op": "send", "path": ["missing"] }, - { "op": "send", "path": ["alias", "x"] }, - { "op": "send", "path": ["a", "b", "x", "y"] } + { + "op": "structure", + "path": [] + }, + { + "op": "send", + "path": [] + }, + { + "op": "send", + "path": [ + "a" + ] + }, + { + "op": "send", + "path": ["a", "b"] + }, + { + "op": "send", + "path": [ + "alias" + ] + }, + { + "op": "send", + "path": [ + "" + ] + }, + { + "op": "send", + "path": ["", ""] + }, + { + "op": "send", + "path": [ + "a/b" + ] + }, + { + "op": "send", + "path": [ + "é" + ] + }, + { + "op": "send", + "path": [ + "é" + ] + }, + { + "op": "send", + "path": [ + "empty" + ] + }, + { + "op": "send", + "path": [ + "missing" + ] + }, + { + "op": "send", + "path": ["alias", "x"] + }, + { + "op": "send", + "path": [ + "a", + "b", + "x", + "y" + ] + } ], "expected": { "trace": [ - ["structure", { "$render": "rroot" }], - ["refused"], - ["refused"], - ["delivered", "leaf", 1], - ["delivered", "leaf", 2], - ["refused"], - ["delivered", "blank-leaf", 1], - ["delivered", "slash", 1], - ["delivered", "acute", 1], - ["delivered", "combining", 1], - ["refused"], - ["missing"], - ["missing"], - ["missing"] + [ + "structure", + { + "$render": "rroot" + } + ], + [ + "refused" + ], + [ + "refused" + ], + [ + "delivered", + "leaf", + 1 + ], + [ + "delivered", + "leaf", + 2 + ], + [ + "refused" + ], + [ + "delivered", + "blank-leaf", + 1 + ], + [ + "delivered", + "slash", + 1 + ], + [ + "delivered", + "acute", + 1 + ], + [ + "delivered", + "combining", + 1 + ], + [ + "refused" + ], + [ + "missing" + ], + [ + "missing" + ], + [ + "missing" + ] ], "partsExact": true, "unchanged": true @@ -145,37 +649,211 @@ "family": "structure", "root": "root", "steps": [ - { "op": "send", "path": [], "selections": [[]] }, - { "op": "send", "path": [], "selections": [[], ["a"], [], ["b"]] }, - { "op": "send", "path": ["b"], "selections": [["a"]] }, - { "op": "send", "path": [], "selections": [["a", "b"]] }, - { "op": "send", "path": [], "selections": [["a"]] }, - { "op": "send", "path": [""], "selections": [[""]] }, - { "op": "send", "path": [], "selections": [[""]] }, - { "op": "send", "path": ["x"], "selections": [["alias"]] }, - { "op": "send", "path": [], "selections": [["alias"], ["x"], []] }, - { "op": "send", "path": [], "selections": [["missing"]] }, - { "op": "send", "path": [], "selections": [["a"], ["missing"]] }, - { "op": "send", "path": [], "selections": [["empty"], []] }, - { "op": "send", "path": [], "selections": [[{ "hex": "ff" }]] }, - { "op": "same", "paths": [["a", "b"], ["alias"]] } + { + "op": "send", + "path": [], + "selections": [ + [] + ] + }, + { + "op": "send", + "path": [], + "selections": [ + [], + [ + "a" + ], + [], + [ + "b" + ] + ] + }, + { + "op": "send", + "path": [ + "b" + ], + "selections": [ + [ + "a" + ] + ] + }, + { + "op": "send", + "path": [], + "selections": [ + ["a", "b"] + ] + }, + { + "op": "send", + "path": [], + "selections": [ + [ + "a" + ] + ] + }, + { + "op": "send", + "path": [ + "" + ], + "selections": [ + [ + "" + ] + ] + }, + { + "op": "send", + "path": [], + "selections": [ + [ + "" + ] + ] + }, + { + "op": "send", + "path": [ + "x" + ], + "selections": [ + [ + "alias" + ] + ] + }, + { + "op": "send", + "path": [], + "selections": [ + [ + "alias" + ], + [ + "x" + ], + [] + ] + }, + { + "op": "send", + "path": [], + "selections": [ + [ + "missing" + ] + ] + }, + { + "op": "send", + "path": [], + "selections": [ + [ + "a" + ], + [ + "missing" + ] + ] + }, + { + "op": "send", + "path": [], + "selections": [ + [ + "empty" + ], + [] + ] + }, + { + "op": "send", + "path": [], + "selections": [ + [ + { + "hex": "ff" + } + ] + ] + }, + { + "op": "same", + "paths": [ + ["a", "b"], + [ + "alias" + ] + ] + } ], "expected": { "trace": [ - ["delivered", "root", 1], - ["delivered", "leaf", 1], - ["delivered", "leaf", 2], - ["delivered", "leaf", 3], - ["delivered", "branch", 1], - ["delivered", "blank-leaf", 1], - ["delivered", "blank", 1], - ["missing"], - ["missing"], - ["missing"], - ["missing"], - ["refused"], - ["delivered", "binary", 1], - ["same", true] + [ + "delivered", + "root", + 1 + ], + [ + "delivered", + "leaf", + 1 + ], + [ + "delivered", + "leaf", + 2 + ], + [ + "delivered", + "leaf", + 3 + ], + [ + "delivered", + "branch", + 1 + ], + [ + "delivered", + "blank-leaf", + 1 + ], + [ + "delivered", + "blank", + 1 + ], + [ + "missing" + ], + [ + "missing" + ], + [ + "missing" + ], + [ + "missing" + ], + [ + "refused" + ], + [ + "delivered", + "binary", + 1 + ], + [ + "same", + true + ] ], "partsExact": true, "unchanged": true @@ -186,27 +864,95 @@ "family": "structure", "root": "root", "steps": [ - { "op": "send", "path": ["missing"] }, - { "op": "send", "path": ["a", "missing"] }, - { "op": "send", "path": ["empty"] }, - { "op": "send", "path": ["empty", "x"] }, - { "op": "send", "path": ["", "missing"] }, - { "op": "send", "path": [], "selections": [["missing"]] }, - { "op": "send", "path": [], "selections": [["a"], ["missing"]] }, - { "op": "send", "path": [] }, - { "op": "send", "path": ["a"] } + { + "op": "send", + "path": [ + "missing" + ] + }, + { + "op": "send", + "path": ["a", "missing"] + }, + { + "op": "send", + "path": [ + "empty" + ] + }, + { + "op": "send", + "path": ["empty", "x"] + }, + { + "op": "send", + "path": ["", "missing"] + }, + { + "op": "send", + "path": [], + "selections": [ + [ + "missing" + ] + ] + }, + { + "op": "send", + "path": [], + "selections": [ + [ + "a" + ], + [ + "missing" + ] + ] + }, + { + "op": "send", + "path": [] + }, + { + "op": "send", + "path": [ + "a" + ] + } ], "expected": { "trace": [ - ["missing"], - ["missing"], - ["refused"], - ["missing"], - ["missing"], - ["missing"], - ["missing"], - ["delivered", "root", 1], - ["delivered", "branch", 1] + [ + "missing" + ], + [ + "missing" + ], + [ + "refused" + ], + [ + "missing" + ], + [ + "missing" + ], + [ + "missing" + ], + [ + "missing" + ], + [ + "delivered", + "root", + 1 + ], + [ + "delivered", + "branch", + 1 + ] ], "partsExact": true, "unchanged": true @@ -217,19 +963,57 @@ "family": "structure", "root": "root", "steps": [ - { "op": "structure", "path": ["empty"] }, - { "op": "structure", "path": ["missing"] }, - { "op": "send", "path": ["empty"] }, - { "op": "send", "path": ["missing"] }, - { "op": "structure", "path": [] } + { + "op": "structure", + "path": [ + "empty" + ] + }, + { + "op": "structure", + "path": [ + "missing" + ] + }, + { + "op": "send", + "path": [ + "empty" + ] + }, + { + "op": "send", + "path": [ + "missing" + ] + }, + { + "op": "structure", + "path": [] + } ], "expected": { "trace": [ - ["structure", { "own": null, "children": [] }], + [ + "structure", + { + "own": null, + "children": [] + } + ], ["structure", "missing"], - ["refused"], - ["missing"], - ["structure", { "$render": "root" }] + [ + "refused" + ], + [ + "missing" + ], + [ + "structure", + { + "$render": "root" + } + ] ], "partsExact": true, "unchanged": true @@ -240,20 +1024,93 @@ "family": "structure", "root": "root", "steps": [ - { "op": "send", "path": ["a", "b"] }, - { "op": "rebuild", "keep": [[""], ["a"], ["a/b"], ["alias"], ["empty"], ["é"], ["é"], [{ "hex": "ff" }]] }, - { "op": "send", "path": ["alias"] }, - { "op": "send", "path": [] }, - { "op": "send", "path": ["a"] }, - { "op": "structure", "path": [] } + { + "op": "send", + "path": ["a", "b"] + }, + { + "op": "rebuild", + "keep": [ + [ + "" + ], + [ + "a" + ], + [ + "a/b" + ], + [ + "alias" + ], + [ + "empty" + ], + [ + "é" + ], + [ + "é" + ], + [ + { + "hex": "ff" + } + ], + [ + "�" + ] + ] + }, + { + "op": "send", + "path": [ + "alias" + ] + }, + { + "op": "send", + "path": [] + }, + { + "op": "send", + "path": [ + "a" + ] + }, + { + "op": "structure", + "path": [] + } ], "expected": { "trace": [ - ["delivered", "leaf", 1], - ["delivered", "leaf", 2], - ["delivered", "root", 1], - ["delivered", "branch", 1], - ["structure", { "$render": "root" }] + [ + "delivered", + "leaf", + 1 + ], + [ + "delivered", + "leaf", + 2 + ], + [ + "delivered", + "root", + 1 + ], + [ + "delivered", + "branch", + 1 + ], + [ + "structure", + { + "$render": "root" + } + ] ], "partsExact": true, "unchanged": true @@ -264,32 +1121,138 @@ "family": "structure", "root": "root", "steps": [ - { "op": "send", "path": ["a", "b"] }, - { "op": "rebuild", "keep": [] }, - { "op": "send", "path": [] }, - { "op": "rebuild", "keep": [["a"]] }, - { "op": "send", "path": ["alias"] }, - { "op": "rebuild", "keep": [[""], ["empty"]] }, - { "op": "send", "path": ["", ""] }, - { "op": "rebuild", "keep": [[]] }, - { "op": "send", "path": ["a"] }, - { "op": "rebuild", "keep": [["a", "b"]] }, - { "op": "send", "path": ["a", "b"] }, - { "op": "send", "path": [{ "hex": "ff" }] }, - { "op": "same", "paths": [["a", "b"], ["alias"]] }, - { "op": "structure", "path": [] } + { + "op": "send", + "path": ["a", "b"] + }, + { + "op": "rebuild", + "keep": [] + }, + { + "op": "send", + "path": [] + }, + { + "op": "rebuild", + "keep": [ + [ + "a" + ] + ] + }, + { + "op": "send", + "path": [ + "alias" + ] + }, + { + "op": "rebuild", + "keep": [ + [ + "" + ], + [ + "empty" + ] + ] + }, + { + "op": "send", + "path": ["", ""] + }, + { + "op": "rebuild", + "keep": [ + [] + ] + }, + { + "op": "send", + "path": [ + "a" + ] + }, + { + "op": "rebuild", + "keep": [ + ["a", "b"] + ] + }, + { + "op": "send", + "path": ["a", "b"] + }, + { + "op": "send", + "path": [ + { + "hex": "ff" + } + ] + }, + { + "op": "same", + "paths": [ + ["a", "b"], + [ + "alias" + ] + ] + }, + { + "op": "structure", + "path": [] + } ], "expected": { "trace": [ - ["delivered", "leaf", 1], - ["delivered", "root", 1], - ["delivered", "leaf", 2], - ["delivered", "blank-leaf", 1], - ["delivered", "branch", 1], - ["delivered", "leaf", 3], - ["delivered", "binary", 1], - ["same", true], - ["structure", { "$render": "root" }] + [ + "delivered", + "leaf", + 1 + ], + [ + "delivered", + "root", + 1 + ], + [ + "delivered", + "leaf", + 2 + ], + [ + "delivered", + "blank-leaf", + 1 + ], + [ + "delivered", + "branch", + 1 + ], + [ + "delivered", + "leaf", + 3 + ], + [ + "delivered", + "binary", + 1 + ], + [ + "same", + true + ], + [ + "structure", + { + "$render": "root" + } + ] ], "partsExact": true, "unchanged": true @@ -300,25 +1263,93 @@ "family": "structure", "root": "rroot", "steps": [ - { "op": "send", "path": ["a", "b"] }, - { "op": "rebuild", "keep": [] }, - { "op": "send", "path": ["alias"] }, - { "op": "rebuild", "keep": [["a"]] }, - { "op": "send", "path": ["", ""] }, - { "op": "rebuild", "keep": [[""], ["empty"]] }, - { "op": "send", "path": ["a", "b"] }, - { "op": "rebuild", "keep": [[]] }, - { "op": "send", "path": [] }, - { "op": "structure", "path": [] } + { + "op": "send", + "path": ["a", "b"] + }, + { + "op": "rebuild", + "keep": [] + }, + { + "op": "send", + "path": [ + "alias" + ] + }, + { + "op": "rebuild", + "keep": [ + [ + "a" + ] + ] + }, + { + "op": "send", + "path": ["", ""] + }, + { + "op": "rebuild", + "keep": [ + [ + "" + ], + [ + "empty" + ] + ] + }, + { + "op": "send", + "path": ["a", "b"] + }, + { + "op": "rebuild", + "keep": [ + [] + ] + }, + { + "op": "send", + "path": [] + }, + { + "op": "structure", + "path": [] + } ], "expected": { "trace": [ - ["delivered", "leaf", 1], - ["delivered", "leaf", 2], - ["delivered", "blank-leaf", 1], - ["delivered", "leaf", 3], - ["refused"], - ["structure", { "$render": "rroot" }] + [ + "delivered", + "leaf", + 1 + ], + [ + "delivered", + "leaf", + 2 + ], + [ + "delivered", + "blank-leaf", + 1 + ], + [ + "delivered", + "leaf", + 3 + ], + [ + "refused" + ], + [ + "structure", + { + "$render": "rroot" + } + ] ], "partsExact": true, "unchanged": true @@ -329,18 +1360,68 @@ "family": "structure", "root": "root", "steps": [ - { "op": "send", "path": [] }, - { "op": "own", "path": [], "own": null }, - { "op": "send", "path": [] }, - { "op": "send", "path": ["alias"] }, - { "op": "structure", "path": ["a"] } + { + "op": "send", + "path": [] + }, + { + "op": "own", + "path": [], + "own": null + }, + { + "op": "send", + "path": [] + }, + { + "op": "send", + "path": [ + "alias" + ] + }, + { + "op": "structure", + "path": [ + "a" + ] + } ], "expected": { "trace": [ - ["delivered", "root", 1], - ["refused"], - ["delivered", "leaf", 1], - ["structure", { "own": ["branch", 1], "children": [["b", { "own": ["leaf", 1], "children": [] }]] }] + [ + "delivered", + "root", + 1 + ], + [ + "refused" + ], + [ + "delivered", + "leaf", + 1 + ], + [ + "structure", + { + "own": [ + "branch", + 1 + ], + "children": [ + [ + "b", + { + "own": [ + "leaf", + 1 + ], + "children": [] + } + ] + ] + } + ] ], "partsExact": true, "unchanged": true @@ -351,20 +1432,77 @@ "family": "structure", "root": "root", "steps": [ - { "op": "send", "path": [] }, - { "op": "send", "path": [] }, - { "op": "own", "path": [], "own": "fresh" }, - { "op": "send", "path": [] }, - { "op": "send", "path": ["a", "b"] }, - { "op": "structure", "path": ["a"] } + { + "op": "send", + "path": [] + }, + { + "op": "send", + "path": [] + }, + { + "op": "own", + "path": [], + "own": "fresh" + }, + { + "op": "send", + "path": [] + }, + { + "op": "send", + "path": ["a", "b"] + }, + { + "op": "structure", + "path": [ + "a" + ] + } ], "expected": { "trace": [ - ["delivered", "root", 1], - ["delivered", "root", 2], - ["delivered", "root", 1], - ["delivered", "leaf", 1], - ["structure", { "own": ["branch", 1], "children": [["b", { "own": ["leaf", 1], "children": [] }]] }] + [ + "delivered", + "root", + 1 + ], + [ + "delivered", + "root", + 2 + ], + [ + "delivered", + "root", + 1 + ], + [ + "delivered", + "leaf", + 1 + ], + [ + "structure", + { + "own": [ + "branch", + 1 + ], + "children": [ + [ + "b", + { + "own": [ + "leaf", + 1 + ], + "children": [] + } + ] + ] + } + ] ], "partsExact": true, "unchanged": true @@ -375,22 +1513,76 @@ "family": "structure", "root": "root", "steps": [ - { "op": "send", "path": ["a", "b"] }, - { "op": "send", "path": ["alias"] }, - { "op": "rebuild", "keep": [] }, - { "op": "send", "path": ["a", "b"] }, - { "op": "send", "path": ["alias"] }, - { "op": "same", "paths": [["a", "b"], ["alias"]] }, - { "op": "structure", "path": [] } + { + "op": "send", + "path": ["a", "b"] + }, + { + "op": "send", + "path": [ + "alias" + ] + }, + { + "op": "rebuild", + "keep": [] + }, + { + "op": "send", + "path": ["a", "b"] + }, + { + "op": "send", + "path": [ + "alias" + ] + }, + { + "op": "same", + "paths": [ + ["a", "b"], + [ + "alias" + ] + ] + }, + { + "op": "structure", + "path": [] + } ], "expected": { "trace": [ - ["delivered", "leaf", 1], - ["delivered", "leaf", 2], - ["delivered", "leaf", 3], - ["delivered", "leaf", 4], - ["same", true], - ["structure", { "$render": "root" }] + [ + "delivered", + "leaf", + 1 + ], + [ + "delivered", + "leaf", + 2 + ], + [ + "delivered", + "leaf", + 3 + ], + [ + "delivered", + "leaf", + 4 + ], + [ + "same", + true + ], + [ + "structure", + { + "$render": "root" + } + ] ], "partsExact": true, "unchanged": true @@ -401,22 +1593,96 @@ "family": "structure", "root": "root", "steps": [ - { "op": "send", "path": ["a", "b"] }, - { "op": "substitute", "path": ["a"], "mode": "parts" }, - { "op": "send", "path": ["a", "b"] }, - { "op": "send", "path": ["alias"] }, - { "op": "send", "path": ["a"] }, - { "op": "same", "paths": [["a", "b"], ["alias"]] }, - { "op": "structure", "path": ["a"] } + { + "op": "send", + "path": ["a", "b"] + }, + { + "op": "substitute", + "path": [ + "a" + ], + "mode": "parts" + }, + { + "op": "send", + "path": ["a", "b"] + }, + { + "op": "send", + "path": [ + "alias" + ] + }, + { + "op": "send", + "path": [ + "a" + ] + }, + { + "op": "same", + "paths": [ + ["a", "b"], + [ + "alias" + ] + ] + }, + { + "op": "structure", + "path": [ + "a" + ] + } ], "expected": { "trace": [ - ["delivered", "leaf", 1], - ["delivered", "leaf", 2], - ["delivered", "leaf", 3], - ["delivered", "branch", 1], - ["same", true], - ["structure", { "own": ["branch", 1], "children": [["b", { "own": ["leaf", 1], "children": [] }]] }] + [ + "delivered", + "leaf", + 1 + ], + [ + "delivered", + "leaf", + 2 + ], + [ + "delivered", + "leaf", + 3 + ], + [ + "delivered", + "branch", + 1 + ], + [ + "same", + true + ], + [ + "structure", + { + "own": [ + "branch", + 1 + ], + "children": [ + [ + "b", + { + "own": [ + "leaf", + 1 + ], + "children": [] + } + ] + ] + } + ] ], "partsExact": true, "unchanged": true @@ -427,24 +1693,109 @@ "family": "structure", "root": "root", "steps": [ - { "op": "send", "path": ["alias"] }, - { "op": "send", "path": ["a"] }, - { "op": "substitute", "path": ["a"], "mode": "copy" }, - { "op": "send", "path": ["a", "b"] }, - { "op": "send", "path": ["a"] }, - { "op": "send", "path": ["alias"] }, - { "op": "same", "paths": [["a", "b"], ["alias"]] }, - { "op": "structure", "path": ["a"] } + { + "op": "send", + "path": [ + "alias" + ] + }, + { + "op": "send", + "path": [ + "a" + ] + }, + { + "op": "substitute", + "path": [ + "a" + ], + "mode": "copy" + }, + { + "op": "send", + "path": ["a", "b"] + }, + { + "op": "send", + "path": [ + "a" + ] + }, + { + "op": "send", + "path": [ + "alias" + ] + }, + { + "op": "same", + "paths": [ + ["a", "b"], + [ + "alias" + ] + ] + }, + { + "op": "structure", + "path": [ + "a" + ] + } ], "expected": { "trace": [ - ["delivered", "leaf", 1], - ["delivered", "branch", 1], - ["delivered", "leaf", 1], - ["delivered", "branch", 1], - ["delivered", "leaf", 2], - ["same", false], - ["structure", { "own": ["branch", 2], "children": [["b", { "own": ["leaf", 2], "children": [] }]] }] + [ + "delivered", + "leaf", + 1 + ], + [ + "delivered", + "branch", + 1 + ], + [ + "delivered", + "leaf", + 1 + ], + [ + "delivered", + "branch", + 1 + ], + [ + "delivered", + "leaf", + 2 + ], + [ + "same", + false + ], + [ + "structure", + { + "own": [ + "branch", + 2 + ], + "children": [ + [ + "b", + { + "own": [ + "leaf", + 2 + ], + "children": [] + } + ] + ] + } + ] ], "partsExact": true, "unchanged": true @@ -455,37 +1806,208 @@ "family": "structure", "root": "root", "steps": [ - { "op": "send", "path": ["a", "b"] }, - { "op": "omit", "path": [], "key": "a" }, - { "op": "send", "path": ["a", "b"] }, - { "op": "send", "path": ["alias"] }, - { "op": "rename", "path": [], "key": "alias", "to": "renamed" }, - { "op": "send", "path": ["alias"] }, - { "op": "send", "path": ["renamed"] }, - { "op": "add", "path": [], "key": "extra", "node": "extra" }, - { "op": "send", "path": ["extra"] }, - { "op": "send", "path": [] }, - { "op": "structure", "path": [] } + { + "op": "send", + "path": ["a", "b"] + }, + { + "op": "omit", + "path": [], + "key": "a" + }, + { + "op": "send", + "path": ["a", "b"] + }, + { + "op": "send", + "path": [ + "alias" + ] + }, + { + "op": "rename", + "path": [], + "key": "alias", + "to": "renamed" + }, + { + "op": "send", + "path": [ + "alias" + ] + }, + { + "op": "send", + "path": [ + "renamed" + ] + }, + { + "op": "add", + "path": [], + "key": "extra", + "node": "extra" + }, + { + "op": "send", + "path": [ + "extra" + ] + }, + { + "op": "send", + "path": [] + }, + { + "op": "structure", + "path": [] + } ], "expected": { "trace": [ - ["delivered", "leaf", 1], - ["missing"], - ["delivered", "leaf", 2], - ["missing"], - ["delivered", "leaf", 3], - ["delivered", "extra", 1], - ["delivered", "root", 1], - ["structure", { "own": ["root", 1], "children": [ - ["", { "own": ["blank", 1], "children": [["", { "own": ["blank-leaf", 1], "children": [] }]] }], - ["a/b", { "own": ["slash", 1], "children": [] }], - ["empty", { "own": null, "children": [] }], - ["extra", { "own": ["extra", 1], "children": [] }], - ["renamed", { "own": ["leaf", 1], "children": [] }], - ["é", { "own": ["acute", 1], "children": [] }], - ["é", { "own": ["combining", 1], "children": [] }], - [{ "hex": "ff" }, { "own": ["binary", 1], "children": [] }] - ] }] + [ + "delivered", + "leaf", + 1 + ], + [ + "missing" + ], + [ + "delivered", + "leaf", + 2 + ], + [ + "missing" + ], + [ + "delivered", + "leaf", + 3 + ], + [ + "delivered", + "extra", + 1 + ], + [ + "delivered", + "root", + 1 + ], + [ + "structure", + { + "own": [ + "root", + 1 + ], + "children": [ + [ + "", + { + "own": [ + "blank", + 1 + ], + "children": [ + [ + "", + { + "own": [ + "blank-leaf", + 1 + ], + "children": [] + } + ] + ] + } + ], + [ + "a/b", + { + "own": [ + "slash", + 1 + ], + "children": [] + } + ], + [ + "empty", + { + "own": null, + "children": [] + } + ], + [ + "extra", + { + "own": [ + "extra", + 1 + ], + "children": [] + } + ], + [ + "renamed", + { + "own": [ + "leaf", + 1 + ], + "children": [] + } + ], + [ + "é", + { + "own": [ + "acute", + 1 + ], + "children": [] + } + ], + [ + "é", + { + "own": [ + "combining", + 1 + ], + "children": [] + } + ], + [ + { + "hex": "ff" + }, + { + "own": [ + "binary", + 1 + ], + "children": [] + } + ], + [ + "�", + { + "own": [ + "fffd", + 1 + ], + "children": [] + } + ] + ] + } + ] ], "partsExact": true, "unchanged": true @@ -496,19 +2018,82 @@ "family": "structure", "root": "root", "steps": [ - { "op": "view", "selections": [["a"], ["b"]] }, - { "op": "replace", "path": ["a", "b"], "node": "replacement" }, - { "op": "send", "path": [], "via": "view" }, - { "op": "send", "path": ["a", "b"] }, - { "op": "send", "path": ["alias"] }, - { "op": "structure", "path": ["a"] } + { + "op": "view", + "selections": [ + [ + "a" + ], + [ + "b" + ] + ] + }, + { + "op": "replace", + "path": ["a", "b"], + "node": "replacement" + }, + { + "op": "send", + "path": [], + "via": "view" + }, + { + "op": "send", + "path": ["a", "b"] + }, + { + "op": "send", + "path": [ + "alias" + ] + }, + { + "op": "structure", + "path": [ + "a" + ] + } ], "expected": { "trace": [ - ["delivered", "leaf", 1], - ["delivered", "replacement", 1], - ["delivered", "leaf", 2], - ["structure", { "own": ["branch", 1], "children": [["b", { "own": ["replacement", 1], "children": [] }]] }] + [ + "delivered", + "leaf", + 1 + ], + [ + "delivered", + "replacement", + 1 + ], + [ + "delivered", + "leaf", + 2 + ], + [ + "structure", + { + "own": [ + "branch", + 1 + ], + "children": [ + [ + "b", + { + "own": [ + "replacement", + 1 + ], + "children": [] + } + ] + ] + } + ] ], "partsExact": true, "unchanged": true @@ -519,16 +2104,34 @@ "family": "structure", "root": "root", "fault": "duplicate", - "steps": [{ "op": "send", "path": ["a"] }], - "expected": { "construction": "refused" } + "steps": [ + { + "op": "send", + "path": [ + "a" + ] + } + ], + "expected": { + "construction": "refused" + } }, { "id": "missing-child-refused", "family": "structure", "root": "root", "fault": "missingChild", - "steps": [{ "op": "send", "path": ["hole"] }], - "expected": { "construction": "refused" } + "steps": [ + { + "op": "send", + "path": [ + "hole" + ] + } + ], + "expected": { + "construction": "refused" + } }, { "id": "cycle-refused", @@ -536,42 +2139,263 @@ "root": "root", "fault": "cycle", "steps": [], - "expected": { "construction": "refused" } + "expected": { + "construction": "refused" + } + }, + { + "id": "foreign-child-accepted", + "family": "structure", + "root": "root", + "foreign": true, + "steps": [ + { + "op": "send", + "path": [ + "foreign" + ] + }, + { + "op": "send", + "path": ["foreign", "inner"] + }, + { + "op": "structure", + "path": [ + "foreign" + ] + }, + { + "op": "rebuild", + "keep": [] + }, + { + "op": "send", + "path": [ + "foreign" + ] + }, + { + "op": "send", + "path": ["foreign", "missing"] + }, + { + "op": "structure", + "path": [ + "foreign" + ] + } + ], + "expected": { + "trace": [ + [ + "delivered", + "foreign", + 1 + ], + [ + "delivered", + "inner", + 1 + ], + [ + "structure", + { + "own": [ + "foreign", + 1 + ], + "children": [ + [ + "inner", + { + "own": [ + "inner", + 1 + ], + "children": [] + } + ] + ] + } + ], + [ + "delivered", + "foreign", + 2 + ], + [ + "missing" + ], + [ + "structure", + { + "own": [ + "foreign", + 1 + ], + "children": [ + [ + "inner", + { + "own": [ + "inner", + 1 + ], + "children": [] + } + ] + ] + } + ] + ], + "partsExact": true, + "unchanged": true + } }, { "id": "bridge-exact-utf8-image", "family": "bridge", "root": "root", "steps": [ - { "op": "bridge", "path": [] }, - { "op": "bridge", "path": ["a", "b"] }, - { "op": "bridge", "path": [""] }, - { "op": "bridge", "path": ["", ""] }, - { "op": "bridge", "path": ["a/b"] }, - { "op": "bridge", "path": ["é"] }, - { "op": "bridge", "path": ["é"] }, - { "op": "bridge", "path": ["ÿ"] }, - { "op": "bridge", "path": ["empty"] }, - { "op": "bridge", "path": ["missing"] }, - { "op": "bridge", "path": ["alias", "x"] }, - { "op": "bridgeInvalid" }, - { "op": "bridge", "path": [] } + { + "op": "bridge", + "path": [] + }, + { + "op": "bridge", + "path": ["a", "b"] + }, + { + "op": "bridge", + "path": [ + "" + ] + }, + { + "op": "bridge", + "path": ["", ""] + }, + { + "op": "bridge", + "path": [ + "a/b" + ] + }, + { + "op": "bridge", + "path": [ + "é" + ] + }, + { + "op": "bridge", + "path": [ + "é" + ] + }, + { + "op": "bridge", + "path": [ + "ÿ" + ] + }, + { + "op": "bridge", + "path": [ + "empty" + ] + }, + { + "op": "bridge", + "path": [ + "missing" + ] + }, + { + "op": "bridge", + "path": ["alias", "x"] + }, + { + "op": "bridge", + "path": [ + "�" + ] + }, + { + "op": "bridgeInvalid", + "utf16": [ + "d800" + ], + "utf8": "ff" + }, + { + "op": "bridge", + "path": [] + } ], "expected": { "trace": [ - ["delivered", "root", 1], - ["delivered", "leaf", 1], - ["delivered", "blank", 1], - ["delivered", "blank-leaf", 1], - ["delivered", "slash", 1], - ["delivered", "acute", 1], - ["delivered", "combining", 1], - ["refused"], - ["refused"], - ["refused"], - ["refused"], - ["refused"], - ["delivered", "root", 2] + [ + "delivered", + "root", + 1 + ], + [ + "delivered", + "leaf", + 1 + ], + [ + "delivered", + "blank", + 1 + ], + [ + "delivered", + "blank-leaf", + 1 + ], + [ + "delivered", + "slash", + 1 + ], + [ + "delivered", + "acute", + 1 + ], + [ + "delivered", + "combining", + 1 + ], + [ + "unreached" + ], + [ + "refused" + ], + [ + "unreached" + ], + [ + "unreached" + ], + [ + "delivered", + "fffd", + 1 + ], + [ + "unreached" + ], + [ + "delivered", + "root", + 2 + ] ], "sendOnly": true, "unchanged": true @@ -582,47 +2406,186 @@ "family": "carrier", "root": "root", "steps": [ - { "op": "send", "path": [] }, - { "op": "send", "path": ["a"] }, - { "op": "send", "path": ["a", "b"] }, - { "op": "send", "path": ["alias"] }, - { "op": "send", "path": [""] }, - { "op": "send", "path": ["", ""] }, - { "op": "send", "path": ["a/b"] }, - { "op": "send", "path": ["é"] }, - { "op": "send", "path": ["é"] }, - { "op": "send", "path": ["empty"] }, - { "op": "send", "path": [{ "hex": "ff" }] }, - { "op": "send", "path": ["alias", "x"] }, - { "op": "sendAddressed", "path": ["t", "missing"] }, - { "op": "sendAddressed", "path": ["t", "alias", "x"] }, - { "op": "sendAddressed", "path": ["t", "ÿ"] }, - { "op": "sendAddressed", "path": ["missing"] }, - { "op": "sendAddressed", "path": [] }, - { "op": "sendAddressed", "path": ["t", "a", "b"] }, - { "op": "structure", "path": [] } + { + "op": "send", + "path": [] + }, + { + "op": "send", + "path": [ + "a" + ] + }, + { + "op": "send", + "path": ["a", "b"] + }, + { + "op": "send", + "path": [ + "alias" + ] + }, + { + "op": "send", + "path": [ + "" + ] + }, + { + "op": "send", + "path": ["", ""] + }, + { + "op": "send", + "path": [ + "a/b" + ] + }, + { + "op": "send", + "path": [ + "é" + ] + }, + { + "op": "send", + "path": [ + "é" + ] + }, + { + "op": "send", + "path": [ + "empty" + ] + }, + { + "op": "send", + "path": [ + { + "hex": "ff" + } + ] + }, + { + "op": "send", + "path": ["alias", "x"] + }, + { + "op": "sendAddressed", + "path": ["t", "missing"] + }, + { + "op": "sendAddressed", + "path": [ + "t", + "alias", + "x" + ] + }, + { + "op": "sendAddressed", + "path": ["t", "ÿ"] + }, + { + "op": "sendAddressed", + "path": [ + "missing" + ] + }, + { + "op": "sendAddressed", + "path": [] + }, + { + "op": "sendAddressed", + "path": [ + "t", + "a", + "b" + ] + }, + { + "op": "structure", + "path": [] + } ], "expected": { "trace": [ - ["delivered", "root", 1], - ["delivered", "branch", 1], - ["delivered", "leaf", 1], - ["delivered", "leaf", 2], - ["delivered", "blank", 1], - ["delivered", "blank-leaf", 1], - ["delivered", "slash", 1], - ["delivered", "acute", 1], - ["delivered", "combining", 1], + [ + "delivered", + "root", + 1 + ], + [ + "delivered", + "branch", + 1 + ], + [ + "delivered", + "leaf", + 1 + ], + [ + "delivered", + "leaf", + 2 + ], + [ + "delivered", + "blank", + 1 + ], + [ + "delivered", + "blank-leaf", + 1 + ], + [ + "delivered", + "slash", + 1 + ], + [ + "delivered", + "acute", + 1 + ], + [ + "delivered", + "combining", + 1 + ], + [ + "refused" + ], ["error", "internal"], - ["missing"], - ["missing"], + [ + "missing" + ], + [ + "missing" + ], ["error", "method_not_found"], ["error", "method_not_found"], ["error", "method_not_found"], ["error", "method_not_found"], - ["refused"], - ["delivered", "leaf", 3], - ["structure", { "$render": "root" }] + [ + "refused" + ], + [ + "delivered", + "leaf", + 3 + ], + [ + "structure", + { + "$render": "root" + } + ] ], "unchanged": true, "borrowedUsable": true @@ -633,25 +2596,102 @@ "family": "carrier", "root": "root", "steps": [ - { "op": "send", "path": ["a", "b"] }, - { "op": "rebuild", "side": "near", "keep": [] }, - { "op": "send", "path": [], "selections": [["a"], ["b"]] }, - { "op": "rebuild", "side": "far", "keep": [] }, - { "op": "send", "path": [] }, - { "op": "send", "path": ["alias"] }, - { "op": "rebuild", "side": "near", "keep": [["a"]] }, - { "op": "rebuild", "side": "far", "keep": [["a"]] }, - { "op": "send", "path": ["", ""] }, - { "op": "structure", "path": [] } + { + "op": "send", + "path": ["a", "b"] + }, + { + "op": "rebuild", + "side": "near", + "keep": [] + }, + { + "op": "send", + "path": [], + "selections": [ + [ + "a" + ], + [ + "b" + ] + ] + }, + { + "op": "rebuild", + "side": "far", + "keep": [] + }, + { + "op": "send", + "path": [] + }, + { + "op": "send", + "path": [ + "alias" + ] + }, + { + "op": "rebuild", + "side": "near", + "keep": [ + [ + "a" + ] + ] + }, + { + "op": "rebuild", + "side": "far", + "keep": [ + [ + "a" + ] + ] + }, + { + "op": "send", + "path": ["", ""] + }, + { + "op": "structure", + "path": [] + } ], "expected": { "trace": [ - ["delivered", "leaf", 1], - ["delivered", "leaf", 2], - ["delivered", "root", 1], - ["delivered", "leaf", 3], - ["delivered", "blank-leaf", 1], - ["structure", { "$render": "root" }] + [ + "delivered", + "leaf", + 1 + ], + [ + "delivered", + "leaf", + 2 + ], + [ + "delivered", + "root", + 1 + ], + [ + "delivered", + "leaf", + 3 + ], + [ + "delivered", + "blank-leaf", + 1 + ], + [ + "structure", + { + "$render": "root" + } + ] ], "unchanged": true, "borrowedUsable": true @@ -663,19 +2703,76 @@ "root": "root", "relay": true, "steps": [ - { "op": "send", "path": ["a", "b"] }, - { "op": "rebuild", "side": "near", "keep": [] }, - { "op": "rebuild", "side": "far", "keep": [] }, - { "op": "send", "path": [], "selections": [["a"], ["b"]] }, - { "op": "send", "path": [] }, - { "op": "send", "path": ["empty"] }, - { "op": "sendAddressed", "path": ["t", "missing"] } + { + "op": "send", + "path": ["a", "b"] + }, + { + "op": "rebuild", + "side": "near", + "keep": [] + }, + { + "op": "rebuild", + "side": "far", + "keep": [] + }, + { + "op": "send", + "path": [], + "selections": [ + [ + "a" + ], + [ + "b" + ] + ] + }, + { + "op": "send", + "path": [] + }, + { + "op": "send", + "path": ["", ""] + }, + { + "op": "send", + "path": [ + "empty" + ] + }, + { + "op": "sendAddressed", + "path": ["t", "missing"] + } ], "expected": { "trace": [ - ["delivered", "leaf", 1], - ["delivered", "leaf", 2], - ["delivered", "root", 1], + [ + "delivered", + "leaf", + 1 + ], + [ + "delivered", + "leaf", + 2 + ], + [ + "delivered", + "root", + 1 + ], + [ + "delivered", + "blank-leaf", + 1 + ], + [ + "refused" + ], ["error", "internal"], ["error", "method_not_found"] ], @@ -686,22 +2783,66 @@ { "id": "carrier-mount-reconstruction", "family": "carrier", - "root": "rroot", + "root": "root", "mount": true, "steps": [ - { "op": "send", "path": ["a", "b"] }, - { "op": "rebuild", "side": "near", "keep": [] }, - { "op": "rebuild", "side": "far", "keep": [] }, - { "op": "send", "path": [], "selections": [["a"], ["b"]] }, - { "op": "send", "path": ["", ""] }, - { "op": "send", "path": [] } + { + "op": "send", + "path": ["a", "b"] + }, + { + "op": "rebuild", + "side": "near", + "keep": [] + }, + { + "op": "rebuild", + "side": "far", + "keep": [] + }, + { + "op": "send", + "path": [], + "selections": [ + [ + "a" + ], + [ + "b" + ] + ] + }, + { + "op": "send", + "path": ["", ""] + }, + { + "op": "send", + "path": [] + } ], "expected": { "trace": [ - ["delivered", "leaf", 1], - ["delivered", "leaf", 2], - ["delivered", "blank-leaf", 1], - ["error", "internal"] + [ + "delivered", + "leaf", + 1 + ], + [ + "delivered", + "leaf", + 2 + ], + [ + "delivered", + "blank-leaf", + 1 + ], + [ + "delivered", + "root", + 1 + ] ], "unchanged": true, "borrowedUsable": true @@ -712,25 +2853,94 @@ "family": "carrier", "root": "root", "steps": [ - { "op": "hold", "path": ["a", "b"] }, - { "op": "rebuild", "side": "near", "keep": [] }, - { "op": "rebuild", "side": "far", "keep": [] }, - { "op": "replace", "side": "far", "path": ["a", "b"], "node": "replacement" }, - { "op": "replace", "side": "far", "path": ["alias"], "node": "replacement" }, - { "op": "replace", "side": "near", "path": ["a", "b"], "node": "replacement" }, - { "op": "replace", "side": "near", "path": ["alias"], "node": "replacement" }, - { "op": "send", "path": ["alias"] }, - { "op": "send", "path": ["a", "b"] }, - { "op": "release" }, - { "op": "send", "path": ["a"] } + { + "op": "hold", + "path": ["a", "b"] + }, + { + "op": "rebuild", + "side": "near", + "keep": [] + }, + { + "op": "rebuild", + "side": "far", + "keep": [] + }, + { + "op": "replace", + "side": "far", + "path": ["a", "b"], + "node": "replacement" + }, + { + "op": "replace", + "side": "far", + "path": [ + "alias" + ], + "node": "replacement" + }, + { + "op": "replace", + "side": "near", + "path": ["a", "b"], + "node": "replacement" + }, + { + "op": "replace", + "side": "near", + "path": [ + "alias" + ], + "node": "replacement" + }, + { + "op": "send", + "path": [ + "alias" + ] + }, + { + "op": "send", + "path": ["a", "b"] + }, + { + "op": "send", + "path": [ + "a" + ] + }, + { + "op": "teardown" + }, + { + "op": "release" + } ], "expected": { "trace": [ - ["held", "leaf", 1], - ["delivered", "replacement", 1], - ["delivered", "replacement", 2], - ["late", "leaf"], - ["delivered", "branch", 1] + [ + "held", + "leaf", + 1 + ], + [ + "delivered", + "replacement", + 1 + ], + [ + "delivered", + "replacement", + 2 + ], + [ + "delivered", + "branch", + 1 + ], + ["late", "leaf"] ], "unchanged": true, "borrowedUsable": true @@ -741,20 +2951,60 @@ "family": "carrier", "root": "root", "steps": [ - { "op": "hold", "path": ["a", "b"] }, - { "op": "rebuild", "side": "far", "keep": [] }, - { "op": "replace", "side": "far", "path": ["a", "b"], "node": "replacement" }, - { "op": "replace", "side": "near", "path": ["a", "b"], "node": "replacement" }, - { "op": "cancel", "path": ["a", "b"] }, - { "op": "send", "path": ["a", "b"] }, - { "op": "send", "path": ["alias"] } + { + "op": "hold", + "path": ["a", "b"] + }, + { + "op": "rebuild", + "side": "far", + "keep": [] + }, + { + "op": "replace", + "side": "far", + "path": ["a", "b"], + "node": "replacement" + }, + { + "op": "replace", + "side": "near", + "path": ["a", "b"], + "node": "replacement" + }, + { + "op": "cancel", + "path": ["a", "b"] + }, + { + "op": "send", + "path": ["a", "b"] + }, + { + "op": "send", + "path": [ + "alias" + ] + } ], "expected": { "trace": [ - ["held", "leaf", 1], + [ + "held", + "leaf", + 1 + ], ["cancelled", "leaf"], - ["delivered", "replacement", 1], - ["delivered", "leaf", 2] + [ + "delivered", + "replacement", + 1 + ], + [ + "delivered", + "leaf", + 2 + ] ], "unchanged": true, "borrowedUsable": true @@ -765,21 +3015,61 @@ "family": "carrier", "root": "root", "steps": [ - { "op": "send", "path": ["a", "b"] }, - { "op": "teardown" }, - { "op": "direct", "path": ["a", "b"] }, - { "op": "direct", "path": ["", ""] }, - { "op": "direct", "path": ["missing"] }, - { "op": "direct", "path": ["empty"] }, - { "op": "send", "path": ["a", "b"] } + { + "op": "send", + "path": ["a", "b"] + }, + { + "op": "teardown" + }, + { + "op": "direct", + "path": ["a", "b"] + }, + { + "op": "direct", + "path": ["", ""] + }, + { + "op": "direct", + "path": [ + "missing" + ] + }, + { + "op": "direct", + "path": [ + "empty" + ] + }, + { + "op": "send", + "path": ["a", "b"] + } ], "expected": { "trace": [ - ["delivered", "leaf", 1], - ["delivered", "leaf", 2], - ["delivered", "blank-leaf", 1], - ["missing"], - ["refused"], + [ + "delivered", + "leaf", + 1 + ], + [ + "delivered", + "leaf", + 2 + ], + [ + "delivered", + "blank-leaf", + 1 + ], + [ + "missing" + ], + [ + "refused" + ], ["error", "method_not_found"] ], "unchanged": true, diff --git a/conformance/wiretree/disposition.json b/conformance/wiretree/disposition.json index 4a1e888..7301dfa 100644 --- a/conformance/wiretree/disposition.json +++ b/conformance/wiretree/disposition.json @@ -20,145 +20,570 @@ "addressed-mount": "Mount/mount is bitruntime's addressed routing operator: child-only, typed by a native map of Endpoints and validated on send. Its recorded gaps remain accurate observations of that operator. Tree construction is Compose/compose." }, "cases": [ - { "id": "origin-and-descendants", "gap": "origin-bearing-construction", "requirements": [ - { "kind": "structural", "cases": ["own-and-descendants"], "note": "Own value at [] beside complete children, empty keys, the literal slash key and both Unicode spellings. Adds the binary key and a present refusing node." }, - { "kind": "carrier", "cases": ["carrier-routing"] }, - { "kind": "historical-addressed", "limitation": "suffix-delivery", "note": "[alias, x] and [a, b, x, y] were delivered to leaf with a suffix; in the full tree both are missing." } - ] }, - { "id": "nested-selection-agrees", "gap": "origin-bearing-construction", "requirements": [ - { "kind": "structural", "cases": ["nested-selection-agrees"], "note": "Nested and concatenated selection reach the same node; selection returns that node itself." }, - { "kind": "historical-addressed", "limitation": "suffix-delivery", "note": "Selections through [alias] then [x] were delivered to leaf with [x]; in the full tree they are missing." } - ] }, - { "id": "missing-never-falls-back", "gap": "origin-bearing-construction", "requirements": [ - { "kind": "structural", "cases": ["missing-never-falls-back"], "note": "Meaning sharpened: missing paths are missing, not refused, and [empty] is a present node whose own refuses. No own value is invoked as fallback." } - ] }, - { "id": "empty-branch-versus-missing", "gap": "origin-bearing-construction", "requirements": [ - { "kind": "structural", "cases": ["refusing-versus-missing"], "note": "Meaning sharpened: the two sends were indistinguishable refusals; selection now distinguishes a present refusing node from a missing one." } - ] }, - { "id": "root-cut-reconstruction", "gap": "origin-bearing-construction", "requirements": [ - { "kind": "structural", "cases": ["root-cut-reconstruction"] } - ] }, - { "id": "complete-cuts-agree", "gap": "origin-bearing-construction", "requirements": [ - { "kind": "structural", "cases": ["complete-cuts-agree"] }, - { "kind": "carrier", "cases": ["carrier-reconstruction"] } - ] }, - { "id": "children-alone-lose-origin", "gap": "origin-bearing-construction", "requirements": [ - { "kind": "structural", "cases": ["children-alone-lose-own"] } - ] }, - { "id": "fresh-origin-resets-parent-state", "gap": "origin-bearing-construction", "requirements": [ - { "kind": "structural", "cases": ["fresh-own-resets-state"] } - ] }, - { "id": "equivalent-substitution", "gap": "origin-bearing-construction", "requirements": [ - { "kind": "structural", "cases": ["equivalent-substitution"] } - ] }, - { "id": "copied-subtree-breaks-sharing", "gap": "origin-bearing-construction", "requirements": [ - { "kind": "structural", "cases": ["copied-subtree-breaks-sharing"] } - ] }, - { "id": "altered-children-detected", "gap": "origin-bearing-construction", "requirements": [ - { "kind": "structural", "cases": ["altered-children-detected"], "note": "Meaning sharpened: an omitted or renamed child is missing rather than refused." } - ] }, - { "id": "invalid-path-never-reaches-origin", "gap": "origin-bearing-construction", "requirements": [ - { "kind": "bridge", "cases": ["bridge-exact-utf8-image"], "note": "Tree paths are bytes and have no invalid form; an ill-formed addressed segment is refused by the bridge before any own Wire." } - ] }, - { "id": "selection-retains-original-binding", "gap": "origin-bearing-construction", "requirements": [ - { "kind": "structural", "cases": ["selected-subtree-keeps-identity"] } - ] }, - { "id": "forwarded-reconstruction", "gap": "origin-bearing-construction", "requirements": [ - { "kind": "carrier", "cases": ["carrier-relay-reconstruction"] } - ] }, - { "id": "mounted-carrier-reconstruction", "gap": "origin-bearing-construction", "requirements": [ - { "kind": "carrier", "cases": ["carrier-mount-reconstruction"] } - ] }, - { "id": "pending-across-reconstruction", "gap": "origin-bearing-construction", "requirements": [ - { "kind": "carrier", "cases": ["carrier-late-reply-across-replacement", "carrier-cancel-across-replacement"], "note": "The captured cancellation is now the requester's cancel frame through tree access, routed by the far dispatcher's captured traversal; the late reply returns to the original return capability." } - ] }, - { "id": "mount-routing", "requirements": [ - { "kind": "structural", "cases": ["refusing-own-and-descendants"], "note": "Meaning sharpened: [missing] is missing, while [], [a], [\"\"] and [empty] are present nodes whose own refuses." }, - { "kind": "historical-addressed", "limitation": "suffix-delivery" } - ] }, - { "id": "mount-nested-selection", "requirements": [ - { "kind": "structural", "cases": ["nested-selection-agrees", "refusing-own-and-descendants"] }, - { "kind": "historical-addressed", "limitation": "suffix-delivery", "note": "[alias] then [x, y] reached leaf with a suffix." } - ] }, - { "id": "mount-complete-cuts-agree", "requirements": [ - { "kind": "structural", "cases": ["refusing-complete-cuts-agree"] } - ] }, - { "id": "mount-empty-branch-versus-missing", "requirements": [ - { "kind": "structural", "cases": ["refusing-versus-missing"] } - ] }, - { "id": "mount-altered-children-detected", "requirements": [ - { "kind": "structural", "cases": ["altered-children-detected"] } - ] }, - { "id": "conflicting-children", "gap": "conflicting-segments-accepted", "requirements": [ - { "kind": "structural", "cases": ["duplicate-keys-refused"] } - ] }, - { "id": "invalid-key", "gap": "invalid-segments-accepted", "requirements": [ - { "kind": "structural", "cases": ["own-and-descendants"], "note": "Meaning changed: a key outside the UTF-8 image is a valid tree key, reachable structurally." }, - { "kind": "bridge", "cases": ["bridge-exact-utf8-image"], "note": "It is unreachable through the bitwire/1 bridge, which never decodes or normalizes." }, - { "kind": "carrier", "cases": ["carrier-routing"] } - ] }, - { "id": "missing-child-value", "gap": "missing-children-accepted", "requirements": [ - { "kind": "structural", "cases": ["missing-child-refused"] } - ] }, - { "id": "cyclic-declaration", "requirements": [ - { "kind": "structural", "cases": ["cycle-refused"], "note": "The historical case refused a cyclic description in the test harness. Construction itself now refuses a child graph that contains a cycle." } - ] }, - { "id": "mount-invalid-path", "requirements": [ - { "kind": "bridge", "cases": ["bridge-exact-utf8-image"] } - ] }, - { "id": "mount-shared-child-state", "requirements": [ - { "kind": "structural", "cases": ["shared-child-state"] } - ] }, - { "id": "mount-equivalent-substitution", "requirements": [ - { "kind": "structural", "cases": ["equivalent-substitution"] } - ] }, - { "id": "mount-copied-subtree-breaks-sharing", "requirements": [ - { "kind": "structural", "cases": ["copied-subtree-breaks-sharing"] } - ] }, - { "id": "mount-forwarded-reconstruction", "requirements": [ - { "kind": "carrier", "cases": ["carrier-relay-reconstruction"] } - ] }, - { "id": "mount-mounted-carrier", "requirements": [ - { "kind": "carrier", "cases": ["carrier-mount-reconstruction"] } - ] }, - { "id": "mount-selection-retains-original-binding", "requirements": [ - { "kind": "structural", "cases": ["selected-subtree-keeps-identity"] } - ] }, - { "id": "mount-pending-across-reconstruction", "requirements": [ - { "kind": "carrier", "cases": ["carrier-late-reply-across-replacement", "carrier-cancel-across-replacement"] } - ] }, - { "id": "mount-teardown-keeps-borrowed", "requirements": [ - { "kind": "carrier", "cases": ["carrier-teardown-keeps-borrowed"], "note": "Closing the serving dispatcher releases its routes, never the borrowed endpoint or the served tree." }, - { "kind": "historical-addressed", "limitation": "suffix-delivery", "note": "The direct send to leaf with [x]." } - ] }, - { "id": "guard-around-composite", "requirements": [ - { "kind": "historical-addressed", "limitation": "path-observing-interception" } - ] }, - { "id": "guard-state-survives-reconstruction", "requirements": [ - { "kind": "structural", "cases": ["root-cut-reconstruction", "shared-child-state"], "note": "Retained primitive state survives reconstruction because reconstruction keeps the capability." }, - { "kind": "historical-addressed", "limitation": "path-observing-interception" } - ] }, - { "id": "fresh-guard-resets-state", "requirements": [ - { "kind": "structural", "cases": ["fresh-own-resets-state"] }, - { "kind": "historical-addressed", "limitation": "path-observing-interception" } - ] }, - { "id": "guarded-child-complete-access", "requirements": [ - { "kind": "historical-addressed", "limitation": "path-observing-interception" }, - { "kind": "historical-addressed", "limitation": "suffix-delivery" } - ] }, - { "id": "rebuilding-from-guarded-views-repeats-checks", "requirements": [ - { "kind": "structural", "cases": ["equivalent-substitution", "nested-selection-agrees"], "note": "Rebuilding from selected children retains their identity." }, - { "kind": "historical-addressed", "limitation": "addressed-views" } - ] } + { + "id": "origin-and-descendants", + "gap": "origin-bearing-construction", + "requirements": [ + { + "kind": "structural", + "cases": [ + "own-and-descendants" + ], + "note": "Own value at [] beside complete children, empty keys, the literal slash key and both Unicode spellings. Adds the binary key and a present refusing node." + }, + { + "kind": "carrier", + "cases": [ + "carrier-routing" + ] + }, + { + "kind": "historical-addressed", + "limitation": "suffix-delivery", + "note": "[alias, x] and [a, b, x, y] were delivered to leaf with a suffix; in the full tree both are missing." + } + ] + }, + { + "id": "nested-selection-agrees", + "gap": "origin-bearing-construction", + "requirements": [ + { + "kind": "structural", + "cases": [ + "nested-selection-agrees" + ], + "note": "Nested and concatenated selection reach the same node; selection returns that node itself." + }, + { + "kind": "historical-addressed", + "limitation": "suffix-delivery", + "note": "Selections through [alias] then [x] were delivered to leaf with [x]; in the full tree they are missing." + } + ] + }, + { + "id": "missing-never-falls-back", + "gap": "origin-bearing-construction", + "requirements": [ + { + "kind": "structural", + "cases": [ + "missing-never-falls-back" + ], + "note": "Meaning sharpened: missing paths are missing, not refused, and [empty] is a present node whose own refuses. No own value is invoked as fallback." + } + ] + }, + { + "id": "empty-branch-versus-missing", + "gap": "origin-bearing-construction", + "requirements": [ + { + "kind": "structural", + "cases": [ + "refusing-versus-missing" + ], + "note": "Meaning sharpened: the two sends were indistinguishable refusals; selection now distinguishes a present refusing node from a missing one." + } + ] + }, + { + "id": "root-cut-reconstruction", + "gap": "origin-bearing-construction", + "requirements": [ + { + "kind": "structural", + "cases": [ + "root-cut-reconstruction" + ] + } + ] + }, + { + "id": "complete-cuts-agree", + "gap": "origin-bearing-construction", + "requirements": [ + { + "kind": "structural", + "cases": [ + "complete-cuts-agree" + ] + }, + { + "kind": "carrier", + "cases": [ + "carrier-reconstruction" + ] + } + ] + }, + { + "id": "children-alone-lose-origin", + "gap": "origin-bearing-construction", + "requirements": [ + { + "kind": "structural", + "cases": [ + "children-alone-lose-own" + ] + } + ] + }, + { + "id": "fresh-origin-resets-parent-state", + "gap": "origin-bearing-construction", + "requirements": [ + { + "kind": "structural", + "cases": [ + "fresh-own-resets-state" + ] + } + ] + }, + { + "id": "equivalent-substitution", + "gap": "origin-bearing-construction", + "requirements": [ + { + "kind": "structural", + "cases": [ + "equivalent-substitution" + ] + } + ] + }, + { + "id": "copied-subtree-breaks-sharing", + "gap": "origin-bearing-construction", + "requirements": [ + { + "kind": "structural", + "cases": [ + "copied-subtree-breaks-sharing" + ] + } + ] + }, + { + "id": "altered-children-detected", + "gap": "origin-bearing-construction", + "requirements": [ + { + "kind": "structural", + "cases": [ + "altered-children-detected" + ], + "note": "Meaning sharpened: an omitted or renamed child is missing rather than refused." + } + ] + }, + { + "id": "invalid-path-never-reaches-origin", + "gap": "origin-bearing-construction", + "requirements": [ + { + "kind": "bridge", + "cases": [ + "bridge-exact-utf8-image" + ], + "note": "Tree paths are bytes and have no invalid form; an ill-formed addressed segment is refused by the bridge before any own Wire." + } + ] + }, + { + "id": "selection-retains-original-binding", + "gap": "origin-bearing-construction", + "requirements": [ + { + "kind": "structural", + "cases": [ + "selected-subtree-keeps-identity" + ] + } + ] + }, + { + "id": "forwarded-reconstruction", + "gap": "origin-bearing-construction", + "requirements": [ + { + "kind": "carrier", + "cases": [ + "carrier-relay-reconstruction" + ] + } + ] + }, + { + "id": "mounted-carrier-reconstruction", + "gap": "origin-bearing-construction", + "requirements": [ + { + "kind": "carrier", + "cases": [ + "carrier-mount-reconstruction" + ], + "note": "Through an addressed Mount of the carrier, reconstruction on either side keeps reaching the own value at [] and the descendants." + } + ] + }, + { + "id": "pending-across-reconstruction", + "gap": "origin-bearing-construction", + "requirements": [ + { + "kind": "carrier", + "cases": ["carrier-late-reply-across-replacement", "carrier-cancel-across-replacement"], + "note": "The captured cancellation is now the requester's cancel frame through tree access, routed by the far dispatcher's captured traversal. The late reply returns to the original return capability after the serving composition is torn down." + } + ] + }, + { + "id": "mount-routing", + "requirements": [ + { + "kind": "structural", + "cases": [ + "refusing-own-and-descendants" + ], + "note": "Meaning sharpened: [missing] is missing, while [], [a], [\"\"] and [empty] are present nodes whose own refuses." + }, + { + "kind": "historical-addressed", + "limitation": "suffix-delivery" + } + ] + }, + { + "id": "mount-nested-selection", + "requirements": [ + { + "kind": "structural", + "cases": ["nested-selection-agrees", "refusing-own-and-descendants"], + "note": "Meaning sharpened: selecting [missing] is missing rather than refused; [] and [a] select present nodes whose own refuses." + }, + { + "kind": "historical-addressed", + "limitation": "suffix-delivery", + "note": "[alias] then [x, y] reached leaf with a suffix." + } + ] + }, + { + "id": "mount-complete-cuts-agree", + "requirements": [ + { + "kind": "structural", + "cases": [ + "refusing-complete-cuts-agree" + ] + } + ] + }, + { + "id": "mount-empty-branch-versus-missing", + "requirements": [ + { + "kind": "structural", + "cases": [ + "refusing-versus-missing" + ], + "note": "Meaning sharpened: sending at [missing] is missing rather than refused; [empty] stays a present node whose own refuses." + } + ] + }, + { + "id": "mount-altered-children-detected", + "requirements": [ + { + "kind": "structural", + "cases": [ + "altered-children-detected" + ], + "note": "Meaning sharpened: an omitted or renamed child is missing rather than refused." + } + ] + }, + { + "id": "conflicting-children", + "gap": "conflicting-segments-accepted", + "requirements": [ + { + "kind": "structural", + "cases": [ + "duplicate-keys-refused" + ] + } + ] + }, + { + "id": "invalid-key", + "gap": "invalid-segments-accepted", + "requirements": [ + { + "kind": "structural", + "cases": [ + "own-and-descendants" + ], + "note": "Meaning changed: a key outside the UTF-8 image is a valid tree key, reachable structurally." + }, + { + "kind": "bridge", + "cases": [ + "bridge-exact-utf8-image" + ], + "note": "It is unreachable through the bitwire/1 bridge, which never decodes or normalizes; an ill-formed segment, including one a lossy decoder would turn into U+FFFD or byte ff, is refused before any primitive." + } + ] + }, + { + "id": "missing-child-value", + "gap": "missing-children-accepted", + "requirements": [ + { + "kind": "structural", + "cases": [ + "missing-child-refused" + ] + } + ] + }, + { + "id": "cyclic-declaration", + "requirements": [ + { + "kind": "structural", + "cases": ["cycle-refused", "foreign-child-accepted"], + "note": "The historical case refused a cyclic description in the test harness. Construction itself now refuses a child graph that contains a cycle, and accepts an acyclic child implemented outside the runtime, so the refusal is not a refusal of every foreign node." + } + ] + }, + { + "id": "mount-invalid-path", + "requirements": [ + { + "kind": "bridge", + "cases": [ + "bridge-exact-utf8-image" + ] + } + ] + }, + { + "id": "mount-shared-child-state", + "requirements": [ + { + "kind": "structural", + "cases": [ + "shared-child-state" + ] + } + ] + }, + { + "id": "mount-equivalent-substitution", + "requirements": [ + { + "kind": "structural", + "cases": [ + "equivalent-substitution" + ] + } + ] + }, + { + "id": "mount-copied-subtree-breaks-sharing", + "requirements": [ + { + "kind": "structural", + "cases": [ + "copied-subtree-breaks-sharing" + ] + } + ] + }, + { + "id": "mount-forwarded-reconstruction", + "requirements": [ + { + "kind": "carrier", + "cases": [ + "carrier-relay-reconstruction" + ], + "note": "Includes the doubly empty path through the relay." + } + ] + }, + { + "id": "mount-mounted-carrier", + "requirements": [ + { + "kind": "carrier", + "cases": [ + "carrier-mount-reconstruction" + ] + } + ] + }, + { + "id": "mount-selection-retains-original-binding", + "requirements": [ + { + "kind": "structural", + "cases": [ + "selected-subtree-keeps-identity" + ] + } + ] + }, + { + "id": "mount-pending-across-reconstruction", + "requirements": [ + { + "kind": "carrier", + "cases": ["carrier-late-reply-across-replacement", "carrier-cancel-across-replacement"] + } + ] + }, + { + "id": "mount-teardown-keeps-borrowed", + "requirements": [ + { + "kind": "carrier", + "cases": [ + "carrier-teardown-keeps-borrowed" + ], + "note": "Closing the serving dispatcher releases its routes, never the borrowed endpoint or the served tree." + }, + { + "kind": "historical-addressed", + "limitation": "suffix-delivery", + "note": "The direct send to leaf with [x]." + } + ] + }, + { + "id": "guard-around-composite", + "requirements": [ + { + "kind": "historical-addressed", + "limitation": "path-observing-interception" + } + ] + }, + { + "id": "guard-state-survives-reconstruction", + "requirements": [ + { + "kind": "structural", + "cases": ["root-cut-reconstruction", "shared-child-state"], + "note": "Retained primitive state survives reconstruction because reconstruction keeps the capability." + }, + { + "kind": "historical-addressed", + "limitation": "path-observing-interception" + } + ] + }, + { + "id": "fresh-guard-resets-state", + "requirements": [ + { + "kind": "structural", + "cases": [ + "fresh-own-resets-state" + ] + }, + { + "kind": "historical-addressed", + "limitation": "path-observing-interception" + } + ] + }, + { + "id": "guarded-child-complete-access", + "requirements": [ + { + "kind": "historical-addressed", + "limitation": "path-observing-interception" + }, + { + "kind": "historical-addressed", + "limitation": "suffix-delivery" + } + ] + }, + { + "id": "rebuilding-from-guarded-views-repeats-checks", + "requirements": [ + { + "kind": "structural", + "cases": ["equivalent-substitution", "nested-selection-agrees"], + "note": "Rebuilding from selected children retains their identity." + }, + { + "kind": "historical-addressed", + "limitation": "addressed-views" + } + ] + } ], "gaps": [ - { "id": "origin-bearing-construction", "disposition": "met-structurally", "cases": ["own-and-descendants", "refusing-own-and-descendants", "complete-cuts-agree", "children-alone-lose-own"], "note": "Public Compose/compose(own, children) takes the own value. The ledger entries remain true of addressed Mount, which is child-only by design.", "limitation": "addressed-mount" }, - { "id": "conflicting-segments-accepted", "disposition": "met-structurally", "cases": ["duplicate-keys-refused"], "limitation": "addressed-mount" }, - { "id": "invalid-segments-accepted", "disposition": "meaning-changed", "cases": ["own-and-descendants", "bridge-exact-utf8-image"], "note": "No tree key is invalid. The bridge carries the exact UTF-8 image and refuses ill-formed segments before any primitive.", "limitation": "addressed-mount" }, - { "id": "missing-children-accepted", "disposition": "met-structurally", "cases": ["missing-child-refused"], "limitation": "addressed-mount" } + { + "id": "origin-bearing-construction", + "disposition": "met-structurally", + "cases": [ + "own-and-descendants", + "refusing-own-and-descendants", + "complete-cuts-agree", + "children-alone-lose-own" + ], + "note": "Public Compose/compose(own, children) takes the own value. The ledger entries remain true of addressed Mount, which is child-only by design. Its carrier members (forwarded-reconstruction, mounted-carrier-reconstruction, pending-across-reconstruction) are met through test-only adapters that bind a Wire to a carrier path and serve a tree on a dispatcher; production facilities for both are bitruntime#15.", + "limitation": "addressed-mount" + }, + { + "id": "conflicting-segments-accepted", + "disposition": "met-structurally", + "cases": [ + "duplicate-keys-refused" + ], + "limitation": "addressed-mount" + }, + { + "id": "invalid-segments-accepted", + "disposition": "meaning-changed", + "cases": ["own-and-descendants", "bridge-exact-utf8-image"], + "note": "No tree key is invalid. The bridge carries the exact UTF-8 image and refuses ill-formed segments before any primitive.", + "limitation": "addressed-mount" + }, + { + "id": "missing-children-accepted", + "disposition": "met-structurally", + "cases": [ + "missing-child-refused" + ], + "limitation": "addressed-mount" + } ], "ledgerLimitations": [ - { "id": "endpoint-typed-children", "disposition": "dissolved", "note": "Tree children are DeixisNode values whose own is a send-only Wire; construction needs no receive attachment or closure authority.", "cases": ["own-and-descendants"] }, - { "id": "owner-retained-parts", "disposition": "superseded", "note": "Decision 0012 makes decomposition public; restricted callers receive the bridge.", "cases": ["bridge-exact-utf8-image"], "limitation": "owner-only-parts" } + { + "id": "endpoint-typed-children", + "disposition": "dissolved", + "note": "Tree children are DeixisNode values whose own is a send-only Wire; construction needs no receive attachment or closure authority.", + "cases": [ + "own-and-descendants" + ] + }, + { + "id": "owner-retained-parts", + "disposition": "superseded", + "note": "Decision 0012 makes decomposition public; restricted callers receive the bridge.", + "cases": [ + "bridge-exact-utf8-image" + ], + "limitation": "owner-only-parts" + } ] } diff --git a/docs/composition.md b/docs/composition.md index f28a4cd..123906a 100644 --- a/docs/composition.md +++ b/docs/composition.md @@ -65,29 +65,6 @@ profile-defined correlation, context and live-reference obligations. invocation-lifecycle paths. Replacing it with a primitive Wire would erase those operations and requires a separate explicit lifecycle design. -## Full trees across carriers - -A tree crosses a carrier as two trees. The far side serves its nodes on an -endpoint; the near side declares the same structure, and each of its own Wires -sends at the corresponding far path. Structure stays local on both sides: -the carrier carries paths and messages, never children or own capabilities. - -- `bitwire/1` refuses a request at a peer root's empty path, so a served tree - sits under a nonempty prefix. -- A carrier path names a far position. Addressed access cannot show that two - positions share a node, so the near side binds each position. -- Missing and refusing remain distinct: a near path that does not exist sends - nothing; a far path without a node answers `method_not_found`; a far node - whose own refuses answers `internal`. -- A far side that serves through an invocation-aware dispatcher keeps each - admitted request's cancellation with the node that admitted it, even after - that node is replaced. - -The [full-tree cases](../conformance/wiretree/README.md) check these -observations against released bitruntime. The adapters that bind a Wire to a -carrier path and serve a tree on a dispatcher are test-only there; production -facilities for them belong to bitruntime. - ## Ownership and evidence Bitwire owns the declarations, laws, protocol and independent expectations. @@ -98,6 +75,9 @@ payloads or make an access handle proof of authorization. The [tree reference cases](../conformance/trees/README.md) exercise the new structural contract using test-only interpreters. The +[full-tree cases](../conformance/wiretree/README.md) run it against released +bitruntime, including across its carriers through test-only adapters; the +carrier model they describe is a test model, not part of this contract. The [declared composition evidence](../conformance/declared/README.md) and [runnable example catalogue](../examples/README.md) retain the older addressed interpretation and its versioned runtime observations. Historical green cases diff --git a/docs/wire/contract.md b/docs/wire/contract.md index d945ed2..0917692 100644 --- a/docs/wire/contract.md +++ b/docs/wire/contract.md @@ -90,7 +90,8 @@ read(tree, path) = tree.at(path).own().read() ``` The final equations require an existing path; read is the sibling DataTree -operation. Missing selection invokes no primitive. Equivalence preserves exact +operation. Missing selection invokes no primitive, and derived sending on a missing +path is refused: it never reports admission. Equivalence preserves exact keys, complete structure, own/child capability identities and shared instances; it does not copy primitive state. Constructors and derived sending belong to bitruntime, not this declarations package. diff --git a/scripts/conformance-runtime.mjs b/scripts/conformance-runtime.mjs index 7d169f9..ff04b64 100644 --- a/scripts/conformance-runtime.mjs +++ b/scripts/conformance-runtime.mjs @@ -60,10 +60,16 @@ const trees = read(files.trees); const gaps = read(files.gaps); const wiretree = read(files.wiretree); const disposition = validateDisposition(read(files.disposition), wiretree, bytes); -// Deliberately unlawful TypeScript realizations; each must fail at least one case. +// Deliberately unlawful TypeScript realizations: each must fail the case aimed at it. const mutants = [ - ['fallback', 'local'], ['wrapping-own', 'local'], ['normalizing', 'local'], ['fabricating', 'local'], - ['latin1-bridge', 'local'], ['incomplete-children', 'local'], ['retargeting-serve', 'carrier'], + ['fallback', 'local', 'missing-never-falls-back'], + ['wrapping-own', 'local', 'root-cut-reconstruction'], + ['normalizing', 'local', 'own-and-descendants'], + ['fabricating', 'local', 'refusing-versus-missing'], + ['latin1-bridge', 'local', 'bridge-exact-utf8-image'], + ['lossy-bridge', 'local', 'bridge-exact-utf8-image'], + ['incomplete-children', 'local', 'own-and-descendants'], + ['retargeting-serve', 'carrier', 'carrier-cancel-across-replacement'], ]; const carriers = [['local', '0'], ['peer', '0'], ['peer', '1']]; const scratch = mkdtempSync(join(tmpdir(), 'bitwire-runtime-')); @@ -255,11 +261,11 @@ try { } } if (language === 'ts') { - for (const [mutant, scope] of mutants) { + for (const [mutant, scope, target] of mutants) { const actual = JSON.parse(driver('wiretree', [`mutant:${mutant}`, scope, wiretreeInput], scope === 'carrier' ? carrierEnv('local', '0') : {})); const failed = wiretreeFailures(wiretree, actual, scope === 'carrier' ? ['carrier'] : ['structure', 'bridge']); - assert.ok(failed.length > 0, `the unlawful realization ${mutant} passes every case`); - pass(`ts/wiretree/mutant/${mutant}`, `rejected by ${failed.length} case${failed.length === 1 ? '' : 's'}, including ${failed[0]}`); + assert.ok(failed.includes(target), `the unlawful realization ${mutant} passes ${target}`); + pass(`ts/wiretree/mutant/${mutant}`, `rejected by ${target}${failed.length > 1 ? ` and ${failed.length - 1} other case${failed.length === 2 ? '' : 's'}` : ''}`); } } } diff --git a/scripts/wiretree-lib.mjs b/scripts/wiretree-lib.mjs index 636c5f5..2b97ce3 100644 --- a/scripts/wiretree-lib.mjs +++ b/scripts/wiretree-lib.mjs @@ -63,9 +63,17 @@ export function validateWiretree(fixture) { assert.ok(declarations.has(test.root), `${test.id}: unknown root`); assert.ok(test.fault === undefined || (test.family === 'structure' && faults.has(test.fault)), `${test.id}: unknown fault`); assert.ok((!test.relay && !test.mount) || test.family === 'carrier', `${test.id}: relay and mount are carrier options`); + assert.ok(!test.foreign || test.family === 'structure', `${test.id}: foreign is a structure option`); for (const step of test.steps) { assert.ok(operations[test.family].has(step.op), `${test.id}: ${step.op} is not a ${test.family} operation`); if (step.node !== undefined) assert.ok(declarations.has(step.node), `${test.id}: unknown node ${step.node}`); + if (step.op === 'bridgeInvalid') { + // One segment that is not a Unicode scalar string, in both representations. + assert.ok(Array.isArray(step.utf16) && step.utf16.every(unit => /^[0-9a-f]{4}$/.test(unit)), `${test.id}: bridgeInvalid needs utf16 code units`); + assert.ok(!String.fromCharCode(...step.utf16.map(unit => parseInt(unit, 16))).isWellFormed(), `${test.id}: the utf16 segment is well formed`); + assert.match(step.utf8, /^(?:[0-9a-f]{2})+$/, `${test.id}: bridgeInvalid needs utf8 bytes`); + assert.throws(() => new TextDecoder('utf-8', { fatal: true }).decode(Buffer.from(step.utf8, 'hex')), `${test.id}: the utf8 segment is valid UTF-8`); + } if (test.family === 'carrier' && ['rebuild', 'replace'].includes(step.op)) { assert.ok(['near', 'far'].includes(step.side), `${test.id}: ${step.op} names its side`); } @@ -95,8 +103,8 @@ export function wiretreeInputs(fixture) { declarations: fixture.declarations.map(({ id, own, children }) => ({ id, own, children: (children ?? []).map(([key, child]) => [keyHex(key), child]), })), - cases: fixture.cases.map(({ id, family, root, fault, relay, mount, steps }) => ({ - id, family, root, ...(fault ? { fault } : {}), ...(relay ? { relay } : {}), ...(mount ? { mount } : {}), + cases: fixture.cases.map(({ id, family, root, fault, foreign, relay, mount, steps }) => ({ + id, family, root, ...(fault ? { fault } : {}), ...(foreign ? { foreign } : {}), ...(relay ? { relay } : {}), ...(mount ? { mount } : {}), steps: steps.map(stepInput), })), }; diff --git a/scripts/wiretree.test.mjs b/scripts/wiretree.test.mjs index 6a682f2..db35a24 100644 --- a/scripts/wiretree.test.mjs +++ b/scripts/wiretree.test.mjs @@ -33,7 +33,7 @@ test('drivers receive inputs, never the oracle', () => { test('missing, extra, duplicate and incorrect observations fail the gate', () => { const families = ['structure', 'bridge']; - assert.equal(compareWiretree(fixture, conforming(families).reverse(), families, 'valid'), 19); + assert.equal(compareWiretree(fixture, conforming(families).reverse(), families, 'valid'), 20); assert.throws(() => compareWiretree(fixture, conforming(families).slice(1), families, 'missing')); assert.throws(() => compareWiretree(fixture, [...conforming(families), conforming(families)[0]], families, 'duplicate')); assert.throws(() => compareWiretree(fixture, [...conforming(families), conforming(['carrier'])[0]], families, 'extra'));