Close HUB-WP-0009 conformance gaps (C2, C7, C9, C10); mark blocked workplans
Some checks are pending
CI Smoke / host-smoke (push) Waiting to run
CI Smoke / pytest-smoke (push) Waiting to run

Implements the four residual conformance checks left open by the T04
minimal vertical:

- C2: GET /ports/registry/registrations/{hub_slug} resolves missing (404),
  ambiguous (shared reuse_surface_id across hub_slugs), and stale
  (deprecated/retired descriptor) registrations; a new .../audit route
  exposes queryable registration history from the existing in-memory
  history and the PostgreSQL runtime_audit_ledger.
- C7: harness proof that disabled compatibility groups deny access
  (404) with no fixture credentials involved, matching the existing
  fail-closed compat router behavior.
- C9: harness proof plus a dedicated test that /readyz degrades only on
  an unavailable configured dependency while unrelated disabled
  projections stay non-blocking.
- C10: ContractValidator now negotiates contract_version_min/max against
  the runtime's contract version and rejects incompatible or inverted
  ranges with an explicit 422 instead of silently accepting them.

HUB-WP-0009 is now finished. HUB-WP-0006 is marked blocked: its only open
task (T06) has no remaining hub-core code path and waits on an external
Forgejo identity/production deployment gate. HUB-WP-0011 is marked
blocked: T02/T03 already waited on external credential/deployment
review, and T01 needs a source/destination ownership and retention
decision against live message data before it can be implemented safely.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: sonnet
Assistant-Process: 310936@bnt-lap001
Assistant-Session: 00cd9abe-09a0-416b-88e0-f907b9101629
This commit is contained in:
tegwick 2026-09-27 23:59:35 +02:00
parent b0e89592c6
commit e89d621f18
11 changed files with 496 additions and 20 deletions

View file

@ -64,6 +64,10 @@ class PortStore(Protocol):
self, *, checked_at: datetime, diagnostic: Mapping[str, Any]
) -> None: ...
async def resolve_registration(self, hub_slug: str) -> PortRecord | None: ...
async def list_registration_audit(self, hub_slug: str) -> PortCollection: ...
class InMemoryPortStore:
"""Deterministic ephemeral backend for local runtime and conformance tests.
@ -77,6 +81,7 @@ class InMemoryPortStore:
def __init__(self) -> None:
self._lock = asyncio.Lock()
self._registrations: dict[str, dict[str, Any]] = {}
self._registration_audit: dict[str, list[dict[str, Any]]] = {}
self._messages: list[dict[str, Any]] = []
self._progress_events: list[dict[str, Any]] = []
self._interaction_events: list[dict[str, Any]] = []
@ -96,12 +101,36 @@ class InMemoryPortStore:
async with self._lock:
duplicate = self._registrations.get(hub_slug) == value
self._registrations[hub_slug] = deepcopy(value)
self._registration_audit.setdefault(hub_slug, []).append(
{
"id": str(uuid4()),
"action": "registry.duplicate" if duplicate else "registry.accepted",
"hub_slug": hub_slug,
"correlation_id": str(correlation_id),
"recorded_at": _now().isoformat(),
}
)
return PortAccepted(
id=hub_slug,
status="duplicate" if duplicate else "accepted",
correlation_id=correlation_id,
)
async def resolve_registration(self, hub_slug: str) -> PortRecord | None:
async with self._lock:
value = self._registrations.get(hub_slug)
if value is None:
return None
registrations = deepcopy(self._registrations)
return _resolve_registration_record("hub-core-memory", hub_slug, value, registrations)
async def list_registration_audit(self, hub_slug: str) -> PortCollection:
async with self._lock:
entries = deepcopy(self._registration_audit.get(hub_slug, []))
return PortCollection(
items=[self._record("registration_audit", entry) for entry in entries]
)
async def send_message(self, command: MessageCommand) -> PortAccepted:
message_id = uuid4()
value = {
@ -250,6 +279,48 @@ def _now() -> datetime:
return datetime.now(timezone.utc)
def _resolve_registration_record(
source_system: str,
hub_slug: str,
value: Mapping[str, Any],
registrations: Mapping[str, Mapping[str, Any]],
) -> PortRecord:
descriptor = value["descriptor"]
reuse_surface_id = descriptor.get("reuse_surface_id")
ambiguous_with = sorted(
other_slug
for other_slug, other_value in registrations.items()
if other_slug != hub_slug
and other_value["descriptor"].get("reuse_surface_id") == reuse_surface_id
)
stale = descriptor.get("status") in {"deprecated", "retired"}
if ambiguous_with:
resolution = "ambiguous"
elif stale:
resolution = "stale"
else:
resolution = "ok"
data = {
"hub_slug": hub_slug,
"resolution": resolution,
"ambiguous_with": ambiguous_with,
"descriptor": deepcopy(dict(descriptor)),
"manifest": deepcopy(dict(value["manifest"])),
}
encoded = json.dumps(data, sort_keys=True, separators=(",", ":")).encode()
return PortRecord(
id=hub_slug,
data=data,
provenance=Provenance(
source_system=source_system,
source_ref=f"{source_system.replace('hub-core-', '')}://registration/{hub_slug}",
schema_version=CONTRACT_VERSION,
content_hash=hashlib.sha256(encoded).hexdigest(),
indexed_at=_now(),
),
)
def _rebuild_sources(projection_id: str) -> list[str]:
return {
"hub_registry": ["hub_descriptors", "hub_manifests"],