[Feature]: Concept versioning engine (P0 core) — schema, atomic publish, safe transition, freshness - #703
Closed
mvkonchits-db wants to merge 14 commits into
Closed
[Feature]: Concept versioning engine (P0 core) — schema, atomic publish, safe transition, freshness#703mvkonchits-db wants to merge 14 commits into
mvkonchits-db wants to merge 14 commits into
Conversation
Add concept_version table (versioned unit = (iri, version); is_current hot set with a partial unique index UNIQUE(iri) WHERE is_current), a concept_version_id FK column on rdf_triples (subject-IRI ownership), and the scheme_membership m:n table. Idempotent + reversible backfill: every existing concept -> v1 is_current, triples owned by subject IRI, scheme membership from skos:inScheme. Chains onto the single live head l1_entity_domain_associations. Co-authored-by: Isaac
Build the served hot graph from CURRENT-ONLY triples so reads carry no version predicate. Add rdf_triples_current view (m2) and list_current() repo method: keep unowned scheme/collection/metadata triples, drop only history owned by a non-current concept-version. Point the single graph-build call site (_load_triples_from_db_to_graph) at list_current; ~20 serve-time self._graph read sites unchanged. Co-authored-by: Isaac
Add ConceptVersionsRepository with get_current / get_by_iri_version / list_versions / max_version / demote_current / create_version. demote_current flips the old current to history and flushes BEFORE the new current is inserted, so the partial unique index never sees two is_current rows per iri. Co-authored-by: Isaac
DB-first atomic swap in one Postgres transaction: demote current concept_version to history (flush before insert so the partial unique index never sees two current rows), insert new current (version=max+1, parent=demoted), overwrite changed SKOS fields, reassign the subject's triples to the new version, commit. Then patch the served graph; on patch failure force rebuild_graph_from_enabled and re-raise (recovery contract, named in code). Stable IRI never changes. Add POST /semantic-models/concepts/version/publish (READ_WRITE, editable-scheme gated) with PublishVersionRequest/Response per signed-off API contract §4 (no graph_refreshed field; DB-committed-but-patch-failed returns 500, not success). Add reassign_subject_to_concept_version repo helper. Co-authored-by: Isaac
…ecated' A prior concept-version replaced by a newer one is superseded (concept stays active; only that version is historical), semantically distinct from deprecated (stop using the concept) / retired (tombstoned). Add ConceptStatus.SUPERSEDED; demote_current defaults to it. Per product decision on P0-3. Co-authored-by: Isaac
Add count_for_iri to the entity_semantic_links repo (the physical UC/asset reference count) and DCT/PROV namespaces to the manager for 2B split lineage links (dct:isReplacedBy / dct:replaces / prov:wasRevisionOf). Co-authored-by: Isaac
…(P0-6) Add reference_count (entity_semantic_links rows + concept->concept broader/ narrower/related/subClassOf refs), deprecate_concept (status=deprecated, stays resolvable; 2B split writes dct:isReplacedBy old->new, prov:wasRevisionOf and dct:replaces new->old), retire_concept (gated on reference_count==0, tombstone status=retired never hard-delete; ReferenceCountError -> 409). Add the three endpoints + Pydantic models per signed-off API contract §5, READ_WRITE + editable-scheme gated, responses carry human label. Co-authored-by: Isaac
Track _graph_last_refreshed on the manager singleton, advanced on every full rebuild AND every targeted write (create/update/publish/deprecate/retire all route through _invalidate_cache, which now bumps it after the post-commit graph patch). Add graph_freshness() (last_refreshed + concept_count + separate UC sync timing reusing the existing uc_tag_sync job-run history, null if absent) and reload_graph() (forces rebuild_graph_from_enabled). Add GET /semantic-models/graph/freshness (READ_ONLY) and POST /graph/reload (READ_WRITE) + GraphFreshnessResponse per signed-off API contract §6. Audit: all new write paths (P0-3 publish, P0-6 deprecate/retire) already DB-first-then-patch the graph and call _invalidate_cache; no gap found. Co-authored-by: Isaac
P0-1 backfill only versioned pre-existing concepts; freshly created ones had no concept_version row, so a later publish computed max_version=0 -> new_version=1 and history was empty. create_concept now inserts version=1 (is_current=true, status=draft) and assigns the new triples' concept_version_id by subject IRI, in the same commit as the concept create. Co-authored-by: Isaac
Build the signed-off contract §1/§2 GET reads that were never implemented:
- GET /semantic-models/concepts/version?iri= -> ConceptVersionInfo (current
version + newest-first history, required label, replaces_iri/replaced_by_iris
lineage from the graph).
- GET /semantic-models/concepts/version/detail?iri=&version= -> concept detail
by (iri,version) key + version/is_current (cold fetch, no hot-graph touch).
Both registered ABOVE the /concepts/{concept_iri:path} catch-all so it does not
swallow /version. READ_ONLY. Logic layered into the manager + concept_versions
repo (list_versions/get_by_iri_version).
Co-authored-by: Isaac
The 6-col uq_rdf_triple (s,p,o,lang,datatype,context) BLOCKS a per-version snapshot row (same triple, different concept_version_id) — verified: 2nd insert raises UniqueViolation. Migration m4 widens it to 7 cols with UNIQUE NULLS NOT DISTINCT so snapshots coexist while NULL-owned/metadata triples stay deduped. add_triple/add_triples_bulk conflict target now includes concept_version_id; add_triple accepts concept_version_id so a snapshot row is born owned by v2. Model UniqueConstraint updated to match (postgresql_nulls_not_distinct). Co-authored-by: Isaac
Publish previously destroyed the prior version's field values: it mutated the concept's triples in place then reassigned ALL of them to v2, leaving v1 owning zero triples — the old definition was gone and P0-4 had nothing to diff. Rework publish to snapshot-per-version: demote v1 (its rows stay owned by the demoted id = frozen snapshot), create v2, COPY the concept's current triples into a new set owned by v2 (copy_triples_to_version), then apply changes to v2's set ONLY (remove_by_subject_predicate + add_triple now version-scoped via concept_version_id). v1's rows are never touched. Served graph shows current-only: _swap_concept_in_graph now re-adds via list_current_by_subject (not list_by_subject) so the frozen v1 rows don't leak. get_concept_version_detail reads the (iri,version)'s OWN triples (list_by_concept_version) so v1 returns its OLD definition text, v2 the new. Co-authored-by: Isaac
…ngle-current index; empirical uc-sync cadence - Add src/tests/integration/test_concept_versioning.py (8 tests): publish bumps version + stable IRI, snapshot preserves prior definition, prior = superseded, current-read no history leak, retire gate 409-then-tombstone, deprecate-with-successor records isReplacedBy, partial unique index rejects a second is_current row. Mirrors test_knowledge_routes.py fixtures. - concept_versions: declare the partial unique index with sqlite_where too, so metadata.create_all reproduces the single-current invariant on the SQLite test DB (was degrading to a full-unique index there). No Postgres change. - _uc_sync_timing: derive next-run estimate from observed run cadence instead of a hardcoded 4h assumption (interval is configurable); null when it can't be inferred from a single run. Co-authored-by: Isaac
This was referenced Aug 13, 2026
…bel, malformed-import negative Fills the programmatic gaps the E2E test plan flagged: - served graph is current-only: list_current + a rebuilt graph exclude the superseded (history) triple while keeping unowned metadata (read-isolation at the build layer, independent of the concept-detail read path). - Simple/Advanced contract: version-info and publish responses always carry a human label, so the Simple view never resolves a raw IRI. - negative import: malformed RDF -> 400 and the collection's concept_count is unchanged (no partial apply). No migration (tests only); single head m4 unchanged. 5/5 pass, versioning suite regression 8/8 green. Co-authored-by: Isaac
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. |
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.
What this is
The P0 correctness core of the concept-versioning engine. Version the CONCEPT (
(iri, version); the IRI is the stable UC join key). Backend-only; no UI in this PR.DRAFT — not review-ready until the companion versioning UI also passes E2E on top. Opened for early visibility + CI.
What's included (P0-1/2/3/6/8)
concept_versiontable (iri, version, is_current, status, parent_version_id, replaces_iri),concept_version_idFK onrdf_triples,scheme_membership. Partial unique indexUNIQUE(iri) WHERE is_currentmakes two-current-per-iri structurally impossible. Backfill stamps existing concepts v1/current by subject-IRI ownership.rdf_triples_currentview; reads carry NO version predicate (Lars's hard requirement). History is cold, keyed by (iri, version).POST /concepts/version/publish): single-transaction version swap, demote-then-insert so the partial index never sees two current rows; DB-first-then-graph-patch with a rebuild recovery contract. Snapshots the prior version's triples so the old definition text is preserved (answers "what was the previous definition"). Migrationm4widensrdf_triplesuniqueness to allow per-version snapshots.reference-count(entity_semantic_links + concept→concept refs) as the retire gate;deprecate(writes isReplacedBy/wasRevisionOf on a 2B split);retiregated on refs==0 → tombstone, never hard-delete. Prior versions get statussuperseded(distinct from deprecated/retired).graph/freshness+graph/reload; every write path dual-writes (DB-first-then-patch); UC-sync timing surfaced separately.Explicitly out of scope
Verification
m4_rdf_triple_version_uq); chain resolves from base.This pull request and its description were written by Isaac.