Files
ATOCore/src/atocore/main.py
Anto01 b94f9dff56 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>
2026-04-22 09:02:13 -04:00

139 lines
4.3 KiB
Python

"""AtoCore — FastAPI application entry point."""
from contextlib import asynccontextmanager
from fastapi import APIRouter, FastAPI
from fastapi.routing import APIRoute
from atocore import __version__
from atocore.api.routes import router
import atocore.config as _config
from atocore.context.project_state import init_project_state_schema
from atocore.engineering.service import init_engineering_schema
from atocore.ingestion.pipeline import get_source_status
from atocore.models.database import init_db
from atocore.observability.logger import get_logger, setup_logging
log = get_logger("main")
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Run setup before the first request and teardown after shutdown.
Replaces the deprecated ``@app.on_event("startup")`` hook with the
modern ``lifespan`` context manager. Setup runs synchronously (the
underlying calls are blocking I/O) so no await is needed; the
function still must be async per the FastAPI contract.
"""
setup_logging()
_config.ensure_runtime_dirs()
init_db()
init_project_state_schema()
init_engineering_schema()
log.info(
"startup_ready",
env=_config.settings.env,
db_path=str(_config.settings.db_path),
chroma_path=str(_config.settings.chroma_path),
source_status=get_source_status(),
)
yield
# No teardown work needed today; SQLite connections are short-lived
# and the Chroma client cleans itself up on process exit.
app = FastAPI(
title="AtoCore",
description="Personal Context Engine for LLM interactions",
version=__version__,
lifespan=lifespan,
)
app.include_router(router)
# Public API v1 — stable contract for external clients (AKC, OpenClaw, etc.).
# Paths listed here are re-mounted under /v1 as aliases of the existing
# unversioned handlers. Unversioned paths continue to work; new endpoints
# land at the latest version; breaking schema changes bump the prefix.
_V1_PUBLIC_PATHS = {
"/entities",
"/entities/{entity_id}",
"/entities/{entity_id}/promote",
"/entities/{entity_id}/reject",
"/entities/{entity_id}/invalidate",
"/entities/{entity_id}/supersede",
"/entities/{entity_id}/audit",
"/relationships",
"/ingest",
"/ingest/sources",
"/context/build",
"/query",
"/projects",
"/projects/{project_name}",
"/projects/{project_name}/refresh",
"/projects/{project_name}/mirror",
"/projects/{project_name}/mirror.html",
"/memory",
"/memory/{memory_id}",
"/memory/{memory_id}/audit",
"/memory/{memory_id}/promote",
"/memory/{memory_id}/reject",
"/memory/{memory_id}/invalidate",
"/memory/{memory_id}/supersede",
"/project/state",
"/project/state/{project_name}",
"/interactions",
"/interactions/{interaction_id}",
"/interactions/{interaction_id}/reinforce",
"/interactions/{interaction_id}/extract",
"/health",
"/sources",
"/stats",
# Issue F: asset store + evidence query
"/assets",
"/assets/{asset_id}",
"/assets/{asset_id}/thumbnail",
"/assets/{asset_id}/meta",
"/entities/{entity_id}/evidence",
# Issue D: engineering query surface (decisions, systems, components,
# gaps, evidence, impact, changes)
"/engineering/projects/{project_name}/systems",
"/engineering/decisions",
"/engineering/components/{component_id}/requirements",
"/engineering/changes",
"/engineering/gaps",
"/engineering/gaps/orphan-requirements",
"/engineering/gaps/risky-decisions",
"/engineering/gaps/unsupported-claims",
"/engineering/impact",
"/engineering/evidence",
}
_v1_router = APIRouter(prefix="/v1", tags=["v1"])
for _route in list(router.routes):
if isinstance(_route, APIRoute) and _route.path in _V1_PUBLIC_PATHS:
_v1_router.add_api_route(
_route.path,
_route.endpoint,
methods=list(_route.methods),
response_model=_route.response_model,
response_class=_route.response_class,
name=f"v1_{_route.name}",
include_in_schema=True,
)
app.include_router(_v1_router)
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"atocore.main:app",
host=_config.settings.host,
port=_config.settings.port,
reload=True,
)