feat: declarative edge uuid_fields so edge ids stop drifting - #122
Merged
Conversation
Edge ids were UUIDv3 hashes of the entire emitted record, so every field was
an identity field: a corrected p_value, a bumped nlp level inside
supporting_text, or a reordered source row each minted a brand-new id, and
downstream consumers saw a new edge rather than the same edge updated.
Graph gains an optional `uuid_fields` list naming the fields that constitute
edge identity; only those feed the hash. Unset preserves the whole-record
derivation, so the feature is strictly opt-in. Narrowing the inputs removes
the accidental cross-graph uniqueness full-record hashing provided, so the
UUID domain moves onto the graph's own infores when `uuid_fields` is set,
with `uuid_domain` as the escape hatch for graphs that must share an id space.
Three correctness fixes make this sound:
- Nested values leaked key order into the hash. Only top-level keys were
sorted; nested values reached the digest via `Value::to_string`, which under
serde_json's preserve_order emits insertion order. Canonicalization now
recurses; array order is preserved because it is semantic.
- `false` was dropped along with its key while strip_nulls deliberately keeps
it, so `{subject, negated: false}` and `{subject}` derived the same id.
- Dedup keyed on full record bytes, which was only safe while the id was a
pure function of those bytes. Edges now dedup on the derived id itself, so
the output can never carry one id twice: an exact repeat collapses, and two
genuinely different edges claiming one id raise `uuid-fields-not-a-key`.
Keying on the id also drops the dedup set from a full copy of every record
(~800 bytes each) to 24 bytes per edge, and the content hash is computed
before the id is inserted so no record is cloned to strip it back out.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NuGRZ2fn3gE5zkUrv56Jqh
Nothing pinned an actual UUID value, so the derivation could change silently and no assertion would fail. Three golden vectors now fix the full-record, declared-field, and raw-parts derivations; if one moves, edge ids in every published graph moved with it. Also covers: nested key-order stability, array order still mattering, `false` staying distinguishable, declared fields ignoring undeclared edits while tracking declared ones, declared-field order not mattering, cross-domain separation, the not-a-key abort and its diagnostic, exact repeats still collapsing, key-order-only duplicates no longer shipping two lines, Graph validation of uuid_fields, and the duplicate-edge-ids study assertion. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NuGRZ2fn3gE5zkUrv56Jqh
docs/api/utils.md documented a "\t".join(values) encoding removed some releases ago (the join has been length-prefixed for collision-safety since) and claimed edge ids covered only "subject, predicate, object, qualifiers, and publication" -- which was never true, and is precisely what uuid_fields now makes achievable. Both corrected, with the canonicalization, namespacing and uniqueness rules documented alongside. The graph config reference gains a Stable edge ids section: how to pick a field set, why the set must be a key, how to read the uuid-fields-not-a-key failure, when to reach for uuid_domain, and the one-time id churn that adopting uuid_fields implies. The guidance is empirical, not aspirational: the field set this page recommends as a starting point is NOT a key for MultiomicsKG 3.0.0 (2,476 collisions in 1.27M edges, all pairs that differed only in the NLP level recorded in supporting_text after two raw strings resolved onto one CURIE), so the page says to build and let the failure name what is missing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NuGRZ2fn3gE5zkUrv56Jqh
Major: recursive canonicalization of nested values changes every existing edge id once. Every edge carries a `sources` array whose sorted key order differs from its insertion order, so the derivation moves for all of them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NuGRZ2fn3gE5zkUrv56Jqh
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NuGRZ2fn3gE5zkUrv56Jqh
This reverts commit 4a8d482. Version stays at 15.1.0 so merging this does not trip tag-version.yml and pipy.yml, which fire on a pyproject.toml version change landing on main. The 16.0.0 CHANGELOG entry stays as written and is ready for whenever the release commit is made deliberately. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NuGRZ2fn3gE5zkUrv56Jqh
SkyeAv
added a commit
that referenced
this pull request
Aug 25, 2026
Cut 16.0.0 and bump the package version in pyproject.toml, uv.lock, and CITATION.cff. Major: edge ids are canonicalized before hashing (nested object keys sorted at every depth; false hashed instead of dropped), so every existing edge id changes once — readers pinning ids must re-key against the rebuilt graph. The same PR adds opt-in uuid_fields so a graph config declares which edge fields constitute identity (930,081 of 1,265,355 ids moved under the whole-record hash on a re-analysis, none under a declared list), derives the UUID namespace from the graph infores with a uuid_domain override, aborts the build on duplicate derived ids instead of shipping them, adds a duplicate-edge-ids QC assertion, and cuts dedup memory 25x (2469 MB -> 100 MB peak, 477s -> 37s on MultiomicsKG 3.0.0) (#122). Also adds config-driven rig.source_files with indented RIG YAML output (#121). Changelog: - The 16.0.0 section shipped with #122; added the missing Added entry covering rig.source_files and the RIG YAML formatting from #121. Docs: none needed here; both PRs shipped with their own docs. Testing: - uv run pytest -q -> 1107 passed, 15 skipped (94% coverage) - uv run ruff check . && uv run ruff format --check . && uv run pyright -> clean / 0 errors - uv lock --check -> up to date - uv run mkdocs build --strict -> clean
SkyeAv
added a commit
to glusman-team/dakp
that referenced
this pull request
Aug 25, 2026
Pin tablassert[qc]>=16.0.0,<17 and adopt 16.0's Graph.uuid_fields (SkyeAv/Tablassert#122): edge ids now derive from exactly subject, predicate, object, publications, FDA_regulatory_approvals, and disease_context_qualifier, so attribute-only changes (case counts, supporting_text, sources) stop minting new edges. A declared field absent from a record contributes nothing to the hash, so the nullable disease_context_qualifier (only some contraindication edges carry one) is safe to declare graph-wide. The UUID namespace moves off the historic TABLASSERT constant onto infores:multiomics-drugapprovals — a one-time re-key of every edge id on the next build.
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.
Summary
Let a graph declare which edge fields constitute edge identity, so edge ids stop drifting on attribute-only changes — and make edge dedup key on the derived id, which guarantees uniqueness and cuts the dedup set from 2469 MB to 100 MB on a 1.27M-edge build.
Changes
What was changed
uuid_fields(opt-in).Graphgains an optionaluuid_fieldslist naming the fields that identify an edge; only those feed the derivedid. Unset preserves today's whole-record hash, so the feature changes nothing for existing configs. A list that cannot be a key — empty, repeating, containingid, or naming a field no edge emits — is rejected at config time asuuid-bad-fields.Namespacing. Narrowing the hash inputs removes the accidental cross-graph uniqueness full-record hashing provided, so the UUID domain moves onto
rig.source_info.infores_idwhenuuid_fieldsis set. New optionaluuid_domainoverrides it for graphs that must deliberately share an id space (sharded builds, a renamed KG keeping its published ids).Three correctness fixes that this design requires:
Value::to_string, which under serde_json'spreserve_orderemits insertion order — so a polars struct-field reordering silently re-minted ids graph-wide. Canonicalization now recurses; array order is preserved because it is semantic.falsewas dropped along with its key whilestrip_nullsdeliberately keeps it, so{subject, negated: false}and{subject}— two distinct records — derived the same id.stable_json_bytesnever sorted despite its name, so two logically identical records arriving with different key order derived one id, produced different bytes, and both shipped.Edges now dedup on the derived id. An exact repeat collapses; two genuinely different edges claiming one id raise
uuid-fields-not-a-keywith the id, the fields that differ, and one offending edge. Nodes are unchanged (their ids are CURIEs, not derived hashes).Also: a
duplicate-edge-idsassertion in--qcstudy output (the symmetric partner toduplicate-node-ids, which had no edge equivalent).Why this change is needed
Every field was an identity field. A corrected
p_value, a bumpedsubject_nlp_levelinsidesupporting_text, a changedsource_record_urls, or a reordered source row each minted a brand-new id — so downstream Translator consumers saw a new edge where they should have seen the same edge with updated attributes.How it affects the project
Measured on MultiomicsKG 3.0.0 (1,265,355 edges), re-analysing
p_valueandeffect_sizeon every row:uuid_fields(today)uuid_fieldsdeclaredDedup peak RSS on the same graph: 2469 MB → 100 MB (25x), and the pass went from 477s to 37s — the old path's allocation churn dominated, so that margin will be smaller on machines under less memory pressure.
Testing
Commands run and results
ruff check: passed ·ruff format --check: 78 files already formatted ·pyright: 0 errorspytest: 1105 passed, 15 skipped (94% coverage)cargo test: 89 + 10 + 14 passed, 0 failed ·cargo fmt --check: passed ·cargo clippy --all-targets -- -D warnings: cleanNote:
make checkwas run stage by stage rather than as one target — this machine's rustup toolchain ships a broken bundled linker wrapper (a dangling/nix/storepath), so every cargo invocation neededRUSTFLAGS="-C link-self-contained=-linker -C link-arg=-fuse-ld=bfd". Unrelated to this change; CI is unaffected.Unit tests added/updated
Rust: golden vectors pinning three concrete UUIDs (nothing pinned an actual value before, so the encoding could change silently); nested key-order stability; array order still mattering;
falsestaying distinguishable; declared fields ignoring undeclared edits while tracking declared ones; declared-field order not mattering; cross-domain separation; the not-a-key abort and its diagnostic; exact repeats still collapsing; key-order-only duplicates no longer shipping two lines.Python:
Graphvalidation ofuuid_fields(casing, empty, duplicate,id, unknown field),uuid_namespaceacross all four unset/set combinations,dedup_streamplumbing, and theduplicate-edge-idsstudy assertion.Integration tests added/updated — none; covered by the real-data run below.
Manual testing performed
Ran the deduper over the published 1.27M-edge MultiomicsKG 3.0.0 edges file with ids stripped back out, in four configurations. Output was 1,265,355 edges with 1,265,355 unique ids in every case.
Worth flagging: the field set the docs suggest as a starting point is not a key for this graph — 2,476 collisions (0.2%), pairs whose subject, predicate, object, publication and row were identical and differed only in the NLP level in
supporting_text, because two raw strings had resolved onto the same CURIE. The guard caught all of them. Addingsupporting_textmade it a key. The docs now say to build and let the failure name what is missing, rather than presenting a field list as an answer.Related Issues
Checklist
Breaking output change. Recursive canonicalization changes every existing edge id, once: every edge carries a
sourcesarray whose sorted key order differs from its insertion order. There is no old-id → new-id mapping; the old value was a hash of a serialization detail.Migration: readers pinning Tablassert edge ids must re-key against the rebuilt graph. Adopt
uuid_fieldsin the same rebuild and ids stop moving for attribute changes thereafter. Thefalsefix moves nothing in practice (MultiomicsKG 3.0.0 contains zerofalsevalues).Release is deliberately deferred
pyproject.tomlstays at15.1.0in this PR. A version change landing onmainis what triggerstag-version.ymlandpipy.yml, so merging this does not tag or publish anything. The## 16.0.0CHANGELOG entry is written and ready; cut the release as a separate deliberatechore(release): 16.0.0commit bumpingpyproject.tomlandCITATION.cffin lockstep.Sizing is unambiguous when you do: this is a major — the edge
idis a public contract and every one of them moves.🤖 Generated with Claude Code
https://claude.ai/code/session_01NuGRZ2fn3gE5zkUrv56Jqh