feat(entities): inbox + cross-project (project="") support (Issue C)
Makes `inbox` a reserved pseudo-project and `project=""` a first-class
cross-project bucket. Unblocks AKC capturing pre-project leads/quotes
and cross-project facts (materials, vendors) that don't fit a single
registered project.
- projects/registry.py: INBOX_PROJECT/GLOBAL_PROJECT constants,
is_reserved_project(), register/update guards, resolve_project_name
passthrough for "inbox"
- engineering/service.py: get_entities scoping rules (inbox-only,
global-only, real+global default, scope_only=true strict).
promote_entity accepts target_project to retarget on promote
- api/routes.py: GET /entities gains scope_only; POST /entities accepts
project=null as ""; POST /entities/{id}/promote accepts
{target_project, note}
- engineering/wiki.py: homepage shows "Inbox & Global" cards with live
counts linking to scoped lists
- tests/test_inbox_crossproject.py: 15 tests (reserved enforcement,
scoping rules, API shape, promote retargeting)
- DEV-LEDGER.md: session log, test_count 463 -> 478
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1374,7 +1374,11 @@ def api_dashboard() -> dict:
|
||||
class EntityCreateRequest(BaseModel):
|
||||
entity_type: str
|
||||
name: str
|
||||
project: str = ""
|
||||
# project accepts: "" (global, cross-project), "inbox" (pre-project
|
||||
# lead / quote bucket), or any registered project id/alias. Unknown
|
||||
# project names are stored verbatim (trust-preserving, same as
|
||||
# pre-registry contract).
|
||||
project: str | None = ""
|
||||
description: str = ""
|
||||
properties: dict | None = None
|
||||
status: str = "active"
|
||||
@@ -1382,6 +1386,11 @@ class EntityCreateRequest(BaseModel):
|
||||
source_refs: list[str] | None = None
|
||||
|
||||
|
||||
class EntityPromoteRequest(BaseModel):
|
||||
target_project: str | None = None
|
||||
note: str = ""
|
||||
|
||||
|
||||
class RelationshipCreateRequest(BaseModel):
|
||||
source_entity_id: str
|
||||
target_entity_id: str
|
||||
@@ -1397,7 +1406,7 @@ def api_create_entity(req: EntityCreateRequest) -> dict:
|
||||
entity = create_entity(
|
||||
entity_type=req.entity_type,
|
||||
name=req.name,
|
||||
project=req.project,
|
||||
project=req.project or "",
|
||||
description=req.description,
|
||||
properties=req.properties,
|
||||
status=req.status,
|
||||
@@ -1417,14 +1426,21 @@ def api_list_entities(
|
||||
status: str = "active",
|
||||
name_contains: str | None = None,
|
||||
limit: int = 100,
|
||||
scope_only: bool = False,
|
||||
) -> dict:
|
||||
"""List engineering entities with optional filters."""
|
||||
"""List engineering entities with optional filters.
|
||||
|
||||
When ``project`` names a real project, cross-project entities
|
||||
(``project=""``) are included by default. Pass ``scope_only=true`` to
|
||||
restrict the result to that project's own entities only.
|
||||
"""
|
||||
entities = get_entities(
|
||||
entity_type=entity_type,
|
||||
project=project,
|
||||
status=status,
|
||||
name_contains=name_contains,
|
||||
limit=limit,
|
||||
scope_only=scope_only,
|
||||
)
|
||||
return {
|
||||
"entities": [
|
||||
@@ -2078,13 +2094,31 @@ def api_evidence_chain(entity: str) -> dict:
|
||||
|
||||
|
||||
@router.post("/entities/{entity_id}/promote")
|
||||
def api_promote_entity(entity_id: str) -> dict:
|
||||
"""Promote a candidate entity to active (Phase 5 Engineering V1)."""
|
||||
def api_promote_entity(
|
||||
entity_id: str,
|
||||
req: EntityPromoteRequest | None = None,
|
||||
) -> dict:
|
||||
"""Promote a candidate entity to active.
|
||||
|
||||
Optional ``target_project`` in the body retargets the entity's
|
||||
project on promote — used to graduate inbox/global leads into a real
|
||||
project when they mature (Issue C).
|
||||
"""
|
||||
from atocore.engineering.service import promote_entity
|
||||
success = promote_entity(entity_id, actor="api-http")
|
||||
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,
|
||||
)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail=f"Entity not found or not a candidate: {entity_id}")
|
||||
return {"status": "promoted", "id": entity_id}
|
||||
result = {"status": "promoted", "id": entity_id}
|
||||
if target_project is not None:
|
||||
result["target_project"] = target_project
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/entities/{entity_id}/reject")
|
||||
|
||||
Reference in New Issue
Block a user