Skip to content

Commit c4b394e

Browse files
v0.2.0: edges [N] section header and edges=N header field
Breaking: encode() and encode_with_session() now emit edges=N in header and ## edges [N] as section header. decode() strips bracket suffix.
1 parent 39224f9 commit c4b394e

7 files changed

Lines changed: 49 additions & 25 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
## v0.2.0 (2026-06-05)
4+
5+
- **Breaking**: `encode()` now emits `edges=N` in header line
6+
- **Breaking**: `encode()` now emits `## edges [N]` section header (was `## edges`)
7+
- `decode()` updated to parse `## edges [N]` format (strips bracket suffix)
8+
- Session encoder updated to emit new edge count format
9+
310
## v0.1.3 (2026-06-04)
411

512
- Docs: update README for PyPI discoverability (gcformat.com, proxy, vs-toon links)

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,12 @@ output = encode(p)
6161

6262
Output:
6363
```
64-
GCF tool=context_for_task budget=5000 tokens=1847 symbols=2
64+
GCF tool=context_for_task budget=5000 tokens=1847 symbols=2 edges=1
6565
## targets
6666
@0 fn pkg.AuthMiddleware 0.78 lsp_resolved
6767
## related
6868
@1 fn pkg.NewServer 0.54 lsp_resolved
69-
## edges
69+
## edges [1]
7070
@0<@1 calls
7171
```
7272

src/gcf/decode.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ def decode(input_text: str) -> Payload:
5151
# Group header.
5252
if line.startswith("## "):
5353
group = line[3:]
54+
# Strip bracket suffix: "edges [200]" -> "edges"
55+
bracket_idx = group.find(" [")
56+
if bracket_idx >= 0:
57+
group = group[:bracket_idx]
5458
in_edges = group == "edges"
5559
if not in_edges:
5660
if group == "targets":

src/gcf/encode.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,23 @@ def encode(p: Payload) -> str:
1717
"""
1818
parts: list[str] = []
1919

20-
# Header line.
21-
header = f"GCF tool={p.tool} budget={p.token_budget} tokens={p.tokens_used} symbols={len(p.symbols)}"
22-
if p.pack_root:
23-
header += f" pack_root={p.pack_root}"
24-
parts.append(header)
25-
2620
# Build symbol index for edge references.
2721
sym_index: dict[str, int] = {}
2822
for i, s in enumerate(p.symbols):
2923
sym_index[s.qualified_name] = i
3024

25+
# Count valid edges (both endpoints in symbol index).
26+
valid_edges = sum(
27+
1 for e in p.edges
28+
if e.source in sym_index and e.target in sym_index
29+
)
30+
31+
# Header line.
32+
header = f"GCF tool={p.tool} budget={p.token_budget} tokens={p.tokens_used} symbols={len(p.symbols)} edges={valid_edges}"
33+
if p.pack_root:
34+
header += f" pack_root={p.pack_root}"
35+
parts.append(header)
36+
3137
# Group symbols by distance.
3238
groups = _group_by_distance(p.symbols)
3339
group_names = ["targets", "related", "extended"]
@@ -58,7 +64,7 @@ def encode(p: Payload) -> str:
5864
if e.status and e.status != "unchanged":
5965
line += f" {e.status}"
6066
edge_lines.append(line)
61-
parts.append("## edges")
67+
parts.append(f"## edges [{len(edge_lines)}]")
6268
parts.extend(edge_lines)
6369

6470
return "\n".join(parts) + "\n"

src/gcf/session.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,20 +77,26 @@ 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.
81+
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+
8091
# Header with session=true marker.
8192
header = (
8293
f"GCF tool={p.tool} budget={p.token_budget} tokens={p.tokens_used} "
83-
f"symbols={len(p.symbols)} session=true"
94+
f"symbols={len(p.symbols)} edges={valid_edges} session=true"
8495
)
8596
if p.pack_root:
8697
header += f" pack_root={p.pack_root}"
8798
parts.append(header)
8899

89-
# Build local ID mapping for this response.
90-
local_index: dict[str, int] = {}
91-
for i, s in enumerate(p.symbols):
92-
local_index[s.qualified_name] = i
93-
94100
# Track which symbols are new (need full declaration).
95101
new_symbols: list[Symbol] = []
96102

@@ -122,7 +128,7 @@ def encode_with_session(p: Payload, sess: Session | None = None) -> str:
122128

123129
# Edges section.
124130
if p.edges:
125-
parts.append("## edges")
131+
parts.append(f"## edges [{valid_edges}]")
126132
for e in p.edges:
127133
src_idx = local_index.get(e.source)
128134
tgt_idx = local_index.get(e.target)

tests/test_decode.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def test_decode_basic_payload():
1313
"@0 fn pkg.AuthMiddleware 0.78 lsp_resolved\n"
1414
"## related\n"
1515
"@1 fn pkg.NewServer 0.54 lsp_resolved\n"
16-
"## edges\n"
16+
"## edges [1]\n"
1717
"@0<@1 calls\n"
1818
)
1919

@@ -93,7 +93,7 @@ def test_decode_edge_with_status():
9393
"## targets\n"
9494
"@0 fn a.A 0.90 x\n"
9595
"@1 fn b.B 0.80 x\n"
96-
"## edges\n"
96+
"## edges [1]\n"
9797
"@0<@1 calls added\n"
9898
)
9999
p = decode(input_text)
@@ -171,7 +171,7 @@ def test_decode_edge_missing_separator():
171171
"## targets\n"
172172
"@0 fn a.A 0.90 x\n"
173173
"@1 fn b.B 0.80 x\n"
174-
"## edges\n"
174+
"## edges [1]\n"
175175
"@0@1 calls\n"
176176
)
177177
with pytest.raises(DecodeError, match="missing '<' separator"):
@@ -184,7 +184,7 @@ def test_decode_edge_unknown_symbol():
184184
"GCF tool=test budget=100 tokens=50 symbols=1\n"
185185
"## targets\n"
186186
"@0 fn a.A 0.90 x\n"
187-
"## edges\n"
187+
"## edges [1]\n"
188188
"@0<@99 calls\n"
189189
)
190190
with pytest.raises(DecodeError, match="unknown symbol id"):

tests/test_encode.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,12 @@ def test_encode_basic_payload():
3232

3333
output = encode(p)
3434
expected = (
35-
"GCF tool=context_for_task budget=5000 tokens=1847 symbols=2\n"
35+
"GCF tool=context_for_task budget=5000 tokens=1847 symbols=2 edges=1\n"
3636
"## targets\n"
3737
"@0 fn pkg.AuthMiddleware 0.78 lsp_resolved\n"
3838
"## related\n"
3939
"@1 fn pkg.NewServer 0.54 lsp_resolved\n"
40-
"## edges\n"
40+
"## edges [1]\n"
4141
"@0<@1 calls\n"
4242
)
4343
assert output == expected
@@ -175,13 +175,14 @@ def test_encode_skips_edges_with_missing_symbols():
175175
)
176176
output = encode(p)
177177
# Section header is emitted (matches Go), but no edge lines beneath it
178-
assert "## edges" in output
179-
lines_after_edges = output.split("## edges\n")[1]
178+
assert "## edges [0]" in output
179+
assert "edges=0" in output
180+
lines_after_edges = output.split("## edges [0]\n")[1]
180181
assert lines_after_edges.strip() == ""
181182

182183

183184
def test_encode_empty_payload():
184185
"""Empty payload produces only header."""
185186
p = Payload(tool="test", token_budget=100, tokens_used=0)
186187
output = encode(p)
187-
assert output == "GCF tool=test budget=100 tokens=0 symbols=0\n"
188+
assert output == "GCF tool=test budget=100 tokens=0 symbols=0 edges=0\n"

0 commit comments

Comments
 (0)