feat(api): PATCH /entities/{id} + /v1/engineering/* aliases
PATCH lets users edit an active entity's description, properties,
confidence, and source_refs without cloning — closes the duplicate-trap
half-fixed by /invalidate + /supersede. Issue D just adds the
/engineering/* query surface to the /v1 allowlist.
- engineering/service.py: update_entity supports description replace,
properties shallow merge with null-delete semantics, confidence
0..1 bounds check, source_refs dedup-append. Writes audit row
- api/routes.py: PATCH /entities/{id} with EntityPatchRequest
- main.py: engineering/* query endpoints aliased under /v1 (Issue D)
- tests/test_patch_entity.py: 12 tests (merge, null-delete, bounds,
dedup, 404, audit, v1 alias)
- DEV-LEDGER.md: session log + test_count 509 -> 521
Forbidden fields via PATCH (by design): entity_type, project, name,
status. Use supersede+create or the dedicated status endpoints.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2197,6 +2197,20 @@ def api_reject_entity(entity_id: str) -> dict:
|
||||
return {"status": "rejected", "id": entity_id}
|
||||
|
||||
|
||||
class EntityPatchRequest(BaseModel):
|
||||
"""Partial update for an existing entity.
|
||||
|
||||
``properties`` is a shallow merge: keys with ``null`` delete,
|
||||
keys with a value overwrite. ``source_refs`` is append-only
|
||||
(duplicates filtered). Omit a field to leave it unchanged.
|
||||
"""
|
||||
description: str | None = None
|
||||
properties: dict | None = None
|
||||
confidence: float | None = None
|
||||
source_refs: list[str] | None = None
|
||||
note: str = ""
|
||||
|
||||
|
||||
class EntityInvalidateRequest(BaseModel):
|
||||
reason: str = ""
|
||||
|
||||
@@ -2206,6 +2220,43 @@ class EntitySupersedeRequest(BaseModel):
|
||||
reason: str = ""
|
||||
|
||||
|
||||
@router.patch("/entities/{entity_id}")
|
||||
def api_patch_entity(entity_id: str, req: EntityPatchRequest) -> dict:
|
||||
"""Update mutable fields on an existing entity.
|
||||
|
||||
Allowed: description, properties (shallow merge, null=delete key),
|
||||
confidence (0..1), source_refs (append, dedup). Forbidden:
|
||||
entity_type, project, name, status — those require supersede+create
|
||||
or the dedicated status endpoints.
|
||||
"""
|
||||
from atocore.engineering.service import update_entity
|
||||
|
||||
try:
|
||||
updated = update_entity(
|
||||
entity_id,
|
||||
description=req.description,
|
||||
properties_patch=req.properties,
|
||||
confidence=req.confidence,
|
||||
append_source_refs=req.source_refs,
|
||||
actor="api-http",
|
||||
note=req.note,
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
if updated is None:
|
||||
raise HTTPException(status_code=404, detail=f"Entity not found: {entity_id}")
|
||||
return {
|
||||
"status": "updated",
|
||||
"id": updated.id,
|
||||
"entity_type": updated.entity_type,
|
||||
"name": updated.name,
|
||||
"description": updated.description,
|
||||
"properties": updated.properties,
|
||||
"confidence": updated.confidence,
|
||||
"source_refs": updated.source_refs,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/entities/{entity_id}/invalidate")
|
||||
def api_invalidate_entity(
|
||||
entity_id: str,
|
||||
|
||||
Reference in New Issue
Block a user