gcf-python 2.2.2__tar.gz → 2.4.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 (44) hide show
  1. {gcf_python-2.2.2 → gcf_python-2.4.0}/.github/workflows/ci.yml +9 -1
  2. gcf_python-2.4.0/CHANGELOG.md +184 -0
  3. gcf_python-2.2.2/README.md → gcf_python-2.4.0/PKG-INFO +77 -4
  4. gcf_python-2.2.2/PKG-INFO → gcf_python-2.4.0/README.md +51 -28
  5. {gcf_python-2.2.2 → gcf_python-2.4.0}/pyproject.toml +5 -5
  6. {gcf_python-2.2.2 → gcf_python-2.4.0}/src/gcf/__init__.py +38 -2
  7. {gcf_python-2.2.2 → gcf_python-2.4.0}/src/gcf/decode_generic.py +21 -1
  8. gcf_python-2.4.0/src/gcf/delta.py +237 -0
  9. {gcf_python-2.2.2 → gcf_python-2.4.0}/src/gcf/encode.py +36 -14
  10. gcf_python-2.4.0/src/gcf/generic_delta.py +487 -0
  11. gcf_python-2.4.0/src/gcf/packroot.py +53 -0
  12. {gcf_python-2.2.2 → gcf_python-2.4.0}/src/gcf/session.py +26 -16
  13. {gcf_python-2.2.2 → gcf_python-2.4.0}/src/gcf/stream.py +22 -13
  14. gcf_python-2.4.0/tests/test_conformance_v2.py +422 -0
  15. {gcf_python-2.2.2 → gcf_python-2.4.0}/tests/test_delta.py +5 -5
  16. {gcf_python-2.2.2 → gcf_python-2.4.0}/tests/test_encode.py +3 -2
  17. gcf_python-2.4.0/tests/test_generic_delta.py +141 -0
  18. gcf_python-2.4.0/tests/test_generic_delta_fuzz.py +60 -0
  19. gcf_python-2.4.0/tests/test_generic_delta_session.py +187 -0
  20. {gcf_python-2.2.2 → gcf_python-2.4.0}/tests/test_roundtrip.py +5 -5
  21. {gcf_python-2.2.2 → gcf_python-2.4.0}/tests/test_session.py +3 -2
  22. gcf_python-2.2.2/CHANGELOG.md +0 -138
  23. gcf_python-2.2.2/src/gcf/delta.py +0 -54
  24. gcf_python-2.2.2/tests/test_conformance_v2.py +0 -117
  25. {gcf_python-2.2.2 → gcf_python-2.4.0}/.github/FUNDING.yml +0 -0
  26. {gcf_python-2.2.2 → gcf_python-2.4.0}/.github/workflows/publish.yml +0 -0
  27. {gcf_python-2.2.2 → gcf_python-2.4.0}/.gitignore +0 -0
  28. {gcf_python-2.2.2 → gcf_python-2.4.0}/LICENSE +0 -0
  29. {gcf_python-2.2.2 → gcf_python-2.4.0}/assets/gcf-python-diagram.png +0 -0
  30. {gcf_python-2.2.2 → gcf_python-2.4.0}/src/gcf/__main__.py +0 -0
  31. {gcf_python-2.2.2 → gcf_python-2.4.0}/src/gcf/cli.py +0 -0
  32. {gcf_python-2.2.2 → gcf_python-2.4.0}/src/gcf/constants.py +0 -0
  33. {gcf_python-2.2.2 → gcf_python-2.4.0}/src/gcf/decode.py +0 -0
  34. {gcf_python-2.2.2 → gcf_python-2.4.0}/src/gcf/generic.py +0 -0
  35. {gcf_python-2.2.2 → gcf_python-2.4.0}/src/gcf/scalar.py +0 -0
  36. {gcf_python-2.2.2 → gcf_python-2.4.0}/src/gcf/stream_generic.py +0 -0
  37. {gcf_python-2.2.2 → gcf_python-2.4.0}/src/gcf/types.py +0 -0
  38. {gcf_python-2.2.2 → gcf_python-2.4.0}/tests/__init__.py +0 -0
  39. {gcf_python-2.2.2 → gcf_python-2.4.0}/tests/test_decode.py +0 -0
  40. {gcf_python-2.2.2 → gcf_python-2.4.0}/tests/test_generic.py +0 -0
  41. {gcf_python-2.2.2 → gcf_python-2.4.0}/tests/test_roundtrip_v2.py +0 -0
  42. {gcf_python-2.2.2 → gcf_python-2.4.0}/tests/test_stream.py +0 -0
  43. {gcf_python-2.2.2 → gcf_python-2.4.0}/tests/test_stream_generic.py +0 -0
  44. {gcf_python-2.2.2 → gcf_python-2.4.0}/uv.lock +0 -0
@@ -13,7 +13,11 @@ jobs:
13
13
  matrix:
14
14
  python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
15
15
  steps:
16
+ # Check out gcf-python and the shared-fixtures gcf repo as SIBLINGS so the
17
+ # conformance runner's ../gcf/tests/conformance path resolves on CI.
16
18
  - uses: actions/checkout@v4
19
+ with:
20
+ path: gcf-python
17
21
  - uses: actions/checkout@v4
18
22
  with:
19
23
  repository: blackwell-systems/gcf
@@ -22,9 +26,13 @@ jobs:
22
26
  with:
23
27
  python-version: ${{ matrix.python-version }}
24
28
  - run: pip install -e ".[dev]" || pip install -e . && pip install pytest
29
+ working-directory: gcf-python
25
30
  - name: Unit tests
26
31
  run: pytest tests/ -v --ignore=tests/test_conformance_v2.py --ignore=tests/test_roundtrip_v2.py
27
- - name: Conformance tests (133 fixtures)
32
+ working-directory: gcf-python
33
+ - name: Conformance tests (shared fixtures)
28
34
  run: pytest tests/test_conformance_v2.py -v
35
+ working-directory: gcf-python
29
36
  - name: Property-based round-trip (100K random + 100K adversarial)
30
37
  run: pytest tests/test_roundtrip_v2.py -v
38
+ working-directory: gcf-python
@@ -0,0 +1,184 @@
1
+ # Changelog
2
+
3
+ ## v2.4.0 (2026-07-12)
4
+
5
+ ### Fixes
6
+
7
+ - The conformance runner now hard-fails on any unhandled operation (instead of silently skipping it) and exercises session, delta, roundtrip, and pack-root fixtures end to end; the graph delta wire decode and verify path is now covered, so no operations remain allow-listed.
8
+ - Implemented the graph delta wire decoder and verifier (`decode_delta` / `verify_delta`): parse a `GCF profile=graph delta=true` wire back into removed/added symbols and edge changes, apply them atomically to a base snapshot, recompute `pack_root`, and reject a wrong `new_root` with `root_mismatch` (SPEC 10.4). The `## added` encoder now emits the trailing `distance` field (SPEC 3.4.1, Section 10.1). The shared `graph-delta` fixtures now run end to end: 001 (encode, gains the trailing distance), 002 (verified apply), 003 (`root_mismatch` rejection).
9
+ - **Session encoding correctness fix.** `encode_with_session` assigned per-response local IDs instead of stable session-global IDs, so the cross-call dedup references (`@N # previously transmitted`) pointed at the wrong symbols, and the header emitted zero-valued `budget`/`tokens`/`edges`. Both are fixed to match the reference; graph session output is now byte-identical across all six SDKs. This had gone undetected because the conformance runner skipped the shared graph-session fixtures (now wired).
10
+ - Added the graph-profile PackRoot (`pack_root(symbols, edges)`, gcf-pack-root-v1, SPEC 10.2): the content-addressed sha256 over canonical, independently-sorted symbol/edge records, byte-identical to gcf-go/rust/typescript/swift/kotlin. The conformance runner now exercises the shared `graph-pack-root` fixtures, which it had been skipping (so this primitive was previously unimplemented and untested).
11
+ - Buffered graph encoder now matches the reference byte-for-byte: symbols are ordered by distance then descending score with local IDs assigned in output order, and the header omits `budget`/`tokens`/`edges` when zero (previously symbols kept input order and zero-valued header fields were always emitted). The conformance runner now exercises the shared `graph-encode` fixtures (001-003), which it had been skipping - which is how this divergence went uncaught.
12
+ - Buffered graph encoder: order edges by source ID, then target ID, then edge type (SPEC 16.1), instead of emitting them in input order. Decode-invariant (edges are a set) and does not affect `pack_root` (which sorts edge records independently), so no content addresses change. Pinned by shared fixture `graph-encode/003`. Streaming edges remain in producer-arrival order.
13
+ - Decoder: reject an orphan `.field` attachment (a `.field` whose name is neither a `^`-marked column of its row nor a `>`-containing field name, SPEC 7.4.6.1.4) instead of silently absorbing it as an undeclared extra field. Such a stray attachment previously decoded to a record no encoder produces, silently injecting a field onto the last-parsed row (a lossless round-trip hole); now rejected per SPEC 16.5 (`orphan_attachment`).
14
+ - Decoder: reject an orphan positional inline body (a pipe-delimited line with no eligible `^{}` attachment-marker cell) instead of silently dropping it. The object-body parser previously skipped any unrecognized line, so a stray positional body (e.g. a second `Bob|b@t.com` after a row's one inline cell was filled) vanished with no error (silent data loss); now rejected per SPEC 16.5 (`orphan_inline_attachment`).
15
+ - Graph streaming trailer: the edge count is now always the last `counts` entry, even when the stream has no edges (positional `counts=2,1,0`; labeled `counts=…,edges:0`). A zero-edge stream previously dropped it, violating the SPEC §8.4 / §8.4.1 rule that the edge count is always present and last (the invariant that keeps the positional form unambiguous). The graph trailer is decoder-ignored, so this changes producer output only.
16
+
17
+ ### Streaming: opt-in labeled trailer counts (SPEC §8.4.1)
18
+
19
+ - New `labeled_trailer_counts` keyword on `StreamEncoder`. When set, the `##! summary` graph streaming trailer emits `counts=` in the labeled form `label:count` per group (e.g. `counts=targets:2,related:1,edges:3`) instead of the default positional values-only form (`counts=2,1,3`). Default false is byte-identical to prior output.
20
+ - Opt-in and non-breaking: a producer-side comprehension aid for known weak consumers. The trailer counts remain informational (decoder-ignored) in both forms; neither changes the decoded payload. Mirrors the `gcf-go` reference.
21
+
22
+ ### Conformance and docs
23
+
24
+ - Streaming graph trailer now emits `distance_N` group counts in pure group-header emission order (dropping a fixed `targets,related,extended` prefix), matching the other SDKs and deterministic per SPEC 16.1. Byte-identical for contract-conformant (ascending-distance) input; pinned by shared fixtures `streaming-v2/010`–`011`.
25
+ - The conformance runner now executes the `graph-stream-encode` fixtures (streaming-encode parity, previously decode-only): fixture 004 (positional trailer) and 005 (labeled trailer).
26
+ - README: corrected the streaming example trailer from the defunct `## _summary … sections=` to the real `##! summary … counts=`; README now leads with the project diagram.
27
+ - Added a generic-delta fuzz test (decoder never crashes; string round-trip).
28
+
29
+ ## v2.3.0 (2026-07-12)
30
+
31
+ ### Generic-profile delta encoding (SPEC §10a)
32
+
33
+ - Full producer + consumer implementation of generic-profile delta, byte-for-byte interoperable with `gcf-go`:
34
+ - `GenericSet` (keyed record set), `GenericDeltaPayload`
35
+ - `generic_pack_root` (`gcf-pack-root-v1`, generic profile) with a purpose-built cell canonicalization decoupled from the wire cell encoder: collision-free (null/bool/number bare, strings always quoted) and record-safe. Fields and records sort by UTF-8 byte order to match Go's `sort.Strings`.
36
+ - `diff_generic_sets` (the blessed producer path; centralizes the keyed-diff invariants), `encode_generic_full`, `encode_generic_delta`
37
+ - `decode_generic_full`, `decode_generic_delta` (consumer wire parsing)
38
+ - `verify_generic_delta` (atomic apply + `new_root` verification)
39
+ - Re-anchor session helper (SPEC §10a.8): `GenericDeltaSession` (`current_full`, `next`) with `ReanchorPolicy` / `fixed_n(n)` / `size_guard()` cadence policies and `DEFAULT_REANCHOR_N = 15`. Producer-side sugar over the primitives; introduces no new wire syntax (every emission is exactly `encode_generic_full` / `encode_generic_delta` output). Re-anchor cadence is byte-for-byte identical to `gcf-go` (size guard uses UTF-8 byte length to match Go's `len(string)`), verified by the shared `generic-delta-session` conformance fixtures.
40
+ - Delta is opt-in and bilateral; the existing `encode_generic` path is unchanged (backward compatible).
41
+
42
+ ### Tests
43
+
44
+ - Unit suite mirroring `gcf-go`: self-proving round-trip (diff -> encode -> apply -> recomputed root), determinism / row-order invariance, no-type-collision canonicalization, every invariant/error path, full-payload wire round-trip, the complete server -> wire -> consumer end-to-end loop, and malformed-wire-fails-closed.
45
+ - Conformance runner support for `generic-pack-root`, `generic-delta`, `generic-delta-verify`, `generic-delta-decode` (12 shared fixtures); verified to produce identical pack roots and delta wire to `gcf-go`.
46
+ - Session helper suite (`test_generic_delta_session.py`) mirroring `gcf-go`: FixedN cadence pattern, size-guard triggering, schema-change forced full, FixedN(15)-over-30-turns count, and the load-bearing consumer-stays-in-sync check under both policies. Conformance runner support for `generic-delta-session` (3 shared fixtures: fixed-N, size-guard, schema-change).
47
+ - Generic-delta fuzz (`test_generic_delta_fuzz.py`), mirroring `gcf-go`: the decoder never crashes on arbitrary/mutated input, and arbitrary UTF-8 string cells (including multi-byte and control characters) survive the full-wire round-trip with the pack root preserved.
48
+
49
+ ## v2.2.2 (2026-07-10)
50
+
51
+ ### Fixes
52
+
53
+ - **Losslessness (nested null):** a nested object that is null at an intermediate level (e.g. `{"meta": {"owner": None}}`) is no longer flattened. Previously its leaves encoded as absent (`~`) and unflattened to a missing key, silently dropping the null. Such fields now fall back to the attachment mechanism; a top-level `None` still flattens losslessly (emits `-`, reconstructs via the all-null rule). Enforced by the shared conformance fixtures `flatten/017`–`019`. Prototype pollution does not affect Python (dicts have no mutable prototype).
54
+
55
+ ### Tests
56
+
57
+ - `test_flatten_roundtrip`: aligned arrays whose shared fields are fixed-shape nested objects with a field or an intermediate nested level sometimes null/absent — the shape the prior scalar-only generator never produced, leaving the flatten/unflatten path unexercised. Verified to fail on the pre-fix encoder and pass on the fix.
58
+
59
+ ## v2.2.1 (2026-06-23)
60
+
61
+ ### Flatten Opt-Out
62
+
63
+ - Added `GenericOptions` dataclass with `no_flatten` field to disable nested object flattening
64
+ - `encode_generic(data, GenericOptions(no_flatten=True))` produces attachment syntax instead of path columns
65
+ - Backward compatible: `encode_generic(data)` behavior unchanged (flatten on by default)
66
+ - Fixed: field names containing `>` no longer appear as tabular columns (spec rule 7.4.6.1.4)
67
+ - Fixed: field names containing `>` no longer eligible for flattening analysis
68
+ - Fixed: decoder no longer treats literal `>` in key names as a path separator
69
+ - Fixed: decoder accepts orphan attachments (fields excluded from column list)
70
+ - 12 targeted edge case tests for `>` in field names
71
+
72
+ ## v2.2.0 (2026-06-22)
73
+
74
+ ### Spec v3.2: Nested Object Flattening
75
+
76
+ - Encoder automatically flattens fixed-shape nested objects into `>` path column names (e.g., `"customer>name"` instead of `^` + `.customer {}` attachment)
77
+ - Decoder reconstructs nested objects from `>` path columns
78
+ - 20-48% fewer tokens on deeply nested API data (Jira, Stripe, K8s, calendar events)
79
+ - 100% comprehension on every frontier model (validated across 9 models, 7 providers)
80
+ - Zero regression on lossless round-trips (230 tests, conformance + property-based)
81
+ - Falls back to attachment mechanism for: variable-length arrays, objects with different keys across rows, objects with `>` in key names, empty nested objects
82
+
83
+ ## v2.1.0 (2026-06-14)
84
+
85
+ ### Spec v3.1
86
+
87
+ - `tool` field in graph profile header is now optional (SHOULD be present for MCP, not required)
88
+
89
+ ### Bug Fixes
90
+
91
+ - Quote strings containing commas (conformance: `inline-schema/006_inline_with_quoted_values`)
92
+ - Decode v2-format indented attachments in tabular rows (conformance: `decode/002_attachment`)
93
+ - Reject duplicate attachments on the same row (conformance: `errors-v2/027_duplicate_attachment`)
94
+
95
+ ## v2.0.0 (2026-06-12)
96
+
97
+ ### Breaking Changes
98
+
99
+ - `encode_generic` now produces inline schema format (not backwards compatible with v1.x decoders)
100
+ - Attachment lines no longer indented (same depth as parent row)
101
+ - Inline object fields use positional encoding without field-name prefix
102
+
103
+ ### New Features
104
+
105
+ - Inline object schema: objects with 3+ scalar fields encoded positionally with `^{fields}` header
106
+ - Shared array schemas: identical nested arrays omit `{fields}` after first row
107
+ - 472M+ fuzz iterations across all 6 implementations, zero failures
108
+
109
+ ### Bug Fixes
110
+
111
+ - Quote strings starting with `.` (dot prefix)
112
+ - Quote C1 control characters (U+0080-U+009F)
113
+ - Quote Unicode whitespace (NBSP, hair space, etc.)
114
+
115
+ ## v1.0.1 (2026-06-10)
116
+
117
+ - CLI: `encode-generic` and `decode-generic` subcommands for generic profile
118
+ - CLI now supports both graph and generic profiles
119
+ - `python -m gcf` entry point
120
+
121
+ ## v1.0.0 (2026-06-10)
122
+
123
+ SPEC v2.0 implementation. 126/133 conformance fixtures passing (7 skipped: session, delta, binary UTF-8, negative zero, graph encode). 40M property-based round-trips with zero failures.
124
+
125
+ ### Breaking changes from v0.5.0
126
+
127
+ - `encode_generic` emits `GCF profile=generic` header
128
+ - `decode_generic` requires `GCF profile=` header
129
+ - Strings colliding with typed literals are quoted
130
+ - Full JSON string escaping and number grammar
131
+ - `-` for null, `~` for absent, `^` for nested attachments
132
+ - `##! summary` trailer replaces `## _summary`
133
+ - Graph encoder emits `profile=graph`
134
+
135
+ ### New
136
+
137
+ - `scalar.py`: common scalar grammar (quoting, escaping, parsing, number formatting)
138
+ - Conformance test runner (133 fixtures)
139
+ - Property-based round-trip tests (40M verified, configurable via `GCF_ITERATIONS`)
140
+
141
+ ## v0.5.0 (2026-06-06)
142
+
143
+ - `GenericStreamEncoder`: zero-buffering tabular streaming encode (begin_array/write_row/end_array/write_kv/write_section/write_inline_array)
144
+ - `decode_generic`: decode any GCF text (tabular or graph) back to Python objects
145
+ - `StreamEncoder`: zero-buffering streaming encode (added in v0.4.0)
146
+
147
+ ## v0.3.0 (2026-06-05)
148
+
149
+ - `encode_generic`: primitive arrays inlined as `name[N]: val1,val2,val3`
150
+
151
+ ## v0.2.0 (2026-06-05)
152
+
153
+ - **Breaking**: `encode()` now emits `edges=N` in header line
154
+ - **Breaking**: `encode()` now emits `## edges [N]` section header (was `## edges`)
155
+ - `decode()` updated to parse `## edges [N]` format (strips bracket suffix)
156
+ - Session encoder updated to emit new edge count format
157
+
158
+ ## v0.1.3 (2026-06-04)
159
+
160
+ - Docs: update README for PyPI discoverability (gcformat.com, proxy, vs-toon links)
161
+ - Fix: decoder rejects headers missing required `tool` field (conformance)
162
+ - Fix: escape newlines as `\n` in quoted strings in `encode_generic`
163
+
164
+ ## v0.1.2 (2026-06-04)
165
+
166
+ - Fix: escape `"` inside quoted strings in `encode_generic`
167
+ - Fix: quote empty strings as `""` per spec
168
+
169
+ ## v0.1.1 (2026-06-03)
170
+
171
+ - `encode_generic`: encode arbitrary Python values into GCF tabular format
172
+ - Tabular encoding: positional rows with pipe separators, section headers, nested field support
173
+ - Uniform array detection with 70% key overlap threshold
174
+
175
+ ## v0.1.0 (2026-06-03)
176
+
177
+ - Initial release
178
+ - `encode` / `decode`: full GCF round-trip
179
+ - `encode_with_session`: session deduplication (92.7% savings by 5th call)
180
+ - `encode_delta`: delta encoding for re-queries (81.2% savings)
181
+ - Thread-safe `Session` class
182
+ - 16 kind abbreviations
183
+ - CLI: `gcf encode`, `gcf decode`, `gcf stats`
184
+ - Type hints, Python 3.9+, zero runtime dependencies
@@ -1,3 +1,32 @@
1
+ Metadata-Version: 2.4
2
+ Name: gcf-python
3
+ Version: 2.4.0
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
+ Project-URL: Homepage, https://github.com/blackwell-systems/gcf-python
6
+ Project-URL: Documentation, https://gcformat.com/
7
+ Project-URL: Specification, https://github.com/blackwell-systems/gcf
8
+ Author: Blackwell Systems
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: agents,delta,gcf,graph,llm,mcp,multi-turn,token-efficient,wire-format
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Software Development :: Libraries
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.9
24
+ Description-Content-Type: text/markdown
25
+
26
+ <p align="center">
27
+ <img src="assets/gcf-python-diagram.png" alt="gcf-python" width="100%">
28
+ </p>
29
+
1
30
  <p align="center">
2
31
  <a href="https://github.com/blackwell-systems"><img src="https://raw.githubusercontent.com/blackwell-systems/blackwell-docs-theme/main/badge-trademark.svg" alt="Blackwell Systems"></a>
3
32
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
@@ -94,7 +123,7 @@ enc = StreamEncoder(sys.stdout, "context_for_task", token_budget=5000)
94
123
  enc.write_symbol(Symbol(qualified_name="pkg.Auth", kind="function", score=0.95, provenance="lsp", distance=0))
95
124
  enc.write_symbol(Symbol(qualified_name="pkg.Server", kind="function", score=0.60, provenance="lsp", distance=1))
96
125
  enc.write_edge(Edge(source="pkg.Server", target="pkg.Auth", edge_type="calls"))
97
- enc.close() # emits ## _summary trailer
126
+ enc.close() # emits ##! summary trailer
98
127
  ```
99
128
 
100
129
  Output:
@@ -106,7 +135,7 @@ GCF tool=context_for_task budget=5000
106
135
  @1 fn pkg.Server 0.60 lsp
107
136
  ## edges [?]
108
137
  @0<@1 calls
109
- ## _summary symbols=2 edges=1 sections=targets:1,related:1,edges:1
138
+ ##! summary symbols=2 edges=1 counts=1,1,1
110
139
  ```
111
140
 
112
141
  The writer is any object with a `write(s: str)` method. Thread-safe. Standard `decode()` handles streaming output with no changes.
@@ -157,6 +186,44 @@ Output:
157
186
 
158
187
  Works on dicts, lists, and primitives. Lists of uniform dicts get tabular rows. Nested dicts use `## key` section headers.
159
188
 
189
+ ## Generic-Profile Delta (multi-turn)
190
+
191
+ In an agent loop the same keyed table gets re-queried turn after turn. Instead of re-sending the whole table each time, send only the changed rows (SPEC §10a):
192
+
193
+ ```python
194
+ from gcf import GenericSet, diff_generic_sets, encode_generic_delta, verify_generic_delta
195
+
196
+ base = GenericSet(key="id", fields=["id", "status"], rows=[
197
+ {"id": 1001, "status": "pending"},
198
+ {"id": 1002, "status": "shipped"},
199
+ ])
200
+ nxt = GenericSet(key="id", fields=["id", "status"], rows=[
201
+ {"id": 1001, "status": "shipped"}, # changed
202
+ {"id": 1003, "status": "pending"}, # added (1002 removed)
203
+ ])
204
+
205
+ d = diff_generic_sets(base, nxt)
206
+ wire = encode_generic_delta(d) # ## added / ## changed / ## removed
207
+ held = verify_generic_delta(base, d, d.new_root) # atomic apply + new_root verification
208
+ ```
209
+
210
+ Opt-in and bilateral, keyed on content-addressed pack roots. By the 5th overlapping call, ~97% fewer tokens than re-sending JSON.
211
+
212
+ ### Re-anchor session helper
213
+
214
+ `GenericDeltaSession` manages the delta/re-anchor cadence for you: each `next()` returns either a compact delta or, on its cadence, a full re-anchor (which re-grounds the consumer), updating its held base.
215
+
216
+ ```python
217
+ from gcf import GenericDeltaSession, fixed_n, size_guard
218
+
219
+ sess = GenericDeltaSession(base, tool="orders", policy=size_guard())
220
+ wire = sess.current_full() # transmit the base once to establish it
221
+ for snapshot in stream: # each turn's current GenericSet
222
+ wire, is_full = sess.next(snapshot) # a compact delta, or a periodic full re-anchor
223
+ ```
224
+
225
+ `fixed_n(15)` re-anchors every N turns; `size_guard()` (recommended) re-anchors once the cumulative delta reaches a full payload's size. It introduces no new wire syntax and the decoder stays cadence-agnostic, so a re-anchor is just the protocol's "full" outcome on a schedule.
226
+
160
227
  ## API
161
228
 
162
229
  | Function | Description |
@@ -165,7 +232,11 @@ Works on dicts, lists, and primitives. Lists of uniform dicts get tabular rows.
165
232
  | `encode_generic(data: Any) -> str` | Encode any value to GCF tabular format |
166
233
  | `decode(input_text: str) -> Payload` | Parse GCF text back to a Payload |
167
234
  | `encode_with_session(p: Payload, s: Session) -> str` | Encode with session deduplication |
168
- | `encode_delta(d: DeltaPayload) -> str` | Encode a delta (added/removed only) |
235
+ | `encode_delta(d: DeltaPayload) -> str` | Encode a graph delta (added/removed only) |
236
+ | `diff_generic_sets(base, next) -> GenericDeltaPayload` | Diff two keyed record sets (generic profile) |
237
+ | `encode_generic_delta(d) -> str` / `decode_generic_delta(s)` | Generic-profile delta wire (§10a) |
238
+ | `verify_generic_delta(base, d, root) -> GenericSet` | Atomic apply + `new_root` verification |
239
+ | `GenericDeltaSession(base, tool, policy)` | Producer-side re-anchor cadence helper (§10a.8) |
169
240
  | `Session()` | Create a new session tracker (thread-safe) |
170
241
 
171
242
  ## Types
@@ -175,7 +246,9 @@ Works on dicts, lists, and primitives. Lists of uniform dicts get tabular rows.
175
246
  | `Payload` | Full GCF payload: tool, budget, symbols, edges, pack root |
176
247
  | `Symbol` | Graph node: qualified name, kind, score, provenance, distance |
177
248
  | `Edge` | Directed relationship: source, target, edge type |
178
- | `DeltaPayload` | Diff between two packs: added/removed symbols and edges |
249
+ | `DeltaPayload` | Diff between two graph packs: added/removed symbols and edges |
250
+ | `GenericSet` / `GenericDeltaPayload` | Keyed record set and its generic-profile diff (§10a) |
251
+ | `GenericDeltaSession` | Stateful producer that schedules delta vs full re-anchor (§10a.8) |
179
252
  | `Session` | Thread-safe tracker for multi-call deduplication |
180
253
  | `KIND_ABBREV` / `KIND_EXPAND` | Bidirectional kind abbreviation dicts |
181
254
 
@@ -1,27 +1,6 @@
1
- Metadata-Version: 2.4
2
- Name: gcf-python
3
- Version: 2.2.2
4
- Summary: The AI-native wire format for structured data. 50-92% fewer tokens than JSON. 100% comprehension on every frontier model. Zero dependencies.
5
- Project-URL: Homepage, https://github.com/blackwell-systems/gcf-python
6
- Project-URL: Documentation, https://blackwell-systems.github.io/gcf/
7
- Project-URL: Specification, https://github.com/blackwell-systems/gcf
8
- Author: Blackwell Systems
9
- License: MIT
10
- License-File: LICENSE
11
- Keywords: gcf,graph,llm,mcp,token-efficient,wire-format
12
- Classifier: Development Status :: 4 - Beta
13
- Classifier: Intended Audience :: Developers
14
- Classifier: License :: OSI Approved :: MIT License
15
- Classifier: Programming Language :: Python :: 3
16
- Classifier: Programming Language :: Python :: 3.9
17
- Classifier: Programming Language :: Python :: 3.10
18
- Classifier: Programming Language :: Python :: 3.11
19
- Classifier: Programming Language :: Python :: 3.12
20
- Classifier: Programming Language :: Python :: 3.13
21
- Classifier: Topic :: Software Development :: Libraries
22
- Classifier: Typing :: Typed
23
- Requires-Python: >=3.9
24
- Description-Content-Type: text/markdown
1
+ <p align="center">
2
+ <img src="assets/gcf-python-diagram.png" alt="gcf-python" width="100%">
3
+ </p>
25
4
 
26
5
  <p align="center">
27
6
  <a href="https://github.com/blackwell-systems"><img src="https://raw.githubusercontent.com/blackwell-systems/blackwell-docs-theme/main/badge-trademark.svg" alt="Blackwell Systems"></a>
@@ -119,7 +98,7 @@ enc = StreamEncoder(sys.stdout, "context_for_task", token_budget=5000)
119
98
  enc.write_symbol(Symbol(qualified_name="pkg.Auth", kind="function", score=0.95, provenance="lsp", distance=0))
120
99
  enc.write_symbol(Symbol(qualified_name="pkg.Server", kind="function", score=0.60, provenance="lsp", distance=1))
121
100
  enc.write_edge(Edge(source="pkg.Server", target="pkg.Auth", edge_type="calls"))
122
- enc.close() # emits ## _summary trailer
101
+ enc.close() # emits ##! summary trailer
123
102
  ```
124
103
 
125
104
  Output:
@@ -131,7 +110,7 @@ GCF tool=context_for_task budget=5000
131
110
  @1 fn pkg.Server 0.60 lsp
132
111
  ## edges [?]
133
112
  @0<@1 calls
134
- ## _summary symbols=2 edges=1 sections=targets:1,related:1,edges:1
113
+ ##! summary symbols=2 edges=1 counts=1,1,1
135
114
  ```
136
115
 
137
116
  The writer is any object with a `write(s: str)` method. Thread-safe. Standard `decode()` handles streaming output with no changes.
@@ -182,6 +161,44 @@ Output:
182
161
 
183
162
  Works on dicts, lists, and primitives. Lists of uniform dicts get tabular rows. Nested dicts use `## key` section headers.
184
163
 
164
+ ## Generic-Profile Delta (multi-turn)
165
+
166
+ In an agent loop the same keyed table gets re-queried turn after turn. Instead of re-sending the whole table each time, send only the changed rows (SPEC §10a):
167
+
168
+ ```python
169
+ from gcf import GenericSet, diff_generic_sets, encode_generic_delta, verify_generic_delta
170
+
171
+ base = GenericSet(key="id", fields=["id", "status"], rows=[
172
+ {"id": 1001, "status": "pending"},
173
+ {"id": 1002, "status": "shipped"},
174
+ ])
175
+ nxt = GenericSet(key="id", fields=["id", "status"], rows=[
176
+ {"id": 1001, "status": "shipped"}, # changed
177
+ {"id": 1003, "status": "pending"}, # added (1002 removed)
178
+ ])
179
+
180
+ d = diff_generic_sets(base, nxt)
181
+ wire = encode_generic_delta(d) # ## added / ## changed / ## removed
182
+ held = verify_generic_delta(base, d, d.new_root) # atomic apply + new_root verification
183
+ ```
184
+
185
+ Opt-in and bilateral, keyed on content-addressed pack roots. By the 5th overlapping call, ~97% fewer tokens than re-sending JSON.
186
+
187
+ ### Re-anchor session helper
188
+
189
+ `GenericDeltaSession` manages the delta/re-anchor cadence for you: each `next()` returns either a compact delta or, on its cadence, a full re-anchor (which re-grounds the consumer), updating its held base.
190
+
191
+ ```python
192
+ from gcf import GenericDeltaSession, fixed_n, size_guard
193
+
194
+ sess = GenericDeltaSession(base, tool="orders", policy=size_guard())
195
+ wire = sess.current_full() # transmit the base once to establish it
196
+ for snapshot in stream: # each turn's current GenericSet
197
+ wire, is_full = sess.next(snapshot) # a compact delta, or a periodic full re-anchor
198
+ ```
199
+
200
+ `fixed_n(15)` re-anchors every N turns; `size_guard()` (recommended) re-anchors once the cumulative delta reaches a full payload's size. It introduces no new wire syntax and the decoder stays cadence-agnostic, so a re-anchor is just the protocol's "full" outcome on a schedule.
201
+
185
202
  ## API
186
203
 
187
204
  | Function | Description |
@@ -190,7 +207,11 @@ Works on dicts, lists, and primitives. Lists of uniform dicts get tabular rows.
190
207
  | `encode_generic(data: Any) -> str` | Encode any value to GCF tabular format |
191
208
  | `decode(input_text: str) -> Payload` | Parse GCF text back to a Payload |
192
209
  | `encode_with_session(p: Payload, s: Session) -> str` | Encode with session deduplication |
193
- | `encode_delta(d: DeltaPayload) -> str` | Encode a delta (added/removed only) |
210
+ | `encode_delta(d: DeltaPayload) -> str` | Encode a graph delta (added/removed only) |
211
+ | `diff_generic_sets(base, next) -> GenericDeltaPayload` | Diff two keyed record sets (generic profile) |
212
+ | `encode_generic_delta(d) -> str` / `decode_generic_delta(s)` | Generic-profile delta wire (§10a) |
213
+ | `verify_generic_delta(base, d, root) -> GenericSet` | Atomic apply + `new_root` verification |
214
+ | `GenericDeltaSession(base, tool, policy)` | Producer-side re-anchor cadence helper (§10a.8) |
194
215
  | `Session()` | Create a new session tracker (thread-safe) |
195
216
 
196
217
  ## Types
@@ -200,7 +221,9 @@ Works on dicts, lists, and primitives. Lists of uniform dicts get tabular rows.
200
221
  | `Payload` | Full GCF payload: tool, budget, symbols, edges, pack root |
201
222
  | `Symbol` | Graph node: qualified name, kind, score, provenance, distance |
202
223
  | `Edge` | Directed relationship: source, target, edge type |
203
- | `DeltaPayload` | Diff between two packs: added/removed symbols and edges |
224
+ | `DeltaPayload` | Diff between two graph packs: added/removed symbols and edges |
225
+ | `GenericSet` / `GenericDeltaPayload` | Keyed record set and its generic-profile diff (§10a) |
226
+ | `GenericDeltaSession` | Stateful producer that schedules delta vs full re-anchor (§10a.8) |
204
227
  | `Session` | Thread-safe tracker for multi-call deduplication |
205
228
  | `KIND_ABBREV` / `KIND_EXPAND` | Bidirectional kind abbreviation dicts |
206
229
 
@@ -4,17 +4,17 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "gcf-python"
7
- version = "2.2.2"
8
- description = "The AI-native wire format for structured data. 50-92% fewer tokens than JSON. 100% comprehension on every frontier model. Zero dependencies."
7
+ version = "2.4.0"
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"}
11
11
  requires-python = ">=3.9"
12
12
  authors = [
13
13
  { name = "Blackwell Systems" },
14
14
  ]
15
- keywords = ["gcf", "llm", "mcp", "token-efficient", "graph", "wire-format"]
15
+ keywords = ["gcf", "llm", "mcp", "token-efficient", "graph", "wire-format", "delta", "multi-turn", "agents"]
16
16
  classifiers = [
17
- "Development Status :: 4 - Beta",
17
+ "Development Status :: 5 - Production/Stable",
18
18
  "Intended Audience :: Developers",
19
19
  "License :: OSI Approved :: MIT License",
20
20
  "Programming Language :: Python :: 3",
@@ -32,7 +32,7 @@ gcf = "gcf.cli:main"
32
32
 
33
33
  [project.urls]
34
34
  Homepage = "https://github.com/blackwell-systems/gcf-python"
35
- Documentation = "https://blackwell-systems.github.io/gcf/"
35
+ Documentation = "https://gcformat.com/"
36
36
  Specification = "https://github.com/blackwell-systems/gcf"
37
37
 
38
38
  [tool.hatch.build.targets.wheel]
@@ -36,9 +36,27 @@ Specification: https://github.com/blackwell-systems/gcf
36
36
 
37
37
  from .constants import KIND_ABBREV, KIND_EXPAND
38
38
  from .decode import DecodeError, decode
39
- from .delta import encode_delta
39
+ from .delta import decode_delta, encode_delta, verify_delta
40
40
  from .encode import encode
41
41
  from .generic import encode_generic, GenericOptions
42
+ from .generic_delta import (
43
+ GenericSet,
44
+ GenericDeltaPayload,
45
+ generic_pack_root,
46
+ diff_generic_sets,
47
+ encode_generic_full,
48
+ encode_generic_delta,
49
+ decode_generic_full,
50
+ decode_generic_delta,
51
+ verify_generic_delta,
52
+ GenericDeltaSession,
53
+ ReanchorPolicy,
54
+ ReanchorMode,
55
+ fixed_n,
56
+ size_guard,
57
+ DEFAULT_REANCHOR_N,
58
+ )
59
+ from .packroot import pack_root
42
60
  from .session import Session, encode_with_session
43
61
  from .decode_generic import decode_generic
44
62
  from .stream import StreamEncoder
@@ -58,12 +76,30 @@ __all__ = [
58
76
  "StreamEncoder",
59
77
  "Symbol",
60
78
  "decode",
79
+ "decode_delta",
61
80
  "decode_generic",
62
81
  "encode",
63
82
  "encode_delta",
83
+ "verify_delta",
64
84
  "encode_generic",
65
85
  "GenericOptions",
66
86
  "encode_with_session",
87
+ "GenericSet",
88
+ "GenericDeltaPayload",
89
+ "generic_pack_root",
90
+ "pack_root",
91
+ "diff_generic_sets",
92
+ "encode_generic_full",
93
+ "encode_generic_delta",
94
+ "decode_generic_full",
95
+ "decode_generic_delta",
96
+ "verify_generic_delta",
97
+ "GenericDeltaSession",
98
+ "ReanchorPolicy",
99
+ "ReanchorMode",
100
+ "fixed_n",
101
+ "size_guard",
102
+ "DEFAULT_REANCHOR_N",
67
103
  ]
68
104
 
69
- __version__ = "2.2.2"
105
+ __version__ = "2.3.0"
@@ -177,7 +177,14 @@ def _parse_object_body(
177
177
  i += 1
178
178
  continue
179
179
 
180
- i += 1
180
+ # An object-body line that is not a `## ` section, a `key=value` field, or
181
+ # an inline array is not valid content and MUST NOT be silently skipped
182
+ # (that dropped data, a lossless round-trip hole). A pipe-delimited line is
183
+ # a stray positional inline body with no eligible `^` cell (SPEC 16.5,
184
+ # orphan_inline_attachment); any other unrecognized line is likewise rejected.
185
+ if "|" in content:
186
+ raise ValueError(f"orphan_inline_attachment: {content}")
187
+ raise ValueError(f"invalid_line: unexpected content in object body: {content!r}")
181
188
  return i - start
182
189
 
183
190
 
@@ -521,6 +528,11 @@ def _parse_tabular_body(
521
528
  if row_has_id:
522
529
  inline_idx = 0
523
530
 
531
+ # Columns that carry a `^` marker cell in this row legitimately expect
532
+ # a `.field` body. Any other `.field` is an orphan (Section 16.5) unless
533
+ # its name contains `>` (the flatten-fallback attachment, Section 7.4.6.1.4).
534
+ expected_att = set(traditional_att_fields) | set(inline_att_fields)
535
+
524
536
  while i < len(lines):
525
537
  a_line = lines[i]
526
538
  a_content: str | None = None
@@ -541,6 +553,14 @@ def _parse_tabular_body(
541
553
  att_name, after_name = _parse_attachment_name(rest)
542
554
  after_name_stripped = after_name.lstrip()
543
555
 
556
+ # Orphan attachment: a `.field` with no matching `^` cell in this
557
+ # row is only legitimate for a `>`-named field (Section 7.4.6.1.4).
558
+ # Any other unmatched attachment is rejected rather than silently
559
+ # injected as an undeclared extra field, which would decode to a
560
+ # record no encoder produces (Section 16.5, lossless round-trip).
561
+ if att_name not in expected_att and ">" not in att_name:
562
+ raise ValueError(f"orphan_attachment: {att_name}")
563
+
544
564
  # Prefixed inline data.
545
565
  ifs = inline_schemas.get(att_name)
546
566
  if ifs and not after_name_stripped.startswith("{}") and not after_name_stripped.startswith("["):