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.
- {gcf_python-2.2.2 → gcf_python-2.4.0}/.github/workflows/ci.yml +9 -1
- gcf_python-2.4.0/CHANGELOG.md +184 -0
- gcf_python-2.2.2/README.md → gcf_python-2.4.0/PKG-INFO +77 -4
- gcf_python-2.2.2/PKG-INFO → gcf_python-2.4.0/README.md +51 -28
- {gcf_python-2.2.2 → gcf_python-2.4.0}/pyproject.toml +5 -5
- {gcf_python-2.2.2 → gcf_python-2.4.0}/src/gcf/__init__.py +38 -2
- {gcf_python-2.2.2 → gcf_python-2.4.0}/src/gcf/decode_generic.py +21 -1
- gcf_python-2.4.0/src/gcf/delta.py +237 -0
- {gcf_python-2.2.2 → gcf_python-2.4.0}/src/gcf/encode.py +36 -14
- gcf_python-2.4.0/src/gcf/generic_delta.py +487 -0
- gcf_python-2.4.0/src/gcf/packroot.py +53 -0
- {gcf_python-2.2.2 → gcf_python-2.4.0}/src/gcf/session.py +26 -16
- {gcf_python-2.2.2 → gcf_python-2.4.0}/src/gcf/stream.py +22 -13
- gcf_python-2.4.0/tests/test_conformance_v2.py +422 -0
- {gcf_python-2.2.2 → gcf_python-2.4.0}/tests/test_delta.py +5 -5
- {gcf_python-2.2.2 → gcf_python-2.4.0}/tests/test_encode.py +3 -2
- gcf_python-2.4.0/tests/test_generic_delta.py +141 -0
- gcf_python-2.4.0/tests/test_generic_delta_fuzz.py +60 -0
- gcf_python-2.4.0/tests/test_generic_delta_session.py +187 -0
- {gcf_python-2.2.2 → gcf_python-2.4.0}/tests/test_roundtrip.py +5 -5
- {gcf_python-2.2.2 → gcf_python-2.4.0}/tests/test_session.py +3 -2
- gcf_python-2.2.2/CHANGELOG.md +0 -138
- gcf_python-2.2.2/src/gcf/delta.py +0 -54
- gcf_python-2.2.2/tests/test_conformance_v2.py +0 -117
- {gcf_python-2.2.2 → gcf_python-2.4.0}/.github/FUNDING.yml +0 -0
- {gcf_python-2.2.2 → gcf_python-2.4.0}/.github/workflows/publish.yml +0 -0
- {gcf_python-2.2.2 → gcf_python-2.4.0}/.gitignore +0 -0
- {gcf_python-2.2.2 → gcf_python-2.4.0}/LICENSE +0 -0
- {gcf_python-2.2.2 → gcf_python-2.4.0}/assets/gcf-python-diagram.png +0 -0
- {gcf_python-2.2.2 → gcf_python-2.4.0}/src/gcf/__main__.py +0 -0
- {gcf_python-2.2.2 → gcf_python-2.4.0}/src/gcf/cli.py +0 -0
- {gcf_python-2.2.2 → gcf_python-2.4.0}/src/gcf/constants.py +0 -0
- {gcf_python-2.2.2 → gcf_python-2.4.0}/src/gcf/decode.py +0 -0
- {gcf_python-2.2.2 → gcf_python-2.4.0}/src/gcf/generic.py +0 -0
- {gcf_python-2.2.2 → gcf_python-2.4.0}/src/gcf/scalar.py +0 -0
- {gcf_python-2.2.2 → gcf_python-2.4.0}/src/gcf/stream_generic.py +0 -0
- {gcf_python-2.2.2 → gcf_python-2.4.0}/src/gcf/types.py +0 -0
- {gcf_python-2.2.2 → gcf_python-2.4.0}/tests/__init__.py +0 -0
- {gcf_python-2.2.2 → gcf_python-2.4.0}/tests/test_decode.py +0 -0
- {gcf_python-2.2.2 → gcf_python-2.4.0}/tests/test_generic.py +0 -0
- {gcf_python-2.2.2 → gcf_python-2.4.0}/tests/test_roundtrip_v2.py +0 -0
- {gcf_python-2.2.2 → gcf_python-2.4.0}/tests/test_stream.py +0 -0
- {gcf_python-2.2.2 → gcf_python-2.4.0}/tests/test_stream_generic.py +0 -0
- {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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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 ::
|
|
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://
|
|
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.
|
|
105
|
+
__version__ = "2.3.0"
|
|
@@ -177,7 +177,14 @@ def _parse_object_body(
|
|
|
177
177
|
i += 1
|
|
178
178
|
continue
|
|
179
179
|
|
|
180
|
-
|
|
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("["):
|