gcf-python 2.5.3__tar.gz → 2.6.0__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.3 → gcf_python-2.6.0}/CHANGELOG.md +4 -0
  2. {gcf_python-2.5.3 → gcf_python-2.6.0}/PKG-INFO +2 -2
  3. {gcf_python-2.5.3 → gcf_python-2.6.0}/pyproject.toml +1 -1
  4. {gcf_python-2.5.3 → gcf_python-2.6.0}/src/gcf/__init__.py +1 -1
  5. {gcf_python-2.5.3 → gcf_python-2.6.0}/src/gcf/scalar.py +28 -6
  6. {gcf_python-2.5.3 → gcf_python-2.6.0}/tests/test_conformance_v2.py +17 -0
  7. {gcf_python-2.5.3 → gcf_python-2.6.0}/.github/FUNDING.yml +0 -0
  8. {gcf_python-2.5.3 → gcf_python-2.6.0}/.github/workflows/ci.yml +0 -0
  9. {gcf_python-2.5.3 → gcf_python-2.6.0}/.github/workflows/publish.yml +0 -0
  10. {gcf_python-2.5.3 → gcf_python-2.6.0}/.gitignore +0 -0
  11. {gcf_python-2.5.3 → gcf_python-2.6.0}/LICENSE +0 -0
  12. {gcf_python-2.5.3 → gcf_python-2.6.0}/README.md +0 -0
  13. {gcf_python-2.5.3 → gcf_python-2.6.0}/assets/divider-wave-2.png +0 -0
  14. {gcf_python-2.5.3 → gcf_python-2.6.0}/assets/divider.png +0 -0
  15. {gcf_python-2.5.3 → gcf_python-2.6.0}/assets/gcf-hero-wire-delta.png +0 -0
  16. {gcf_python-2.5.3 → gcf_python-2.6.0}/assets/gcf-python-diagram.png +0 -0
  17. {gcf_python-2.5.3 → gcf_python-2.6.0}/src/gcf/__main__.py +0 -0
  18. {gcf_python-2.5.3 → gcf_python-2.6.0}/src/gcf/cli.py +0 -0
  19. {gcf_python-2.5.3 → gcf_python-2.6.0}/src/gcf/constants.py +0 -0
  20. {gcf_python-2.5.3 → gcf_python-2.6.0}/src/gcf/decode.py +0 -0
  21. {gcf_python-2.5.3 → gcf_python-2.6.0}/src/gcf/decode_generic.py +0 -0
  22. {gcf_python-2.5.3 → gcf_python-2.6.0}/src/gcf/delta.py +0 -0
  23. {gcf_python-2.5.3 → gcf_python-2.6.0}/src/gcf/encode.py +0 -0
  24. {gcf_python-2.5.3 → gcf_python-2.6.0}/src/gcf/generic.py +0 -0
  25. {gcf_python-2.5.3 → gcf_python-2.6.0}/src/gcf/generic_delta.py +0 -0
  26. {gcf_python-2.5.3 → gcf_python-2.6.0}/src/gcf/keyed_map.py +0 -0
  27. {gcf_python-2.5.3 → gcf_python-2.6.0}/src/gcf/packroot.py +0 -0
  28. {gcf_python-2.5.3 → gcf_python-2.6.0}/src/gcf/session.py +0 -0
  29. {gcf_python-2.5.3 → gcf_python-2.6.0}/src/gcf/stream.py +0 -0
  30. {gcf_python-2.5.3 → gcf_python-2.6.0}/src/gcf/stream_generic.py +0 -0
  31. {gcf_python-2.5.3 → gcf_python-2.6.0}/src/gcf/types.py +0 -0
  32. {gcf_python-2.5.3 → gcf_python-2.6.0}/tests/__init__.py +0 -0
  33. {gcf_python-2.5.3 → gcf_python-2.6.0}/tests/test_decode.py +0 -0
  34. {gcf_python-2.5.3 → gcf_python-2.6.0}/tests/test_delta.py +0 -0
  35. {gcf_python-2.5.3 → gcf_python-2.6.0}/tests/test_encode.py +0 -0
  36. {gcf_python-2.5.3 → gcf_python-2.6.0}/tests/test_generic.py +0 -0
  37. {gcf_python-2.5.3 → gcf_python-2.6.0}/tests/test_generic_delta.py +0 -0
  38. {gcf_python-2.5.3 → gcf_python-2.6.0}/tests/test_generic_delta_fuzz.py +0 -0
  39. {gcf_python-2.5.3 → gcf_python-2.6.0}/tests/test_generic_delta_session.py +0 -0
  40. {gcf_python-2.5.3 → gcf_python-2.6.0}/tests/test_keyed_map_fuzz.py +0 -0
  41. {gcf_python-2.5.3 → gcf_python-2.6.0}/tests/test_roundtrip.py +0 -0
  42. {gcf_python-2.5.3 → gcf_python-2.6.0}/tests/test_roundtrip_v2.py +0 -0
  43. {gcf_python-2.5.3 → gcf_python-2.6.0}/tests/test_session.py +0 -0
  44. {gcf_python-2.5.3 → gcf_python-2.6.0}/tests/test_stream.py +0 -0
  45. {gcf_python-2.5.3 → gcf_python-2.6.0}/tests/test_stream_fielddecl.py +0 -0
  46. {gcf_python-2.5.3 → gcf_python-2.6.0}/tests/test_stream_generic.py +0 -0
  47. {gcf_python-2.5.3 → gcf_python-2.6.0}/uv.lock +0 -0
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## v2.6.0 (2026-08-14)
4
+
5
+ - **Numeric domain (spec v3.5.3, SPEC 2.3.2).** Specifies the canonical numeric domain as signed `int64` for integers and IEEE-754 double for non-integers. Earlier versions left integers beyond the double-exact range (2^53) to the host numeric type; this version parses integer literals to an exact `int64` on decode and on the JSON-to-value bridge, returns an out-of-range error for a value outside `int64` on both decode and encode, and models larger values (unsigned-64 identifiers, exact decimals) as strings. Canonical number formatting aligns to the domain: a double at or above 2^53 renders in exponent notation. Verified against new `numbers/017-024` and `errors-v2/041-042` conformance fixtures and the cross-SDK differential fuzz. An out-of-range value raises `ValueError`.
6
+
3
7
  ## v2.5.3 (2026-08-10)
4
8
 
5
9
  - **Losslessness fix (spec v3.5.2, SPEC 2.3/2.4).** The number grammar and numeric-like classification now pin digits to ASCII `[0-9]`. Python's `re` compiles `\d` in Unicode mode (matching `\p{Nd}`), so a value like `1.٥` (ASCII `1`, `.`, U+0665) was treated as number-shaped: quoted on encode, and a bare `1.٥` decoded through `float()` to the number `1.5`. That diverged from the ASCII SDKs and silently turned a string into a number when decoding wire produced by another SDK. `\d` is replaced with `[0-9]`; such tokens stay strings and encode bare. Verified against new `scalar/029-031` and `decode/007` conformance fixtures and the cross-SDK differential fuzz.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: gcf-python
3
- Version: 2.5.3
3
+ Version: 2.6.0
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/
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "gcf-python"
7
- version = "2.5.3"
7
+ version = "2.6.0"
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"}
@@ -102,4 +102,4 @@ __all__ = [
102
102
  "DEFAULT_REANCHOR_N",
103
103
  ]
104
104
 
105
- __version__ = "2.3.0"
105
+ __version__ = "2.6.0"
@@ -93,6 +93,15 @@ def format_scalar(v: Any, delimiter: str = "") -> str:
93
93
  if isinstance(v, bool):
94
94
  return "true" if v else "false"
95
95
  if isinstance(v, int) and not isinstance(v, bool):
96
+ # The encoder enforces the int64 domain (SPEC 2.3.2): a Python int is
97
+ # arbitrary-precision, so a host integer outside int64 is rejected here
98
+ # rather than emitted as a bare token the decoder would reject.
99
+ if v < -(2**63) or v > 2**63 - 1:
100
+ raise ValueError(
101
+ f"out_of_range: integer {v} is outside the canonical int64 "
102
+ "domain [-9223372036854775808, 9223372036854775807]; "
103
+ "model larger values as strings (SPEC 2.3.2)"
104
+ )
96
105
  return str(v)
97
106
  if isinstance(v, float):
98
107
  return format_number(v)
@@ -111,7 +120,12 @@ def format_number(f: float) -> str:
111
120
  # Negative zero canonicalizes to 0 (SPEC 2.3.1): -0.0 equals 0.0 by value.
112
121
  return "0"
113
122
  a = abs(f)
114
- if 1e-6 <= a < 1e21:
123
+ # Plain decimal only below 2^53. Every double at or above 2^53 is integer-valued,
124
+ # so a plain rendering would emit a bare-integer token: indistinguishable from an
125
+ # int64 on the wire and beyond the binary64 safe-integer range (2^53-1), so a
126
+ # JavaScript decoder rejects it under its default policy. Exponent shape keeps bare
127
+ # tokens int64 and decimal/exponent tokens doubles (SPEC 2.3.1). Ints format above.
128
+ if 1e-6 <= a < 2**53:
115
129
  # Use repr for shortest round-trippable form.
116
130
  s = repr(f)
117
131
  # If repr chose scientific notation, convert to plain decimal.
@@ -165,12 +179,20 @@ def parse_scalar(s: str, tabular_context: bool = False) -> Any:
165
179
  if s == "false":
166
180
  return False
167
181
  if _JSON_NUMBER_RE.match(s):
182
+ # Token shape follows domain (SPEC 2.3.2): a bare-integer literal (no
183
+ # fraction, no exponent) is an int64-domain integer parsed exactly, not
184
+ # routed through float(); a decimal or exponent literal is a double.
185
+ if "." not in s and "e" not in s and "E" not in s:
186
+ n = int(s)
187
+ if n < -(2**63) or n > 2**63 - 1:
188
+ raise ValueError(
189
+ f"out_of_range: integer {s} is outside the canonical int64 "
190
+ "domain [-9223372036854775808, 9223372036854775807]; "
191
+ "model larger values as strings (SPEC 2.3.2)"
192
+ )
193
+ return n
168
194
  try:
169
- f = float(s)
170
- if "." not in s and "e" not in s and "E" not in s:
171
- if abs(f) <= 2**53:
172
- return int(f)
173
- return f
195
+ return float(s)
174
196
  except ValueError:
175
197
  pass
176
198
  return s
@@ -183,6 +183,23 @@ def test_conformance(rel_path, data):
183
183
  with pytest.raises((ValueError, Exception)):
184
184
  decode_generic(data["input"])
185
185
 
186
+ elif op == "roundtrip-wire":
187
+ # Input and expected are wire strings: decode then re-encode and require the
188
+ # result to equal the input wire. The value never becomes a host number, so a
189
+ # value that a JSON parser would float (an integer beyond 2^53) is pinned here.
190
+ decoded = decode_generic(data["input"])
191
+ reencoded = encode_generic(decoded)
192
+ assert reencoded == data["expected"], (
193
+ f"wire idempotence mismatch:\n got: {reencoded!r}\n exp: {data['expected']!r}"
194
+ )
195
+
196
+ elif op == "encode-error":
197
+ # Input is a JSON value (encode-side) out of the numeric domain; encoding it
198
+ # must raise. In Python the JSON parser preserves big integers exactly, so the
199
+ # encoder is the domain-enforcement site.
200
+ with pytest.raises((ValueError, Exception)):
201
+ encode_generic(data["input"])
202
+
186
203
  elif op == "generic-pack-root":
187
204
  from gcf.generic_delta import GenericSet, generic_pack_root
188
205
 
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
File without changes