Skip to content

Commit 4d325ab

Browse files
fix: order graph edges by source then target in buffered encoder (SPEC 16.1)
The buffered graph encoder emitted edges in input order; SPEC 16.1 requires ordering by source ID then target ID. Edges are now sorted by (source ID, target ID, edge type) before emitting. Decode-invariant (edges are a set) and does not affect pack_root (which sorts edge records independently), so no content addresses change. Streaming edges remain in producer-arrival order. Pinned by shared fixture graph-encode/003.
1 parent fe01f42 commit 4d325ab

2 files changed

Lines changed: 14 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Fixes
66

7+
- 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 fixture `graph-encode/003`. Streaming edges remain in producer-arrival order.
78
- Decoder: reject an orphan `.field` attachment (a `.field` whose 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`).
89
- 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 second `Bob|b@t.com` after a row's one inline cell was filled) vanished with no error (silent data loss); now rejected per SPEC 16.5 (`orphan_inline_attachment`).
910
- Graph streaming trailer: the edge count is now always the last `counts` entry, even when the stream has no edges (positional `counts=2,1,0`; labeled `counts=…,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.

src/gcf/encode.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,17 +52,25 @@ def encode(p: Payload) -> str:
5252
kind = KIND_ABBREV.get(s.kind, s.kind)
5353
parts.append(f"@{idx} {kind} {s.qualified_name} {s.score:.2f} {s.provenance}")
5454

55-
# Edges section.
55+
# Edges section. Order edges by source ID then target ID (then edge type
56+
# for parallel edges) so the wire is canonical regardless of the order
57+
# edges were provided (SPEC 16.1). Edge reordering is decode-invariant
58+
# (edges are a set) and does not affect pack_root, which sorts edge records
59+
# independently.
5660
if p.edges:
57-
edge_lines: list[str] = []
61+
resolved: list[tuple[int, int, str, str]] = []
5862
for e in p.edges:
5963
src_idx = sym_index.get(e.source)
6064
tgt_idx = sym_index.get(e.target)
6165
if src_idx is None or tgt_idx is None:
6266
continue
63-
line = f"@{tgt_idx}<@{src_idx} {e.edge_type}"
64-
if e.status and e.status != "unchanged":
65-
line += f" {e.status}"
67+
resolved.append((src_idx, tgt_idx, e.edge_type, e.status))
68+
resolved.sort(key=lambda r: (r[0], r[1], r[2]))
69+
edge_lines: list[str] = []
70+
for src_idx, tgt_idx, edge_type, status in resolved:
71+
line = f"@{tgt_idx}<@{src_idx} {edge_type}"
72+
if status and status != "unchanged":
73+
line += f" {status}"
6674
edge_lines.append(line)
6775
parts.append(f"## edges [{len(edge_lines)}]")
6876
parts.extend(edge_lines)

0 commit comments

Comments
 (0)