Skip to content

Commit a1f73ff

Browse files
fix: session encoding used per-response IDs, not stable session-global IDs
encodeWithSession assigned per-response local IDs instead of stable session-global IDs, so cross-call dedup references (@n # previously transmitted) pointed at the wrong symbols; the header also emitted zero-valued budget/tokens/edges. Both fixed to match the Go reference and the shared graph-session fixtures - graph session output is now byte-identical across all six SDKs. Undetected because the conformance runner skipped the graph-session fixtures. Runner hardened: unhandled operations now hard-fail instead of silently skipping; session, delta-encode, roundtrip, and pack-root fixtures now run; delta-verify is the only explicit allow-list (pending a graph delta wire decoder).
1 parent 6228b61 commit a1f73ff

3 files changed

Lines changed: 111 additions & 20 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
### Fixes
66

7+
- The conformance runner now hard-fails on any unhandled operation (instead of silently skipping it) and exercises session, delta-encode, roundtrip, and pack-root fixtures; only `delta-verify` is explicitly allow-listed, pending a graph delta wire decoder.
8+
- **Session encoding correctness fix.** `encode_with_session` assigned 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-valued `budget`/`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).
79
- 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 shared `graph-pack-root` fixtures, which it had been skipping (so this primitive was previously unimplemented and untested).
810
- 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`/`edges` when zero (previously symbols kept input order and zero-valued header fields were always emitted). The conformance runner now exercises the shared `graph-encode` fixtures (001-003), which it had been skipping - which is how this divergence went uncaught.
911
- 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.

src/gcf/session.py

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -77,22 +77,32 @@ def encode_with_session(p: Payload, sess: Session | None = None) -> str:
7777

7878
parts: list[str] = []
7979

80-
# Build local ID mapping for this response.
80+
# Build session-stable ID mapping (matching the reference encoder):
81+
# previously transmitted symbols keep their existing session-global ID;
82+
# new symbols get the next available session IDs in payload order. These
83+
# are NOT per-call local indices, so IDs stay stable across calls within a
84+
# session (see graph-session conformance fixtures).
8185
local_index: dict[str, int] = {}
82-
for i, s in enumerate(p.symbols):
83-
local_index[s.qualified_name] = i
84-
85-
# Count valid edges.
86-
valid_edges = sum(
87-
1 for e in p.edges
88-
if e.source in local_index and e.target in local_index
89-
)
90-
91-
# Header with session=true marker.
92-
header = (
93-
f"GCF profile=graph tool={p.tool} budget={p.token_budget} tokens={p.tokens_used} "
94-
f"symbols={len(p.symbols)} edges={valid_edges} session=true"
95-
)
86+
for s in p.symbols:
87+
if sess.transmitted(s.qualified_name):
88+
local_index[s.qualified_name] = sess.get_id(s.qualified_name)
89+
next_new = sess.size()
90+
for s in p.symbols:
91+
if not sess.transmitted(s.qualified_name):
92+
local_index[s.qualified_name] = next_new
93+
next_new += 1
94+
95+
# Header with session=true marker. Omit budget/tokens/edges when zero,
96+
# matching the reference encoder.
97+
header = f"GCF profile=graph tool={p.tool}"
98+
if p.token_budget > 0:
99+
header += f" budget={p.token_budget}"
100+
if p.tokens_used > 0:
101+
header += f" tokens={p.tokens_used}"
102+
header += f" symbols={len(p.symbols)}"
103+
if p.edges:
104+
header += f" edges={len(p.edges)}"
105+
header += " session=true"
96106
if p.pack_root:
97107
header += f" pack_root={p.pack_root}"
98108
parts.append(header)
@@ -128,7 +138,7 @@ def encode_with_session(p: Payload, sess: Session | None = None) -> str:
128138

129139
# Edges section.
130140
if p.edges:
131-
parts.append(f"## edges [{valid_edges}]")
141+
parts.append(f"## edges [{len(p.edges)}]")
132142
for e in p.edges:
133143
src_idx = local_index.get(e.source)
134144
tgt_idx = local_index.get(e.target)

tests/test_conformance_v2.py

Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,6 @@ def _subset_match(expected, got):
8383
def test_conformance(rel_path, data):
8484
op = data.get("operation")
8585

86-
if op in ("session", "delta"):
87-
pytest.skip(f"{op} not implemented")
88-
8986
if data.get("inputBase64"):
9087
pytest.skip("binary input")
9188

@@ -309,5 +306,87 @@ def _set(s):
309306
f"graph pack-root mismatch:\n got: {got}\n exp: {data['expected']}"
310307
)
311308

309+
elif op == "session":
310+
# Graph session dedup (SPEC 6.x). A single Session persists across all
311+
# calls in the fixture; symbol IDs must stay session-stable, and already
312+
# transmitted symbols collapse to bare `@N # previously transmitted`
313+
# references. Byte-compare every call against its expected wire.
314+
from gcf.session import Session, encode_with_session
315+
from gcf.types import Edge, Payload, Symbol
316+
317+
sess = Session()
318+
for i, call in enumerate(data["calls"]):
319+
inp = call["input"]
320+
payload = Payload(
321+
tool=inp.get("tool", ""),
322+
token_budget=inp.get("tokenBudget", 0),
323+
tokens_used=inp.get("tokensUsed", 0),
324+
pack_root=inp.get("packRoot", ""),
325+
symbols=[
326+
Symbol(
327+
qualified_name=s["qualifiedName"],
328+
kind=s["kind"],
329+
score=s["score"],
330+
provenance=s["provenance"],
331+
distance=s.get("distance", 0),
332+
)
333+
for s in inp.get("symbols", [])
334+
],
335+
edges=[
336+
Edge(source=e["source"], target=e["target"], edge_type=e["edgeType"])
337+
for e in inp.get("edges", [])
338+
],
339+
)
340+
got = encode_with_session(payload, sess)
341+
assert got == call["expected"], (
342+
f"session call {i + 1} wire mismatch:\n"
343+
f" got: {got!r}\n exp: {call['expected']!r}"
344+
)
345+
346+
elif op == "delta":
347+
# graph-delta fixtures share the `delta` operation but come in two shapes:
348+
# - encode: `input` is a structured object -> byte-compare encode_delta.
349+
# - verify: `input` is a WIRE STRING (with a base_snapshot) -> the wire
350+
# decoder + verifier is not yet implemented, so skip like delta-verify.
351+
inp = data["input"]
352+
if isinstance(inp, str) or "base_snapshot" in data:
353+
pytest.skip("graph delta wire decoder not yet implemented")
354+
355+
from gcf.delta import encode_delta
356+
from gcf.types import DeltaPayload, Edge, Symbol
357+
358+
def _sym(s):
359+
return Symbol(
360+
qualified_name=s["qualifiedName"],
361+
kind=s["kind"],
362+
score=s.get("score", 0.0),
363+
provenance=s.get("provenance", ""),
364+
distance=s.get("distance", 0),
365+
)
366+
367+
def _edge(e):
368+
return Edge(source=e["source"], target=e["target"], edge_type=e["edgeType"])
369+
370+
d = DeltaPayload(
371+
tool=inp.get("tool", ""),
372+
base_root=inp["baseRoot"],
373+
new_root=inp["newRoot"],
374+
removed=[_sym(s) for s in inp.get("removed", [])],
375+
added=[_sym(s) for s in inp.get("added", [])],
376+
removed_edges=[_edge(e) for e in inp.get("removedEdges", [])],
377+
added_edges=[_edge(e) for e in inp.get("addedEdges", [])],
378+
delta_tokens=inp.get("deltaTokens", 0),
379+
full_tokens=inp.get("fullTokens", 0),
380+
)
381+
got = encode_delta(d)
382+
assert got == data["expected"], (
383+
f"delta encode mismatch:\n got: {got!r}\n exp: {data['expected']!r}"
384+
)
385+
386+
elif op == "delta-verify":
387+
pytest.skip("graph delta wire decoder not yet implemented")
388+
312389
else:
313-
pytest.skip(f"unknown operation: {op}")
390+
pytest.fail(
391+
f"unhandled operation: {op}; must be handled or explicitly allow-listed"
392+
)

0 commit comments

Comments
 (0)