Skip to content

[Draft] Concept diff engine — file re-upload as a bulk versioning event (P0-4) - #705

Closed
mvkonchits-db wants to merge 5 commits into
feat/concept-versioning-cleanfrom
feat/concept-diff-engine
Closed

mvkonchits-db wants to merge 5 commits into
feat/concept-versioning-cleanfrom
feat/concept-diff-engine

Conversation

@mvkonchits-db

Copy link
Copy Markdown
Contributor

DRAFT — not for review yet. Stacked on the concept-versioning engine (#703); must merge AFTER it. Held from review-ready until the full stack passes E2E.

What this is (P0-4)

Turns re-uploading an ontology file from a wholesale wipe-and-replace into a smart, atomic versioning event. Backend only. Composes the existing versioning primitives — no new schema, single alembic head unchanged (m4_rdf_triple_version_uq).

How it works

replace_content() routes a re-upload (a model that already has stored triples) to apply_upload_as_versioning_event(); first-ever uploads stay on the plain import path (nothing to diff).

compute_concept_diff() canonicalizes BOTH the incoming graph and the current stored graph with URDNA2015 (rdflib.compare.to_canonical_graph) BEFORE grouping by concept subject IRI, then buckets each concept:

  • unchanged (canonical triple sets identical) → nothing. This is what makes a byte-identical re-upload mint zero versions.
  • modifiedpublish_concept_version (demote + snapshot + swap = v2).
  • new → import file-native triples + mint v1.
  • removeddeprecate_concept ONLY — tombstone, never retire/hard-delete, regardless of reference count.

Stored skolemized blank nodes are de-skolemized before canonicalization so URDNA2015 re-derives stable labels; object-fingerprints capture bnode closures so owl:Restriction-style structures compare stably.

All-or-nothing atomicity

The composed primitives each commit() internally; the orchestrator neutralizes those (redirects commitflush) for the duration of the apply and issues ONE real commit at the end. Any exception restores commit, rolls back (no half-replaced file), rebuilds the served graph, and re-raises. commit is restored on every path (except + finally).

Tests — test_concept_diff_engine.py, 6 passed

  • Byte-identical bnode-heavy (owl:Restriction) re-upload → 0 versions (canonicalization proof).
  • 1 changed definition → exactly 1 v2, others untouched.
  • Added concept → v1.
  • Removed+referenced → deprecated, still resolvable, reference survives.
  • Removed+unreferenced → deprecated tombstone, no hard delete.
  • Mid-apply failure → changeset rolled back (store unchanged).
    Regression: the versioning suite (8) still green.

Not included

  • REST-route-level upload E2E (backend orchestrator is driven directly in tests; full deployed-app E2E happens with the stack).
  • The steward review/preview UX (P1) — this ships the engine, not the approval screen.

This pull request and its description were written by Isaac.

New module concept_diff.py: canonicalizes both the incoming graph and the
current stored triples with rdflib to_canonical_graph BEFORE grouping by
concept subject IRI, so blank-node-heavy re-uploads (owl:Restriction etc.)
do not report spurious changes. Returns {unchanged, modified, new, removed}
buckets, with incoming human-field values for modified/new.

Co-authored-by: Isaac
apply_upload_as_versioning_event composes the existing versioning
primitives per diff bucket: MODIFIED -> publish_concept_version (v2),
NEW -> import file-native triples + mint v1, REMOVED -> deprecate_concept
(tombstone only, never retire/hard-delete), UNCHANGED -> no-op.

Atomicity: inner primitive commits are redirected to flush for the
duration of the apply so the whole changeset is ONE Postgres transaction;
any exception restores commit and rolls back (no half-replace). The served
graph is refreshed once after commit (DB-first recovery contract).

Also adds _ensure_concept_version_v1 (lazily mints v1 for file-imported
concepts that never got a concept_version row) and
_import_new_concept_from_graph (inserts a new concept keeping its
file-native IRI + blank-node closure).

Co-authored-by: Isaac
replace_content now routes to apply_upload_as_versioning_event when the
model already has stored triples (a re-upload), diffing instead of the
blind remove_by_context + re-import. First-ever uploads (no prior context)
stay on the plain import path since there is nothing to diff.

Co-authored-by: Isaac
Restrict bucketing to subjects carrying a versionable rdf:type (skos:Concept,
owl:Class, rdfs:Class, property types). ConceptScheme / owl:Ontology header
subjects and blank nodes are excluded so they are never routed through the
concept versioning primitives (which call get_concept and would fail on
non-concept subjects).

Co-authored-by: Isaac
test_concept_diff_engine.py covers all acceptance bullets: byte-identical
bnode-heavy (owl:Restriction) re-upload mints ZERO versions (canonicalization
proof), one changed definition -> exactly one v2, added concept -> v1,
removed-but-referenced -> deprecated + resolvable + ref survives,
removed-unreferenced -> deprecated tombstone (no hard delete), mid-apply
failure -> whole changeset rolls back.

publish_concept_version / deprecate_concept gain bypass_editable_gate
(default False, all existing callers unchanged): the re-upload path is the
sanctioned write channel for file-sourced collections auto-registered as
non-editable. Exception path now also rebuilds the served graph so a partial
in-memory patch is reconciled with the reverted DB.

Co-authored-by: Isaac
@mvkonchits-db

Copy link
Copy Markdown
Contributor Author

Superseded by #734 (feat/concepts-v2). Verified the content is folded in: the distinctive modules/tests for this PR are present on the concepts-v2 branch. Closing to consolidate the concepts-v2 work into a single PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant