feat(projects): Wave 1.5 — live emerging-project registration proposals
GET /admin/projects/proposals?min_active=N — on-demand companion to
the nightly scripts/detect_emerging.py cache. Reads SQL + the registry
directly so the result is current.
Each proposal:
- project_id (literal label as captured)
- active_count / candidate_count from current SQL
- sample_memories: 3 most recent active rows with content preview
- suggested_aliases: sibling labels sharing a >=4-char token,
case-insensitive (lead-space + lead-space-exploration-ltd +
space-exploration-ltd cluster; apm and apm-fpga do NOT cluster
via the 3-char 'apm')
- guessed_ingest_root: vault:incoming/projects/<id>/
Workflow: hit /admin/projects/proposals to see "what should I register?",
then POST to existing /admin/projects/register-emerging.
For prod: apm has 165 active memories, openclaw has 17,
hydrotech-mining variants combine to 13. apm is overdue.
Closes Codex's prior P2 from the state-of-service review. Reviewed by
Codex on tip e8ac8bb (verdict GO); two follow-on improvements (stronger
negative-clustering test + case-insensitive tokens) folded into f70fa6b.
10 regression tests covering: registered canonical/alias exclusion,
threshold filtering, sibling clustering, short-token negative,
case-insensitive clustering, registered-token-leak guard, sample shape,
candidate counting, param validation, sort order.
Test count: 586 -> 596.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -474,6 +474,33 @@ def api_register_emerging_project(req: RegisterEmergingRequest) -> dict:
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/admin/projects/proposals")
|
||||
def api_project_proposals(min_active: int = 10) -> dict:
|
||||
"""Live registration proposals for unregistered projects.
|
||||
|
||||
Reads SQL + the registry directly, so the result is current — unlike
|
||||
`/admin/dashboard.proposals.unregistered_projects` which is the
|
||||
nightly cache from `scripts/detect_emerging.py`. Each proposal
|
||||
includes a guessed ingest root, sibling labels suggested as aliases,
|
||||
and a few sample memories so the operator can sanity-check before
|
||||
POSTing to /admin/projects/register-emerging.
|
||||
|
||||
Query params:
|
||||
min_active: minimum active-memory count for a label to surface
|
||||
(default 10).
|
||||
"""
|
||||
from atocore.memory.service import propose_emerging_projects
|
||||
|
||||
if min_active < 1:
|
||||
raise HTTPException(status_code=400, detail="min_active must be >= 1")
|
||||
proposals = propose_emerging_projects(min_active=min_active)
|
||||
return {
|
||||
"proposals": proposals,
|
||||
"count": len(proposals),
|
||||
"min_active": min_active,
|
||||
}
|
||||
|
||||
|
||||
@router.put("/projects/{project_name}")
|
||||
def api_project_update(project_name: str, req: ProjectUpdateRequest) -> dict:
|
||||
"""Update an existing project registration."""
|
||||
|
||||
Reference in New Issue
Block a user