From f19b3a3d0f046ef41449301c0449de07504d17e3 Mon Sep 17 00:00:00 2001 From: Anto01 Date: Wed, 22 Apr 2026 15:29:45 -0400 Subject: [PATCH] =?UTF-8?q?fix(api):=20R14=20=E2=80=94=20promote=20route?= =?UTF-8?q?=20translates=20V1-0=20ValueError=20to=20400?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .obsidian/app.json | 1 + .obsidian/appearance.json | 1 + .obsidian/core-plugins.json | 33 +++++ .obsidian/workspace.json | 190 ++++++++++++++++++++++++++++ DEV-LEDGER.md | 2 +- src/atocore/api/routes.py | 17 ++- tests/test_v1_0_write_invariants.py | 36 ++++++ 7 files changed, 273 insertions(+), 7 deletions(-) create mode 100644 .obsidian/app.json create mode 100644 .obsidian/appearance.json create mode 100644 .obsidian/core-plugins.json create mode 100644 .obsidian/workspace.json diff --git a/.obsidian/app.json b/.obsidian/app.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.obsidian/app.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/.obsidian/appearance.json b/.obsidian/appearance.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.obsidian/appearance.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/.obsidian/core-plugins.json b/.obsidian/core-plugins.json new file mode 100644 index 0000000..639b90d --- /dev/null +++ b/.obsidian/core-plugins.json @@ -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 +} \ No newline at end of file diff --git a/.obsidian/workspace.json b/.obsidian/workspace.json new file mode 100644 index 0000000..e783938 --- /dev/null +++ b/.obsidian/workspace.json @@ -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" + ] +} \ No newline at end of file diff --git a/DEV-LEDGER.md b/DEV-LEDGER.md index 3c099e5..885d361 100644 --- a/DEV-LEDGER.md +++ b/DEV-LEDGER.md @@ -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) | | 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) | -| 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 diff --git a/src/atocore/api/routes.py b/src/atocore/api/routes.py index cd93786..dcfa42f 100644 --- a/src/atocore/api/routes.py +++ b/src/atocore/api/routes.py @@ -2187,12 +2187,17 @@ def api_promote_entity( from atocore.engineering.service import promote_entity target_project = req.target_project if req is not None else None note = req.note if req is not None else "" - success = promote_entity( - entity_id, - actor="api-http", - note=note, - target_project=target_project, - ) + try: + success = promote_entity( + entity_id, + actor="api-http", + 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: raise HTTPException(status_code=404, detail=f"Entity not found or not a candidate: {entity_id}") result = {"status": "promoted", "id": entity_id} diff --git a/tests/test_v1_0_write_invariants.py b/tests/test_v1_0_write_invariants.py index d993c6d..e619382 100644 --- a/tests/test_v1_0_write_invariants.py +++ b/tests/test_v1_0_write_invariants.py @@ -160,6 +160,42 @@ def test_promote_rejects_legacy_candidate_without_provenance(tmp_data_dir): 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): """The other side of the promote re-check: hand_authored=1 with empty source_refs still lets promote succeed, matching