v2.4.0: Graph delta decode + verify; session + conformance hardening
Fixes
- The conformance runner now hard-fails on any unhandled operation (instead of silently skipping it) and exercises session, delta, roundtrip, and pack-root fixtures end to end; the graph delta wire decode and verify path is now covered, so no operations remain allow-listed.
- Implemented the graph delta wire decoder and verifier (
decode_delta/verify_delta): parse aGCF profile=graph delta=truewire back into removed/added symbols and edge changes, apply them atomically to a base snapshot, recomputepack_root, and reject a wrongnew_rootwithroot_mismatch(SPEC 10.4). The## addedencoder now emits the trailingdistancefield (SPEC 3.4.1, Section 10.1). The sharedgraph-deltafixtures now run end to end: 001 (encode, gains the trailing distance), 002 (verified apply), 003 (root_mismatchrejection). - Session encoding correctness fix.
encode_with_sessionassigned per-response local IDs instead of stable session-global IDs, so the cross-call dedup references (@N # previously transmitted) pointed at the wrong symbols, and the header emitted zero-valuedbudget/tokens/edges. Both are fixed to match the reference; graph session output is now byte-identical across all six SDKs. This had gone undetected because the conformance runner skipped the shared graph-session fixtures (now wired). - Added the graph-profile PackRoot (
pack_root(symbols, edges), gcf-pack-root-v1, SPEC 10.2): the content-addressed sha256 over canonical, independently-sorted symbol/edge records, byte-identical to gcf-go/rust/typescript/swift/kotlin. The conformance runner now exercises the sharedgraph-pack-rootfixtures, which it had been skipping (so this primitive was previously unimplemented and untested). - Buffered graph encoder now matches the reference byte-for-byte: symbols are ordered by distance then descending score with local IDs assigned in output order, and the header omits
budget/tokens/edgeswhen zero (previously symbols kept input order and zero-valued header fields were always emitted). The conformance runner now exercises the sharedgraph-encodefixtures (001-003), which it had been skipping - which is how this divergence went uncaught. - Buffered graph encoder: order edges by source ID, then target ID, then edge type (SPEC 16.1), instead of emitting them in input order. Decode-invariant (edges are a set) and does not affect
pack_root(which sorts edge records independently), so no content addresses change. Pinned by shared fixturegraph-encode/003. Streaming edges remain in producer-arrival order. - Decoder: reject an orphan
.fieldattachment (a.fieldwhose name is neither a^-marked column of its row nor a>-containing field name, SPEC 7.4.6.1.4) instead of silently absorbing it as an undeclared extra field. Such a stray attachment previously decoded to a record no encoder produces, silently injecting a field onto the last-parsed row (a lossless round-trip hole); now rejected per SPEC 16.5 (orphan_attachment). - Decoder: reject an orphan positional inline body (a pipe-delimited line with no eligible
^{}attachment-marker cell) instead of silently dropping it. The object-body parser previously skipped any unrecognized line, so a stray positional body (e.g. a secondBob|b@t.comafter a row's one inline cell was filled) vanished with no error (silent data loss); now rejected per SPEC 16.5 (orphan_inline_attachment). - Graph streaming trailer: the edge count is now always the last
countsentry, even when the stream has no edges (positionalcounts=2,1,0; labeledcounts=…,edges:0). A zero-edge stream previously dropped it, violating the SPEC §8.4 / §8.4.1 rule that the edge count is always present and last (the invariant that keeps the positional form unambiguous). The graph trailer is decoder-ignored, so this changes producer output only.
Streaming: opt-in labeled trailer counts (SPEC §8.4.1)
- New
labeled_trailer_countskeyword onStreamEncoder. When set, the##! summarygraph streaming trailer emitscounts=in the labeled formlabel:countper group (e.g.counts=targets:2,related:1,edges:3) instead of the default positional values-only form (counts=2,1,3). Default false is byte-identical to prior output. - Opt-in and non-breaking: a producer-side comprehension aid for known weak consumers. The trailer counts remain informational (decoder-ignored) in both forms; neither changes the decoded payload. Mirrors the
gcf-goreference.
Conformance and docs
- Streaming graph trailer now emits
distance_Ngroup counts in pure group-header emission order (dropping a fixedtargets,related,extendedprefix), matching the other SDKs and deterministic per SPEC 16.1. Byte-identical for contract-conformant (ascending-distance) input; pinned by shared fixturesstreaming-v2/010–011. - The conformance runner now executes the
graph-stream-encodefixtures (streaming-encode parity, previously decode-only): fixture 004 (positional trailer) and 005 (labeled trailer). - README: corrected the streaming example trailer from the defunct
## _summary … sections=to the real##! summary … counts=; README now leads with the project diagram. - Added a generic-delta fuzz test (decoder never crashes; string round-trip).
Full Changelog: v2.3.0...v2.4.0