Structural and merge replacements, Hypothesis property tests - #8
Merged
Conversation
A replacement value may now be a dict or a list as well as a string.
Container values match structurally: any subtree equal to the value in
canonical JSON form - keys sorted, so ordering never matters - is
replaced whole with the existing {"$": id} marker. Matching is
outermost-wins and strictly structural: a string containing the JSON
serialization of a value is never matched, because a reference in
string context must resolve to a string.
On the way out, container references resolve to independent deep
copies, so mutating a result cannot alias the replacements mapping or
a sibling marker. A container id appearing inside $r string segments
raises UncondenseError. Round trips are structurally equal rather than
byte-identical: a matched subtree comes back with the replacement's
key order, since the original ordering is not recorded.
Canonical forms of input subtrees are memoized per call, and a
(type, length) shape check skips nodes that cannot possibly match, so
documents with no plausible matches never pay for serialization.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dict replacements now double as merge bases. A dict node that is
mostly equal to a base - a large static envelope with a few varying
fields, like API response metadata - is stored as a dict-valued
marker:
{"$": {"m": base_id, "u": {added or changed}, "d": [absent keys]}}
Uncondensing deep-copies the base, removes the "d" keys and applies
the "u" entries, themselves uncondensed recursively so markers nest
inside patches. Deletion is the explicit key list rather than the
JSON Merge Patch null sentinel, because null is a legitimate value in
real payloads. Malformed references - unknown or non-dict base, extra
fields, non-list "d", deleting a key the base lacks, non-dict "u" -
raise UncondenseError, as does a merge reference inside $r segments.
Condensing needs no similarity heuristic: for each base the patch is
computed (per-key canonical equality, so True/1/1.0 stay distinct and
unserializable values always travel in the patch) and both encodings
are measured - the merge form is emitted only when it is smaller than
writing the dict out. Unrelated bases price themselves out. Bases are
kept in mapping order with equivalent values deduplicated, so the
first ID wins, and cost ties also go to the earlier entry. An exact
match still takes the plain {"$": id} form. The dict-valued marker
stays a single-key "$" dict, so the existing $raw escaping covers
input that already looks like a merge reference.
Sizing serializes candidate encodings, so it is guarded to keep
condense tolerant of JSON-ish input, and a key-overlap early-out skips
the whole computation for the overwhelming majority of nodes that
share no keys with any base - without it, deeply nested documents
would go quadratic.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Six properties over generated documents: the round-trip contract uncondense(condense(obj, r), r) == obj under arbitrary, self-derived (subtrees and substrings of the document itself) and merge-targeted (near-copies of a base) replacements; condensed output always JSON-serializable; repeated application still lossless; and uncondense on arbitrary input either resolves or raises UncondenseError, never anything else. The strategies were tuned by mutation testing - planting bugs and checking the properties catch them. Two lessons are baked in: a confusables generator draws True/False/0/1/0.0/1.0 often, because the bool/int collision that breaks canonical-equality code is otherwise too rare to generate; and assertions compare canonical JSON alongside Python ==, because True == 1 makes that exact corruption invisible to == alone. Key generation is biased toward the marker vocabulary so escaping collisions occur constantly. HYPOTHESIS_PROFILE=thorough runs 2000 examples per property instead of 100, for pre-release checks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The opening Usage prose still told the string-only story - matching substrings, two marker shapes, uncondense restoring "the original strings" - leaving structural and merge references undocumented until their own sections much further down. The function descriptions now enumerate all three reference forms with links, and the strictness paragraph mentions malformed merge references. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three features that extend replacements beyond substring matching, built for llm's condensed response payload storage.
Three key changes:
Claude Fable 5 description
Structural replacements
A replacement value may now be a dict or a list. Container values match structurally - any subtree equal to the value in canonical JSON form is replaced whole with the existing
{"$": id}marker, so key order and serialization bytes never matter. Matching is outermost-wins and strictly structural (a string containing the JSON serialization of a value never matches). Resolution substitutes independent deep copies; round trips are structurally equal rather than byte-identical, since the original key order of a matched subtree is not recorded.Merge references
Dict replacements double as merge bases: a dict that is mostly equal to a base - a static envelope with a few varying fields, like API response metadata - is stored as a dict-valued marker:
{"$": {"m": "base_id", "u": {"added or changed": "keys"}, "d": ["absent keys"]}}Condensing needs no similarity heuristic: for each base the patch is computed and both encodings are measured, and the merge form is emitted only when it is smaller - unrelated bases price themselves out. Deletion is the explicit
dlist rather than a JSON Merge Patch-stylenullsentinel, becausenullis a legitimate payload value. The dict-valued marker is still a single-key$dict, so the existing$rawescaping already protects input that looks like a merge reference. Measured on real OpenAI Responses payloads in llm, merge references took a typical reply from 22% to 44% saved, since the ~15 static top-level envelope keys collapse to one reference.Performance was benchmarked and tuned: a key-overlap early-out skips the cost computation for nodes sharing no keys with any base (without it, deeply nested documents went quadratic - 29ms for a 36KB doc, now 0.6ms), base canonical forms are computed once per call, and input subtree canonicals are memoized. A realistic payload condenses in under 0.1ms; adding bases to a large string-replacement workload costs nothing measurable.
Property-based tests
Six Hypothesis properties: the round-trip contract under arbitrary, document-derived, and merge-targeted replacements; output always JSON-serializable; repeated application lossless; and
uncondense_jsonon arbitrary input either resolves or raisesUncondenseError, never anything else. The strategies were validated by mutation testing - three planted bugs (equality-semantics, dropped escaping, raw-instead-of-processed patches) are all caught. That process exposed and fixed two real weaknesses: a confusables generator now drawsTrue/False/0/1/0.0/1.0frequently, and assertions compare canonical JSON alongside==, becauseTrue == 1makes bool/int corruption invisible to==alone.HYPOTHESIS_PROFILE=thoroughruns 2,000 examples per property.Notes for review
u(condensed recursively on the way). Nested bases provide depth when wanted.uv run pytest: 71 passed.uv run mypy condense_json tests: clean. Black formatted. README documents both new behaviors with executable examples.🤖 Generated with Claude Code