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:
2026-04-21 20:17:32 -04:00
parent 5fbd7e6094
commit b1a3dd071e
6 changed files with 374 additions and 12 deletions

View File

@@ -23,7 +23,11 @@ from atocore.engineering.service import (
get_relationships,
)
from atocore.memory.service import get_memories
from atocore.projects.registry import load_project_registry
from atocore.projects.registry import (
GLOBAL_PROJECT,
INBOX_PROJECT,
load_project_registry,
)
_TOP_NAV_LINKS = [
@@ -147,6 +151,43 @@ def render_homepage() -> str:
except Exception:
pass
# Issue C: Inbox + Global pseudo-projects alongside registered projects.
# scope_only=True keeps real-project entities out of these counts.
try:
inbox_count = len(get_entities(
project=INBOX_PROJECT, scope_only=True, limit=500,
))
global_count = len(get_entities(
project=GLOBAL_PROJECT, scope_only=True, limit=500,
))
except Exception:
inbox_count = 0
global_count = 0
lines.append('<h2>📥 Inbox &amp; Global</h2>')
lines.append(
'<p class="emerging-intro">Entities that don\'t belong to a specific '
'project yet. <strong>Inbox</strong> holds pre-project leads and quotes. '
'<strong>Global</strong> holds cross-project facts (material properties, '
'vendor capabilities) that apply everywhere.</p>'
)
lines.append('<div class="card-grid">')
lines.append(
f'<a href="/entities?project=inbox&scope_only=true" class="card">'
f'<h3>📥 Inbox</h3>'
f'<p>Pre-project leads, quotes, early conversations.</p>'
f'<div class="stats">{inbox_count} entities</div>'
f'</a>'
)
lines.append(
f'<a href="/entities?project=&scope_only=true" class="card">'
f'<h3>🌐 Global</h3>'
f'<p>Cross-project facts: materials, vendors, shared knowledge.</p>'
f'<div class="stats">{global_count} entities</div>'
f'</a>'
)
lines.append('</div>')
for bucket_name, items in buckets.items():
if not items:
continue