gcf-python 2.5.0__tar.gz → 2.5.2__tar.gz

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.
Files changed (47) hide show
  1. {gcf_python-2.5.0 → gcf_python-2.5.2}/CHANGELOG.md +8 -0
  2. {gcf_python-2.5.0 → gcf_python-2.5.2}/PKG-INFO +2 -2
  3. {gcf_python-2.5.0 → gcf_python-2.5.2}/README.md +1 -1
  4. {gcf_python-2.5.0 → gcf_python-2.5.2}/pyproject.toml +1 -1
  5. {gcf_python-2.5.0 → gcf_python-2.5.2}/src/gcf/decode.py +26 -1
  6. {gcf_python-2.5.0 → gcf_python-2.5.2}/src/gcf/decode_generic.py +8 -1
  7. {gcf_python-2.5.0 → gcf_python-2.5.2}/src/gcf/generic_delta.py +27 -13
  8. {gcf_python-2.5.0 → gcf_python-2.5.2}/.github/FUNDING.yml +0 -0
  9. {gcf_python-2.5.0 → gcf_python-2.5.2}/.github/workflows/ci.yml +0 -0
  10. {gcf_python-2.5.0 → gcf_python-2.5.2}/.github/workflows/publish.yml +0 -0
  11. {gcf_python-2.5.0 → gcf_python-2.5.2}/.gitignore +0 -0
  12. {gcf_python-2.5.0 → gcf_python-2.5.2}/LICENSE +0 -0
  13. {gcf_python-2.5.0 → gcf_python-2.5.2}/assets/divider-wave-2.png +0 -0
  14. {gcf_python-2.5.0 → gcf_python-2.5.2}/assets/divider.png +0 -0
  15. {gcf_python-2.5.0 → gcf_python-2.5.2}/assets/gcf-hero-wire-delta.png +0 -0
  16. {gcf_python-2.5.0 → gcf_python-2.5.2}/assets/gcf-python-diagram.png +0 -0
  17. {gcf_python-2.5.0 → gcf_python-2.5.2}/src/gcf/__init__.py +0 -0
  18. {gcf_python-2.5.0 → gcf_python-2.5.2}/src/gcf/__main__.py +0 -0
  19. {gcf_python-2.5.0 → gcf_python-2.5.2}/src/gcf/cli.py +0 -0
  20. {gcf_python-2.5.0 → gcf_python-2.5.2}/src/gcf/constants.py +0 -0
  21. {gcf_python-2.5.0 → gcf_python-2.5.2}/src/gcf/delta.py +0 -0
  22. {gcf_python-2.5.0 → gcf_python-2.5.2}/src/gcf/encode.py +0 -0
  23. {gcf_python-2.5.0 → gcf_python-2.5.2}/src/gcf/generic.py +0 -0
  24. {gcf_python-2.5.0 → gcf_python-2.5.2}/src/gcf/keyed_map.py +0 -0
  25. {gcf_python-2.5.0 → gcf_python-2.5.2}/src/gcf/packroot.py +0 -0
  26. {gcf_python-2.5.0 → gcf_python-2.5.2}/src/gcf/scalar.py +0 -0
  27. {gcf_python-2.5.0 → gcf_python-2.5.2}/src/gcf/session.py +0 -0
  28. {gcf_python-2.5.0 → gcf_python-2.5.2}/src/gcf/stream.py +0 -0
  29. {gcf_python-2.5.0 → gcf_python-2.5.2}/src/gcf/stream_generic.py +0 -0
  30. {gcf_python-2.5.0 → gcf_python-2.5.2}/src/gcf/types.py +0 -0
  31. {gcf_python-2.5.0 → gcf_python-2.5.2}/tests/__init__.py +0 -0
  32. {gcf_python-2.5.0 → gcf_python-2.5.2}/tests/test_conformance_v2.py +0 -0
  33. {gcf_python-2.5.0 → gcf_python-2.5.2}/tests/test_decode.py +0 -0
  34. {gcf_python-2.5.0 → gcf_python-2.5.2}/tests/test_delta.py +0 -0
  35. {gcf_python-2.5.0 → gcf_python-2.5.2}/tests/test_encode.py +0 -0
  36. {gcf_python-2.5.0 → gcf_python-2.5.2}/tests/test_generic.py +0 -0
  37. {gcf_python-2.5.0 → gcf_python-2.5.2}/tests/test_generic_delta.py +0 -0
  38. {gcf_python-2.5.0 → gcf_python-2.5.2}/tests/test_generic_delta_fuzz.py +0 -0
  39. {gcf_python-2.5.0 → gcf_python-2.5.2}/tests/test_generic_delta_session.py +0 -0
  40. {gcf_python-2.5.0 → gcf_python-2.5.2}/tests/test_keyed_map_fuzz.py +0 -0
  41. {gcf_python-2.5.0 → gcf_python-2.5.2}/tests/test_roundtrip.py +0 -0
  42. {gcf_python-2.5.0 → gcf_python-2.5.2}/tests/test_roundtrip_v2.py +0 -0
  43. {gcf_python-2.5.0 → gcf_python-2.5.2}/tests/test_session.py +0 -0
  44. {gcf_python-2.5.0 → gcf_python-2.5.2}/tests/test_stream.py +0 -0
  45. {gcf_python-2.5.0 → gcf_python-2.5.2}/tests/test_stream_fielddecl.py +0 -0
  46. {gcf_python-2.5.0 → gcf_python-2.5.2}/tests/test_stream_generic.py +0 -0
  47. {gcf_python-2.5.0 → gcf_python-2.5.2}/uv.lock +0 -0
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## v2.5.2 (2026-08-09)
4
+
5
+ - **Spec v3.5.1 conformance (SPEC 5, score-rounding errata).** SPEC 5 now pins the graph `score` two-decimal wire form to round-half-to-even on the exact IEEE-754 double, resolving a midpoint divergence in the JavaScript and Kotlin SDKs. This SDK's `f"{score:.2f}"` formatter already rounds half-to-even, so there is no behavior change; re-verified against the new `graph-encode/004_score_midpoint_rounding` conformance fixture.
6
+
7
+ ## v2.5.1 (2026-08-07)
8
+
9
+ - Decoders now reject a declared `[N]` section count that does not match the actual item count, in both directions, per SPEC Section 13 (Count Validation). A declared count smaller than the rows or entries present was previously read as a limit and the surplus was dropped; it is now an error. Covers the generic tabular, keyed-map, and root-array forms, the delta and full-set decoders, and the graph `## edges [N]` section. Valid payloads and encoding are unchanged.
10
+
3
11
  ## v2.5.0 (2026-08-07)
4
12
 
5
13
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gcf-python
3
- Version: 2.5.0
3
+ Version: 2.5.2
4
4
  Summary: The AI-native wire format for structured data. 50-92% fewer tokens than JSON, with multi-turn delta encoding for agent loops. 100% comprehension on every frontier model. Zero dependencies.
5
5
  Project-URL: Homepage, https://github.com/blackwell-systems/gcf-python
6
6
  Project-URL: Documentation, https://gcformat.com/
@@ -304,7 +304,7 @@ GCF wins 15/16 datasets on the expanded [token efficiency benchmark](https://git
304
304
 
305
305
  **Zero runtime dependencies. Permanently.** All six implementations depend only on their language's standard library. No transitive dependencies. No supply chain risk. This is a permanent commitment: GCF will never take on external runtime dependencies. MIT licensed. All implementations support both generic profile (`encodeGeneric`) and graph profile (`encode`). CLI included in all 6 languages.
306
306
 
307
- **Specification:** [SPEC v3.4.1 Stable](https://github.com/blackwell-systems/gcf/blob/main/SPEC.md) with 204 conformance fixtures, 43,000,000,000+ lossless round-trips verified across 5 formats and 6 languages. All implementations at v2.4.0+ (Go v1.5.0). Cross-language 6x6 matrix verified.
307
+ **Specification:** [SPEC v3.5.0 Stable](https://github.com/blackwell-systems/gcf/blob/main/SPEC.md) with 264 conformance fixtures, 43,000,000,000+ lossless round-trips verified across 5 formats and 6 languages. All implementations at v2.5.1+ (Go v1.6.1, Swift v2.6.0). Cross-language 6x6 matrix verified.
308
308
 
309
309
  ## Adopted by
310
310
 
@@ -279,7 +279,7 @@ GCF wins 15/16 datasets on the expanded [token efficiency benchmark](https://git
279
279
 
280
280
  **Zero runtime dependencies. Permanently.** All six implementations depend only on their language's standard library. No transitive dependencies. No supply chain risk. This is a permanent commitment: GCF will never take on external runtime dependencies. MIT licensed. All implementations support both generic profile (`encodeGeneric`) and graph profile (`encode`). CLI included in all 6 languages.
281
281
 
282
- **Specification:** [SPEC v3.4.1 Stable](https://github.com/blackwell-systems/gcf/blob/main/SPEC.md) with 204 conformance fixtures, 43,000,000,000+ lossless round-trips verified across 5 formats and 6 languages. All implementations at v2.4.0+ (Go v1.5.0). Cross-language 6x6 matrix verified.
282
+ **Specification:** [SPEC v3.5.0 Stable](https://github.com/blackwell-systems/gcf/blob/main/SPEC.md) with 264 conformance fixtures, 43,000,000,000+ lossless round-trips verified across 5 formats and 6 languages. All implementations at v2.5.1+ (Go v1.6.1, Swift v2.6.0). Cross-language 6x6 matrix verified.
283
283
 
284
284
  ## Adopted by
285
285
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "gcf-python"
7
- version = "2.5.0"
7
+ version = "2.5.2"
8
8
  description = "The AI-native wire format for structured data. 50-92% fewer tokens than JSON, with multi-turn delta encoding for agent loops. 100% comprehension on every frontier model. Zero dependencies."
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -45,6 +45,8 @@ def decode(input_text: str) -> Payload:
45
45
  sym_by_id: dict[int, Symbol] = {}
46
46
  current_distance = 0
47
47
  in_edges = False
48
+ declared_edges = -1
49
+ edges_declared = False
48
50
 
49
51
  for line in lines[1:]:
50
52
  line = line.rstrip("\r")
@@ -58,13 +60,29 @@ def decode(input_text: str) -> Payload:
58
60
  # Group header.
59
61
  if line.startswith("## "):
60
62
  group = line[3:]
61
- # Strip bracket suffix: "edges [200]" -> "edges"
63
+ # Strip bracket suffix: "edges [200]" -> "edges", capturing the
64
+ # declared count so it can be enforced per Section 13.
65
+ declared_count = -1
62
66
  bracket_idx = group.find(" [")
63
67
  if bracket_idx >= 0:
68
+ bracket = group[bracket_idx + 2:]
64
69
  group = group[:bracket_idx]
70
+ end = bracket.find("]")
71
+ if end >= 0:
72
+ cnt_str = bracket[:end]
73
+ if cnt_str != "?": # "[?]" is a streaming deferred count (Section 8)
74
+ try:
75
+ declared_count = int(cnt_str)
76
+ except ValueError:
77
+ raise DecodeError(
78
+ f"count_mismatch: invalid section count {cnt_str!r}"
79
+ )
65
80
  if is_delta and group not in valid_delta_sections:
66
81
  raise DecodeError(f"malformed_delta: invalid delta section {group!r}")
67
82
  in_edges = group == "edges"
83
+ if in_edges and declared_count >= 0:
84
+ declared_edges = declared_count
85
+ edges_declared = True
68
86
  if not in_edges:
69
87
  if group == "targets":
70
88
  current_distance = 0
@@ -91,6 +109,13 @@ def decode(input_text: str) -> Payload:
91
109
  symbols.append(sym)
92
110
  sym_by_id[sym_id] = sym
93
111
 
112
+ # Section 13: a declared [N] section count MUST match the actual item count.
113
+ # The graph edges section is the graph profile's only [N]-bearing section.
114
+ if edges_declared and len(p.edges) != declared_edges:
115
+ raise DecodeError(
116
+ f"count_mismatch: declared {declared_edges} edges, got {len(p.edges)}"
117
+ )
118
+
94
119
  p.symbols = symbols
95
120
  return p
96
121
 
@@ -91,7 +91,14 @@ def decode_generic(input_text: str) -> Any:
91
91
  return parse_scalar(first[1:])
92
92
 
93
93
  if first.startswith("## ["):
94
- arr, _ = _parse_array_from_header(content_lines, 0, 0, first[3:])
94
+ arr, consumed = _parse_array_from_header(content_lines, 0, 0, first[3:])
95
+ # A root array or keyed map spans the whole document, so any structural line
96
+ # past the consumed rows is a surplus item, not sibling content. The row loop
97
+ # stops at the declared count, so the count assert only catches the deficit
98
+ # case; surplus is caught here (SPEC Section 13: a mismatch, fewer OR more
99
+ # items than declared, is an error).
100
+ if consumed < len(content_lines):
101
+ raise ValueError("count_mismatch: declared count is fewer than the rows present")
95
102
  return arr
96
103
 
97
104
  result: dict[str, Any] = {}
@@ -299,16 +299,23 @@ def decode_generic_full(text: str) -> tuple[GenericSet, str]:
299
299
  while i < len(lines):
300
300
  line = lines[i]
301
301
  if not line.startswith("## "):
302
- i += 1
303
- continue
302
+ # Only blank lines, comments, and the ##! summary trailer are valid
303
+ # outside a section; any other line is a surplus row past a declared
304
+ # section count (Section 13).
305
+ if line == "" or line.startswith("# ") or line.startswith("##! "):
306
+ i += 1
307
+ continue
308
+ raise ValueError(
309
+ f"count_mismatch: unexpected content after declared section rows: {line!r}"
310
+ )
304
311
  name, count, fields, key_field = _parse_section_header(line[3:])
305
312
  s.name, s.fields = name, fields
306
313
  if not s.key:
307
314
  s.key = key_field
308
315
  i += 1
309
- for _ in range(count):
310
- if i >= len(lines):
311
- raise ValueError("delta_invalid: fewer rows than declared count")
316
+ for j in range(count):
317
+ if i >= len(lines) or lines[i].startswith("## "):
318
+ raise ValueError(f"count_mismatch: declared {count} rows, got {j}")
312
319
  s.rows.append(_parse_row(lines[i], fields))
313
320
  i += 1
314
321
  return s, hdr.get("pack_root", "")
@@ -335,8 +342,15 @@ def decode_generic_delta(text: str) -> GenericDeltaPayload:
335
342
  while i < len(lines):
336
343
  line = lines[i]
337
344
  if not line.startswith("## "):
338
- i += 1
339
- continue
345
+ # Only blank lines, comments, and the ##! summary trailer are valid
346
+ # outside a section; any other line is a surplus row past a declared
347
+ # section count (Section 13).
348
+ if line == "" or line.startswith("# ") or line.startswith("##! "):
349
+ i += 1
350
+ continue
351
+ raise ValueError(
352
+ f"count_mismatch: unexpected content after declared section rows: {line!r}"
353
+ )
340
354
  name, count, fields, key_field = _parse_section_header(line[3:])
341
355
  if not d.key and key_field:
342
356
  d.key = key_field
@@ -345,9 +359,9 @@ def decode_generic_delta(text: str) -> GenericDeltaPayload:
345
359
  i += 1
346
360
  if name in ("added", "changed"):
347
361
  rows = []
348
- for _ in range(count):
349
- if i >= len(lines):
350
- raise ValueError(f"delta_invalid: fewer rows than declared count in ## {name}")
362
+ for j in range(count):
363
+ if i >= len(lines) or lines[i].startswith("## "):
364
+ raise ValueError(f"count_mismatch: declared {count} rows in ## {name}, got {j}")
351
365
  rows.append(_parse_row(lines[i], fields))
352
366
  i += 1
353
367
  if name == "added":
@@ -355,9 +369,9 @@ def decode_generic_delta(text: str) -> GenericDeltaPayload:
355
369
  else:
356
370
  d.changed = rows
357
371
  elif name == "removed":
358
- for _ in range(count):
359
- if i >= len(lines):
360
- raise ValueError("delta_invalid: fewer identities than declared count in ## removed")
372
+ for j in range(count):
373
+ if i >= len(lines) or lines[i].startswith("## "):
374
+ raise ValueError(f"count_mismatch: declared {count} identities in ## removed, got {j}")
361
375
  d.removed.append(parse_scalar(lines[i], True))
362
376
  i += 1
363
377
  else:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes