gcf-python 2.5.0__tar.gz → 2.5.1__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.
- {gcf_python-2.5.0 → gcf_python-2.5.1}/CHANGELOG.md +4 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/PKG-INFO +1 -1
- {gcf_python-2.5.0 → gcf_python-2.5.1}/pyproject.toml +1 -1
- {gcf_python-2.5.0 → gcf_python-2.5.1}/src/gcf/decode.py +26 -1
- {gcf_python-2.5.0 → gcf_python-2.5.1}/src/gcf/decode_generic.py +8 -1
- {gcf_python-2.5.0 → gcf_python-2.5.1}/src/gcf/generic_delta.py +27 -13
- {gcf_python-2.5.0 → gcf_python-2.5.1}/.github/FUNDING.yml +0 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/.github/workflows/ci.yml +0 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/.github/workflows/publish.yml +0 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/.gitignore +0 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/LICENSE +0 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/README.md +0 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/assets/divider-wave-2.png +0 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/assets/divider.png +0 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/assets/gcf-hero-wire-delta.png +0 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/assets/gcf-python-diagram.png +0 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/src/gcf/__init__.py +0 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/src/gcf/__main__.py +0 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/src/gcf/cli.py +0 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/src/gcf/constants.py +0 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/src/gcf/delta.py +0 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/src/gcf/encode.py +0 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/src/gcf/generic.py +0 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/src/gcf/keyed_map.py +0 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/src/gcf/packroot.py +0 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/src/gcf/scalar.py +0 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/src/gcf/session.py +0 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/src/gcf/stream.py +0 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/src/gcf/stream_generic.py +0 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/src/gcf/types.py +0 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/tests/__init__.py +0 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/tests/test_conformance_v2.py +0 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/tests/test_decode.py +0 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/tests/test_delta.py +0 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/tests/test_encode.py +0 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/tests/test_generic.py +0 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/tests/test_generic_delta.py +0 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/tests/test_generic_delta_fuzz.py +0 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/tests/test_generic_delta_session.py +0 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/tests/test_keyed_map_fuzz.py +0 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/tests/test_roundtrip.py +0 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/tests/test_roundtrip_v2.py +0 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/tests/test_session.py +0 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/tests/test_stream.py +0 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/tests/test_stream_fielddecl.py +0 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/tests/test_stream_generic.py +0 -0
- {gcf_python-2.5.0 → gcf_python-2.5.1}/uv.lock +0 -0
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v2.5.1 (2026-08-07)
|
|
4
|
+
|
|
5
|
+
- 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.
|
|
6
|
+
|
|
3
7
|
## v2.5.0 (2026-08-07)
|
|
4
8
|
|
|
5
9
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gcf-python
|
|
3
|
-
Version: 2.5.
|
|
3
|
+
Version: 2.5.1
|
|
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.
|
|
7
|
+
version = "2.5.1"
|
|
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,
|
|
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
|
-
|
|
303
|
-
|
|
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
|
|
310
|
-
if i >= len(lines):
|
|
311
|
-
raise ValueError("
|
|
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
|
-
|
|
339
|
-
|
|
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
|
|
349
|
-
if i >= len(lines):
|
|
350
|
-
raise ValueError(f"
|
|
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
|
|
359
|
-
if i >= len(lines):
|
|
360
|
-
raise ValueError("
|
|
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
|
|
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
|
|
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
|
|
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
|