protobuf: add Value.t AST + Cursor zipper
Schema-free layer on top of [Codec]: two new modules that together
let callers inspect, query, and rewrite arbitrary protobuf messages
without a typed schema.
- [Protobuf.Value] — AST over the four wire-type leaves plus a
[Message] variant ([(int * t) list] with wire-order-preserving
repeated tags). Per-node [Loc.Meta.t] sets up future byte-offset
tracking; for now everything is [Meta.none].
- constructors: [varint] / [fixed32] / [fixed64] / [length_delim]
/ [message]
- queries: [find : int -> t -> t option],
[find_all : int -> t -> t list],
[message_of : t -> t option] (re-parse a length-delim blob as
a nested message)
- IO: [of_string] / [of_string_exn] / [to_string]
- [pp] / [equal]
Length-delim blobs stay raw in the AST because schema-free parsing
cannot distinguish a string from a bytes from a nested message.
- [Protobuf.Cursor] — zipper over [Value.t] with ancestor stack:
[root] / [focus] / [up] / [top] / [set] / [down_field : int] /
[down_length_delim]. The last re-parses a length-delim leaf as a
message and descends, making multi-level traversals straightforward
once the caller knows a blob is a sub-message.
FieldMask paths: [of_field_mask] / [to_field_mask] parse and
serialise dotted-integer paths (e.g. ["1.3.2"]). The protobuf spec's
[google.protobuf.FieldMask] uses field *names*; our schema-free
cursor only sees tags, so integers replace names.
Top-level [Protobuf] re-exports [module Value = Value] and
[module Cursor = Cursor].
Tests: three Value round-trips (encode via a schema codec, decode as
Value, re-encode, compare bytes) and four Cursor operations (root
focus, down+up, set+rebuild, FieldMask parse + nested length-delim
descent). All 60 unit + 17 fuzz + 2 protoc interop tests pass.