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 CHANGED
@@ -65,4 +65,4 @@ __all__ = [
65
65
  "encode_with_session",
66
66
  ]
67
67
 
68
- __version__ = "0.1.3"
68
+ __version__ = "1.0.0"
gcf/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Allow running as `python -m gcf`."""
2
+
3
+ from .cli import main
4
+
5
+ main()
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 [file] Encode JSON payload to GCF (stdin if no file)
14
- gcf decode [file] Decode GCF text to JSON (stdin if no file)
15
- gcf stats [file] Compare token counts: JSON vs GCF (stdin if no file)
16
- gcf version Print version
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
- print("gcf 0.1.0")
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]