gcf-python 0.5.1__py3-none-any.whl → 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gcf/__init__.py +1 -1
- gcf/__main__.py +5 -0
- gcf/cli.py +32 -5
- gcf/decode.py +17 -7
- gcf/decode_generic.py +399 -149
- gcf/delta.py +1 -1
- gcf/encode.py +1 -1
- gcf/generic.py +148 -125
- gcf/scalar.py +298 -0
- gcf/session.py +1 -1
- gcf/stream.py +9 -9
- gcf/stream_generic.py +7 -27
- {gcf_python-0.5.1.dist-info → gcf_python-1.0.1.dist-info}/METADATA +15 -5
- gcf_python-1.0.1.dist-info/RECORD +19 -0
- {gcf_python-0.5.1.dist-info → gcf_python-1.0.1.dist-info}/licenses/LICENSE +1 -1
- gcf_python-0.5.1.dist-info/RECORD +0 -17
- {gcf_python-0.5.1.dist-info → gcf_python-1.0.1.dist-info}/WHEEL +0 -0
- {gcf_python-0.5.1.dist-info → gcf_python-1.0.1.dist-info}/entry_points.txt +0 -0
gcf/__init__.py
CHANGED
gcf/__main__.py
ADDED
gcf/cli.py
CHANGED
|
@@ -5,19 +5,25 @@ import sys
|
|
|
5
5
|
|
|
6
6
|
from .decode import decode
|
|
7
7
|
from .encode import encode
|
|
8
|
+
from .decode_generic import decode_generic
|
|
9
|
+
from .generic import encode_generic
|
|
8
10
|
from .types import Edge, Payload, Symbol
|
|
9
11
|
|
|
10
12
|
USAGE = """gcf - token-optimized wire format for LLM tool responses
|
|
11
13
|
|
|
12
14
|
Usage:
|
|
13
|
-
gcf encode
|
|
14
|
-
gcf decode
|
|
15
|
-
gcf
|
|
16
|
-
gcf
|
|
15
|
+
gcf encode [file] Encode JSON graph payload to GCF (stdin if no file)
|
|
16
|
+
gcf decode [file] Decode GCF graph text to JSON (stdin if no file)
|
|
17
|
+
gcf encode-generic [file] Encode JSON to GCF generic profile (stdin if no file)
|
|
18
|
+
gcf decode-generic [file] Decode GCF generic text to JSON (stdin if no file)
|
|
19
|
+
gcf stats [file] Compare token counts: JSON vs GCF (stdin if no file)
|
|
20
|
+
gcf version Print version
|
|
17
21
|
|
|
18
22
|
Examples:
|
|
19
23
|
gcf encode < payload.json
|
|
20
24
|
gcf decode < payload.gcf
|
|
25
|
+
echo '{"name":"Alice"}' | gcf encode-generic
|
|
26
|
+
echo 'GCF profile=generic\\nname=Alice' | gcf decode-generic
|
|
21
27
|
gcf stats payload.json
|
|
22
28
|
"""
|
|
23
29
|
|
|
@@ -37,11 +43,18 @@ def main() -> None:
|
|
|
37
43
|
elif cmd == "decode":
|
|
38
44
|
data = _read_input(file_args)
|
|
39
45
|
_do_decode(data)
|
|
46
|
+
elif cmd == "encode-generic":
|
|
47
|
+
data = _read_input(file_args)
|
|
48
|
+
_do_encode_generic(data)
|
|
49
|
+
elif cmd == "decode-generic":
|
|
50
|
+
data = _read_input(file_args)
|
|
51
|
+
_do_decode_generic(data)
|
|
40
52
|
elif cmd == "stats":
|
|
41
53
|
data = _read_input(file_args)
|
|
42
54
|
_do_stats(data)
|
|
43
55
|
elif cmd == "version":
|
|
44
|
-
|
|
56
|
+
from . import __version__
|
|
57
|
+
print(f"gcf {__version__}")
|
|
45
58
|
else:
|
|
46
59
|
print(f"unknown command: {cmd}\n", file=sys.stderr)
|
|
47
60
|
print(USAGE, file=sys.stderr, end="")
|
|
@@ -115,6 +128,20 @@ def _payload_to_json(p: Payload) -> str:
|
|
|
115
128
|
return json.dumps(obj, indent=2)
|
|
116
129
|
|
|
117
130
|
|
|
131
|
+
def _do_encode_generic(data: str) -> None:
|
|
132
|
+
try:
|
|
133
|
+
obj = json.loads(data)
|
|
134
|
+
except json.JSONDecodeError as e:
|
|
135
|
+
print(f"error: invalid JSON: {e}", file=sys.stderr)
|
|
136
|
+
sys.exit(1)
|
|
137
|
+
print(encode_generic(obj), end="")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _do_decode_generic(data: str) -> None:
|
|
141
|
+
result = decode_generic(data)
|
|
142
|
+
print(json.dumps(result))
|
|
143
|
+
|
|
144
|
+
|
|
118
145
|
def _do_encode(data: str) -> None:
|
|
119
146
|
try:
|
|
120
147
|
p = _payload_from_json(data)
|
gcf/decode.py
CHANGED
|
@@ -35,7 +35,11 @@ def decode(input_text: str) -> Payload:
|
|
|
35
35
|
_parse_header(header[4:], p)
|
|
36
36
|
|
|
37
37
|
if not p.tool:
|
|
38
|
-
raise DecodeError("header missing required 'tool' field")
|
|
38
|
+
raise DecodeError("missing_tool: header missing required 'tool' field")
|
|
39
|
+
|
|
40
|
+
# Detect delta mode.
|
|
41
|
+
is_delta = "delta=true" in header
|
|
42
|
+
valid_delta_sections = {"removed", "added", "edges_removed", "edges_added"}
|
|
39
43
|
|
|
40
44
|
# Parse body: symbols and edges.
|
|
41
45
|
symbols: list[Symbol] = []
|
|
@@ -48,6 +52,10 @@ def decode(input_text: str) -> Payload:
|
|
|
48
52
|
if not line:
|
|
49
53
|
continue
|
|
50
54
|
|
|
55
|
+
# Skip ##! summary trailer.
|
|
56
|
+
if line.startswith("##! "):
|
|
57
|
+
continue
|
|
58
|
+
|
|
51
59
|
# Group header.
|
|
52
60
|
if line.startswith("## "):
|
|
53
61
|
group = line[3:]
|
|
@@ -55,6 +63,8 @@ def decode(input_text: str) -> Payload:
|
|
|
55
63
|
bracket_idx = group.find(" [")
|
|
56
64
|
if bracket_idx >= 0:
|
|
57
65
|
group = group[:bracket_idx]
|
|
66
|
+
if is_delta and group not in valid_delta_sections:
|
|
67
|
+
raise DecodeError(f"malformed_delta: invalid delta section {group!r}")
|
|
58
68
|
in_edges = group == "edges"
|
|
59
69
|
if not in_edges:
|
|
60
70
|
if group == "targets":
|
|
@@ -113,19 +123,19 @@ def _parse_header(fields: str, p: Payload) -> None:
|
|
|
113
123
|
def _parse_symbol_line(line: str, distance: int) -> tuple[Symbol, int]:
|
|
114
124
|
"""Parse a symbol line into a Symbol and its local ID."""
|
|
115
125
|
if not line.startswith("@"):
|
|
116
|
-
raise DecodeError(f"expected symbol line starting with @, got {line!r}")
|
|
126
|
+
raise DecodeError(f"invalid_node_line: expected symbol line starting with @, got {line!r}")
|
|
117
127
|
|
|
118
128
|
parts = line.split()
|
|
119
129
|
if len(parts) < 5:
|
|
120
130
|
raise DecodeError(
|
|
121
|
-
f"symbol line needs at least 5 fields, got {len(parts)} in {line!r}"
|
|
131
|
+
f"invalid_node_line: symbol line needs at least 5 fields, got {len(parts)} in {line!r}"
|
|
122
132
|
)
|
|
123
133
|
|
|
124
134
|
id_str = parts[0][1:] # strip @
|
|
125
135
|
try:
|
|
126
136
|
sym_id = int(id_str)
|
|
127
137
|
except ValueError as e:
|
|
128
|
-
raise DecodeError(f"invalid symbol id {id_str!r}: {e}") from e
|
|
138
|
+
raise DecodeError(f"invalid_symbol_id: invalid symbol id {id_str!r}: {e}") from e
|
|
129
139
|
|
|
130
140
|
kind = parts[1]
|
|
131
141
|
kind = KIND_EXPAND.get(kind, kind)
|
|
@@ -135,7 +145,7 @@ def _parse_symbol_line(line: str, distance: int) -> tuple[Symbol, int]:
|
|
|
135
145
|
try:
|
|
136
146
|
score = float(parts[3])
|
|
137
147
|
except ValueError as e:
|
|
138
|
-
raise DecodeError(f"invalid score {parts[3]!r}: {e}") from e
|
|
148
|
+
raise DecodeError(f"invalid_score: invalid score {parts[3]!r}: {e}") from e
|
|
139
149
|
|
|
140
150
|
provenance = parts[4]
|
|
141
151
|
|
|
@@ -157,7 +167,7 @@ def _parse_edge_line(line: str, sym_by_id: dict[int, Symbol]) -> Edge:
|
|
|
157
167
|
ref = parts[0]
|
|
158
168
|
lt_idx = ref.find("<")
|
|
159
169
|
if lt_idx < 0:
|
|
160
|
-
raise DecodeError(f"edge line missing '<' separator in {ref!r}")
|
|
170
|
+
raise DecodeError(f"invalid_edge_syntax: edge line missing '<' separator in {ref!r}")
|
|
161
171
|
|
|
162
172
|
target_id_str = ref[1:lt_idx] # strip leading @
|
|
163
173
|
source_id_str = ref[lt_idx + 2:] # strip <@
|
|
@@ -176,7 +186,7 @@ def _parse_edge_line(line: str, sym_by_id: dict[int, Symbol]) -> Edge:
|
|
|
176
186
|
source_sym = sym_by_id.get(source_id)
|
|
177
187
|
if target_sym is None or source_sym is None:
|
|
178
188
|
raise DecodeError(
|
|
179
|
-
f"edge references unknown symbol id(s): target={target_id} source={source_id}"
|
|
189
|
+
f"unknown_edge_reference: edge references unknown symbol id(s): target={target_id} source={source_id}"
|
|
180
190
|
)
|
|
181
191
|
|
|
182
192
|
edge_type = parts[1]
|