feat: implement ISSUE-WP-0005 connector mapping and scope alignment
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Build and Publish Container Image / build-and-push (push) Successful in 1m22s

Add MappingService (work_record_uuid ↔ external id), project/map CLI with
outward push-status, optional TaskSpec work_record fields, boundary-sync
policy, and packaging/capability framing cleanup. Mark WP-0005 finished.
This commit is contained in:
tegwick 2026-07-22 00:25:06 +02:00
parent 6855fb2a32
commit f88e9a28fe
16 changed files with 1300 additions and 179 deletions

View file

@ -21,6 +21,7 @@ from ..backends.gitea import GiteaBackend
from ..backends.local import LocalSQLiteBackend
from ..cli.utils import get_config_dir, load_backend_configs
from ..core.interfaces import BackendFactory, IssueBackend
from ..core.mapping import MappingService
from ..core.models import Issue, IssueState, Label
from .auth import require_api_key
from .schemas import BackendName, TaskIngestionRequest, TaskIngestionResponse
@ -87,6 +88,12 @@ def _build_issue(payload: TaskIngestionRequest, backend_type: str) -> Issue:
ingestion_meta["due_at"] = (now + timedelta(days=payload.due_in_days)).isoformat()
sync_metadata: Dict[str, Any] = {"ingestion": ingestion_meta}
if payload.work_record_uuid:
sync_metadata["mapping"] = {
"work_record_uuid": payload.work_record_uuid,
"work_record_id": payload.work_record_id,
"work_record_kind": payload.work_record_kind,
}
return Issue(
id="",
@ -102,15 +109,65 @@ def _build_issue(payload: TaskIngestionRequest, backend_type: str) -> Issue:
)
def _mapping_backend_name(backend_type: str) -> str:
return "sqlite" if backend_type == "local" else backend_type
def _upsert_mapping_if_requested(
payload: TaskIngestionRequest,
*,
backend_type: str,
issue_id: str,
issue_url: Optional[str],
) -> None:
if not payload.work_record_uuid:
return
svc = MappingService()
svc.connect(str(get_config_dir() / "mappings.db"))
try:
svc.upsert(
work_record_uuid=payload.work_record_uuid,
work_record_id=payload.work_record_id,
work_record_kind=payload.work_record_kind,
backend=_mapping_backend_name(backend_type),
external_id=issue_id,
external_url=issue_url,
target_repo=payload.target_repo,
direction="outward",
)
finally:
svc.disconnect()
@router.post(
"/issues/",
response_model=TaskIngestionResponse,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(require_api_key)],
summary="Ingest a task from an external emitter (e.g. activity-core).",
summary="Create an external tracker issue (intentional emit; optional work-record mapping).",
)
async def ingest_task(payload: TaskIngestionRequest) -> TaskIngestionResponse:
backend, backend_type = _resolve_backend()
backend_name: BackendName = _BACKEND_TYPE_TO_NAME.get(backend_type, backend_type) # type: ignore[assignment]
mapping_backend = _mapping_backend_name(backend_type)
# Idempotent: if work_record_uuid already mapped, return existing issue.
if payload.work_record_uuid:
map_svc = MappingService()
map_svc.connect(str(get_config_dir() / "mappings.db"))
try:
existing = map_svc.get_active_by_uuid(
payload.work_record_uuid, backend=mapping_backend
)
if existing:
return TaskIngestionResponse(
issue_id=existing.external_id,
issue_url=existing.external_url,
backend=backend_name,
)
finally:
map_svc.disconnect()
draft = _build_issue(payload, backend_type)
try:
created: Issue = backend.create_issue(draft)
@ -131,7 +188,20 @@ async def ingest_task(payload: TaskIngestionRequest) -> TaskIngestionResponse:
issue_url: Optional[str] = None
if created.sync_metadata:
issue_url = created.sync_metadata.get("url") or created.sync_metadata.get("html_url")
backend_name: BackendName = _BACKEND_TYPE_TO_NAME.get(backend_type, backend_type) # type: ignore[assignment]
try:
_upsert_mapping_if_requested(
payload,
backend_type=backend_type,
issue_id=issue_id,
issue_url=issue_url,
)
except ValueError as exc:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=str(exc),
) from exc
return TaskIngestionResponse(
issue_id=issue_id,
issue_url=issue_url,

View file

@ -34,10 +34,25 @@ class TaskIngestionRequest(BaseModel):
min_length=1,
description=(
"Activity event UUID, or a stable scheduler/source key when no "
"event row exists."
"event row exists. Not a work-record UUID."
),
)
activity_definition_id: str = Field(..., min_length=1)
work_record_uuid: Optional[str] = Field(
default=None,
description=(
"Optional work-record UUIDv7. When set, ingestion upserts a "
"mapping row (work_record_uuid ↔ backend issue id)."
),
)
work_record_id: Optional[str] = Field(
default=None,
description="Optional canonical work-record id (human-facing).",
)
work_record_kind: Optional[str] = Field(
default=None,
description="Optional work-record kind (task, intake, …).",
)
class TaskIngestionResponse(BaseModel):