Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion docs/engineering/ontology_publication_authority.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
- **Authority scope:** Canonical Vontology publication, retraction, identity
consolidation, and publication-scope change
- **Owner:** Von maintainers
- **Last reviewed:** 13 August 2026
- **Last reviewed:** 18 August 2026
- **Review trigger:** Merge, deployment, a role-lifecycle change, or a new
canonical ontology mutation entry point
- **Live authority or implementation evidence:** Initial activation completed
Expand Down Expand Up @@ -94,6 +94,16 @@ and graph-wide rename and delete execution remain fail-closed until they can
carry an equivalent complete effect plan; their read or preview paths remain
available where safe.

Legacy inline `names[]` cleanup is a dedicated governed effect rather than a
generic concept update. The authenticated client receives an opaque selector
bound to the concept, exact array position, exact entry value, and complete
array snapshot. Execution rechecks that selector under the same concept-level
lock used by canonical `hasName` mutations, requires a canonical `hasName` to
remain, compare-and-sets only the selected legacy entry, and succeeds only when
read-back proves both the removal and the unchanged canonical-name snapshot.
Natural-language values are not trimmed, case-folded, title-cased, or Unicode
normalised by the deletion boundary.

Governed creation defaults to the actor's exact user-private publication
context. Organisation publication must be requested explicitly as
`organisation_general` and requires that organisation's semantic authority.
Expand Down Expand Up @@ -173,6 +183,12 @@ tampering, and cross-audience delegation denial; alternate entry-point
enforcement; private-context non-leakage; retry/concurrency behaviour; and
receipt-backed canonical read-back of successful and partial effects.

For legacy inline-name cleanup, the bounded evidence additionally covers exact
Unicode preservation, stale and repeated selector refusal, canonical-name
retention, shared locking with canonical `hasName` writes, authenticated HTTP
actor binding, embedding invalidation, and user-visible deletion through the
same browser control used by the Concept tab.

The motivating historical changes are evidence targets, not general permission
to alter live Vontology. Applying any such change still requires a deployed
authority configuration and a separately authorised actor/effect.
Expand Down
62 changes: 62 additions & 0 deletions src/backend/server/routes/concept_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
maybe_delete_text_relation_doc_from_rag,
maybe_sync_concept_text_relations_to_rag,
)
from ...services.ontology_mutation_command_service import delete_legacy_name
from ...services.paper_recommendation_profile_vontology_service import (
load_paper_recommendation_profile,
upsert_paper_recommendation_profile,
Expand Down Expand Up @@ -84,6 +85,13 @@ def _governed_mutation_response(
"ontology_mutation_replay_projection_unavailable",
}:
return jsonify(result), 503
if error_code in {
"legacy_name_snapshot_precondition_failed",
"legacy_name_selector_precondition_failed",
"legacy_name_compare_and_set_failed",
"canonical_has_name_precondition_failed",
}:
return jsonify(result), 409
return jsonify(result), 400


Expand Down Expand Up @@ -1757,3 +1765,57 @@ def delete_concept_text_relation_route(concept_id: str, relation_id: str):
exc_info=True,
)
return jsonify(error="Failed to delete text relation"), 500


@concept_bp.route("/<string:concept_id>/legacy-names", methods=["DELETE"])
def delete_concept_legacy_name_route(concept_id: str):
"""Remove one exact legacy inline name through semantic authority."""

from ...security.access_control import (
AUTHENTICATED_SESSION_ACTOR_SOURCE,
AUTHENTICATED_SESSION_DERIVED_ACTOR_SOURCE,
get_effective_user_concept_id_with_source,
)

actor_id, actor_source = get_effective_user_concept_id_with_source()
if not actor_id or actor_source not in {
AUTHENTICATED_SESSION_ACTOR_SOURCE,
AUTHENTICATED_SESSION_DERIVED_ACTOR_SOURCE,
}:
return _governed_mutation_response(
{
"success": False,
"effect_status": "not_started",
"mutation_outcome": "not_started",
"changed": False,
"error_code": "authenticated_actor_context_required",
"error": "A trusted signed-in actor is required.",
}
)
payload = request.get_json(silent=True) or {}
selector = payload.get("legacy_name_selector")
if not isinstance(selector, dict):
return (
jsonify(
success=False,
effect_status="not_started",
mutation_outcome="not_started",
changed=False,
error_code="exact_legacy_name_selector_required",
error="legacy_name_selector must be an exact selector object.",
),
400,
)
try:
result = delete_legacy_name(
concept_id=concept_id,
legacy_name_selector=selector,
request_id=payload.get("request_id"),
)
return _governed_mutation_response(result)
except Exception:
current_app.logger.exception(
"Error deleting a legacy inline name for %s",
concept_id,
)
return jsonify(error="Failed to delete legacy name"), 500
13 changes: 12 additions & 1 deletion src/backend/services/concept_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -1224,6 +1224,7 @@ def enrich_concept_with_text_relations(
"language": item.get("lang", "en-NZ"),
"type": item.get("context", {}).get("name_type", "NL"),
"relation_id": item.get("relation_id"),
"storage_kind": "text_relation",
}
for item in names_from_relations
]
Expand All @@ -1240,7 +1241,11 @@ def enrich_concept_with_text_relations(
if isinstance(item, dict)
}
if isinstance(legacy_names, list):
for legacy_name in legacy_names:
from .ontology_mutation_command_service import (
legacy_name_selector_metadata,
)

for legacy_ordinal, legacy_name in enumerate(legacy_names):
name_text = (
legacy_name.get("name", "")
if isinstance(legacy_name, dict)
Expand Down Expand Up @@ -1268,6 +1273,12 @@ def enrich_concept_with_text_relations(
"language": language,
"type": name_type,
"relation_id": None,
"storage_kind": "legacy_inline",
"legacy_name_selector": legacy_name_selector_metadata(
concept_id=concept_id,
names=legacy_names,
ordinal=legacy_ordinal,
),
}
)
existing_name_keys.add(key)
Expand Down
Loading
Loading