fix(api): R14 — promote route translates V1-0 ValueError to 400
POST /entities/{id}/promote now wraps the promote_entity call in
try/except ValueError → HTTPException(400). Previously the new
V1-0 provenance re-check raised ValueError that the route didn't
catch, so legacy no-provenance candidates promoted via the API
surfaced as 500 instead of 400.
Matches the existing ValueError → 400 handling on POST /entities
(api_create_entity at routes.py:1490).
Test: test_api_promote_returns_400_on_legacy_no_provenance inserts
a pre-V1-0 legacy candidate directly, POSTs to the promote route,
asserts 400 with the expected detail, asserts the row stays
candidate.
Test count: 547 -> 548. Full suite green in 72.91s.
Closes R14.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1
.obsidian/app.json
vendored
Normal file
1
.obsidian/app.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
1
.obsidian/appearance.json
vendored
Normal file
1
.obsidian/appearance.json
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
33
.obsidian/core-plugins.json
vendored
Normal file
33
.obsidian/core-plugins.json
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"file-explorer": true,
|
||||||
|
"global-search": true,
|
||||||
|
"switcher": true,
|
||||||
|
"graph": true,
|
||||||
|
"backlink": true,
|
||||||
|
"canvas": true,
|
||||||
|
"outgoing-link": true,
|
||||||
|
"tag-pane": true,
|
||||||
|
"footnotes": false,
|
||||||
|
"properties": true,
|
||||||
|
"page-preview": true,
|
||||||
|
"daily-notes": true,
|
||||||
|
"templates": true,
|
||||||
|
"note-composer": true,
|
||||||
|
"command-palette": true,
|
||||||
|
"slash-command": false,
|
||||||
|
"editor-status": true,
|
||||||
|
"bookmarks": true,
|
||||||
|
"markdown-importer": false,
|
||||||
|
"zk-prefixer": false,
|
||||||
|
"random-note": false,
|
||||||
|
"outline": true,
|
||||||
|
"word-count": true,
|
||||||
|
"slides": false,
|
||||||
|
"audio-recorder": false,
|
||||||
|
"workspaces": false,
|
||||||
|
"file-recovery": true,
|
||||||
|
"publish": false,
|
||||||
|
"sync": true,
|
||||||
|
"bases": true,
|
||||||
|
"webviewer": false
|
||||||
|
}
|
||||||
190
.obsidian/workspace.json
vendored
Normal file
190
.obsidian/workspace.json
vendored
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
{
|
||||||
|
"main": {
|
||||||
|
"id": "b77ba76979ead837",
|
||||||
|
"type": "split",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "f87c2c42c078a85b",
|
||||||
|
"type": "tabs",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "c56c6d5a90db0355",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "markdown",
|
||||||
|
"state": {
|
||||||
|
"file": "docs/MASTER-BRAIN-PLAN.md",
|
||||||
|
"mode": "source",
|
||||||
|
"source": false
|
||||||
|
},
|
||||||
|
"icon": "lucide-file",
|
||||||
|
"title": "MASTER-BRAIN-PLAN"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"direction": "vertical"
|
||||||
|
},
|
||||||
|
"left": {
|
||||||
|
"id": "b855c3c36d491ee3",
|
||||||
|
"type": "split",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "7d6978e4bb8c4218",
|
||||||
|
"type": "tabs",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "f799b7c8adbccbc2",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "file-explorer",
|
||||||
|
"state": {
|
||||||
|
"sortOrder": "alphabetical",
|
||||||
|
"autoReveal": false
|
||||||
|
},
|
||||||
|
"icon": "lucide-folder-closed",
|
||||||
|
"title": "Files"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "7de38afa76edc676",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "search",
|
||||||
|
"state": {
|
||||||
|
"query": "",
|
||||||
|
"matchingCase": false,
|
||||||
|
"explainSearch": false,
|
||||||
|
"collapseAll": false,
|
||||||
|
"extraContext": false,
|
||||||
|
"sortOrder": "alphabetical"
|
||||||
|
},
|
||||||
|
"icon": "lucide-search",
|
||||||
|
"title": "Search"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "babc39197dd9c9c6",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "bookmarks",
|
||||||
|
"state": {},
|
||||||
|
"icon": "lucide-bookmark",
|
||||||
|
"title": "Bookmarks"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"direction": "horizontal",
|
||||||
|
"width": 300
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"id": "d63c3fe3c986f84d",
|
||||||
|
"type": "split",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "33e2645653a3a2d0",
|
||||||
|
"type": "tabs",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "5827678b9e27e77c",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "backlink",
|
||||||
|
"state": {
|
||||||
|
"file": "docs/MASTER-BRAIN-PLAN.md",
|
||||||
|
"collapseAll": false,
|
||||||
|
"extraContext": false,
|
||||||
|
"sortOrder": "alphabetical",
|
||||||
|
"showSearch": false,
|
||||||
|
"searchQuery": "",
|
||||||
|
"backlinkCollapsed": false,
|
||||||
|
"unlinkedCollapsed": true
|
||||||
|
},
|
||||||
|
"icon": "links-coming-in",
|
||||||
|
"title": "Backlinks for MASTER-BRAIN-PLAN"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fec1e8678fe61e1a",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "outgoing-link",
|
||||||
|
"state": {
|
||||||
|
"file": "docs/MASTER-BRAIN-PLAN.md",
|
||||||
|
"linksCollapsed": false,
|
||||||
|
"unlinkedCollapsed": true
|
||||||
|
},
|
||||||
|
"icon": "links-going-out",
|
||||||
|
"title": "Outgoing links from MASTER-BRAIN-PLAN"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0c524cb35a17b042",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "tag",
|
||||||
|
"state": {
|
||||||
|
"sortOrder": "frequency",
|
||||||
|
"useHierarchy": true,
|
||||||
|
"showSearch": false,
|
||||||
|
"searchQuery": ""
|
||||||
|
},
|
||||||
|
"icon": "lucide-tags",
|
||||||
|
"title": "Tags"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "0c7517dd072e31f4",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "all-properties",
|
||||||
|
"state": {
|
||||||
|
"sortOrder": "frequency",
|
||||||
|
"showSearch": false,
|
||||||
|
"searchQuery": ""
|
||||||
|
},
|
||||||
|
"icon": "lucide-archive",
|
||||||
|
"title": "All properties"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "f2b9b2636ff29057",
|
||||||
|
"type": "leaf",
|
||||||
|
"state": {
|
||||||
|
"type": "outline",
|
||||||
|
"state": {
|
||||||
|
"file": "docs/MASTER-BRAIN-PLAN.md",
|
||||||
|
"followCursor": false,
|
||||||
|
"showSearch": false,
|
||||||
|
"searchQuery": ""
|
||||||
|
},
|
||||||
|
"icon": "lucide-list",
|
||||||
|
"title": "Outline of MASTER-BRAIN-PLAN"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"direction": "horizontal",
|
||||||
|
"width": 300,
|
||||||
|
"collapsed": true
|
||||||
|
},
|
||||||
|
"left-ribbon": {
|
||||||
|
"hiddenItems": {
|
||||||
|
"switcher:Open quick switcher": false,
|
||||||
|
"graph:Open graph view": false,
|
||||||
|
"canvas:Create new canvas": false,
|
||||||
|
"daily-notes:Open today's daily note": false,
|
||||||
|
"templates:Insert template": false,
|
||||||
|
"command-palette:Open command palette": false,
|
||||||
|
"bases:Create new base": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"active": "c56c6d5a90db0355",
|
||||||
|
"lastOpenFiles": [
|
||||||
|
"DEV-LEDGER.md.tmp.94864.1776886173652"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -143,7 +143,7 @@ One branch `codex/extractor-eval-loop` for Day 1-5, a second `codex/retrieval-ha
|
|||||||
| R11 | Codex | P2 | src/atocore/api/routes.py:773-845 | `POST /admin/extract-batch` still accepts `mode="llm"` inside the container and returns a successful 0-candidate result instead of surfacing that host-only LLM extraction is unavailable from this runtime. That is a misleading API contract for operators. | fixed | Claude | 2026-04-12 | (pending) |
|
| R11 | Codex | P2 | src/atocore/api/routes.py:773-845 | `POST /admin/extract-batch` still accepts `mode="llm"` inside the container and returns a successful 0-candidate result instead of surfacing that host-only LLM extraction is unavailable from this runtime. That is a misleading API contract for operators. | fixed | Claude | 2026-04-12 | (pending) |
|
||||||
| R12 | Codex | P2 | scripts/batch_llm_extract_live.py:39-190 | The host-side extractor duplicates the LLM system prompt and JSON parsing logic from `src/atocore/memory/extractor_llm.py`. It works today, but this is now a prompt/parser drift risk across the container and host implementations. | fixed | Claude | 2026-04-12 | (pending) |
|
| R12 | Codex | P2 | scripts/batch_llm_extract_live.py:39-190 | The host-side extractor duplicates the LLM system prompt and JSON parsing logic from `src/atocore/memory/extractor_llm.py`. It works today, but this is now a prompt/parser drift risk across the container and host implementations. | fixed | Claude | 2026-04-12 | (pending) |
|
||||||
| R13 | Codex | P2 | DEV-LEDGER.md:12 | The new `286 passing` test-count claim is not reproducibly auditable from the current audit environments: neither Dalidou nor the clean worktree has `pytest` available. The claim may be true in Claude's dev shell, but it remains unverified in this audit. | fixed | Claude | 2026-04-12 | (pending) |
|
| R13 | Codex | P2 | DEV-LEDGER.md:12 | The new `286 passing` test-count claim is not reproducibly auditable from the current audit environments: neither Dalidou nor the clean worktree has `pytest` available. The claim may be true in Claude's dev shell, but it remains unverified in this audit. | fixed | Claude | 2026-04-12 | (pending) |
|
||||||
| R14 | Codex | P2 | src/atocore/api/routes.py (POST /entities/{id}/promote) | The HTTP `POST /entities/{id}/promote` route does not translate the new service-layer `ValueError("source_refs required: cannot promote a candidate with no provenance...")` into a 400. A legacy no-provenance candidate promoted through the API currently surfaces as a 500. Does not block V1-0 acceptance; tidy in a follow-up. | open | Claude | 2026-04-22 | |
|
| R14 | Codex | P2 | src/atocore/api/routes.py (POST /entities/{id}/promote) | The HTTP `POST /entities/{id}/promote` route does not translate the new service-layer `ValueError("source_refs required: cannot promote a candidate with no provenance...")` into a 400. A legacy no-provenance candidate promoted through the API currently surfaces as a 500. Does not block V1-0 acceptance; tidy in a follow-up. | fixed | Claude | 2026-04-22 | (pending) |
|
||||||
|
|
||||||
## Recent Decisions
|
## Recent Decisions
|
||||||
|
|
||||||
|
|||||||
@@ -2187,12 +2187,17 @@ def api_promote_entity(
|
|||||||
from atocore.engineering.service import promote_entity
|
from atocore.engineering.service import promote_entity
|
||||||
target_project = req.target_project if req is not None else None
|
target_project = req.target_project if req is not None else None
|
||||||
note = req.note if req is not None else ""
|
note = req.note if req is not None else ""
|
||||||
success = promote_entity(
|
try:
|
||||||
entity_id,
|
success = promote_entity(
|
||||||
actor="api-http",
|
entity_id,
|
||||||
note=note,
|
actor="api-http",
|
||||||
target_project=target_project,
|
note=note,
|
||||||
)
|
target_project=target_project,
|
||||||
|
)
|
||||||
|
except ValueError as e:
|
||||||
|
# V1-0 F-8 re-check raises ValueError for no-provenance candidates
|
||||||
|
# (see service.promote_entity). Surface as 400, not 500.
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
if not success:
|
if not success:
|
||||||
raise HTTPException(status_code=404, detail=f"Entity not found or not a candidate: {entity_id}")
|
raise HTTPException(status_code=404, detail=f"Entity not found or not a candidate: {entity_id}")
|
||||||
result = {"status": "promoted", "id": entity_id}
|
result = {"status": "promoted", "id": entity_id}
|
||||||
|
|||||||
@@ -160,6 +160,42 @@ def test_promote_rejects_legacy_candidate_without_provenance(tmp_data_dir):
|
|||||||
assert got.status == "candidate"
|
assert got.status == "candidate"
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_promote_returns_400_on_legacy_no_provenance(tmp_data_dir):
|
||||||
|
"""R14 (Codex, 2026-04-22): the HTTP promote route must translate
|
||||||
|
the V1-0 ValueError for no-provenance candidates into 400, not 500.
|
||||||
|
Previously the route didn't catch ValueError so legacy bad
|
||||||
|
candidates surfaced as a server error."""
|
||||||
|
init_db()
|
||||||
|
init_engineering_schema()
|
||||||
|
|
||||||
|
import uuid as _uuid
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from atocore.main import app
|
||||||
|
|
||||||
|
entity_id = str(_uuid.uuid4())
|
||||||
|
with get_connection() as conn:
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO entities (id, entity_type, name, project, "
|
||||||
|
"description, properties, status, confidence, source_refs, "
|
||||||
|
"extractor_version, canonical_home, hand_authored, "
|
||||||
|
"created_at, updated_at) "
|
||||||
|
"VALUES (?, 'component', 'Legacy HTTP', 'p04-gigabit', "
|
||||||
|
"'', '{}', 'candidate', 1.0, '[]', '', 'entity', 0, "
|
||||||
|
"CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
|
||||||
|
(entity_id,),
|
||||||
|
)
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
r = client.post(f"/entities/{entity_id}/promote")
|
||||||
|
assert r.status_code == 400
|
||||||
|
assert "source_refs required" in r.json().get("detail", "")
|
||||||
|
|
||||||
|
# Row still candidate — the 400 didn't half-transition.
|
||||||
|
got = get_entity(entity_id)
|
||||||
|
assert got is not None
|
||||||
|
assert got.status == "candidate"
|
||||||
|
|
||||||
|
|
||||||
def test_promote_accepts_candidate_flagged_hand_authored(tmp_data_dir):
|
def test_promote_accepts_candidate_flagged_hand_authored(tmp_data_dir):
|
||||||
"""The other side of the promote re-check: hand_authored=1 with
|
"""The other side of the promote re-check: hand_authored=1 with
|
||||||
empty source_refs still lets promote succeed, matching
|
empty source_refs still lets promote succeed, matching
|
||||||
|
|||||||
Reference in New Issue
Block a user