Project Hub started as a read-only viewer for portfolio state. In this window, it turned into an active coordination layer. When multiple independent services and agent swarms evolve simultaneously, inter-service contracts cannot just be informal slack messages or unindexed markdown notes. They need formal lifecycle management, explicit cross-project dependency tracking, and atomic execution guarantees.
The centerpiece of this burst is ADR-008: Interface Requests (#36). Rather
than letting consuming services guess when another project will deliver a needed
API change, consumer projects can now file formal interface requests against a
published contract. The backing storage was split cleanly across two migrations
(012_contracts.sql and 013_interface_requests.sql), sharing a unified prefix
key allocator (HubKey) with standard work items. Importantly, created_at
was tracked independently of updated_at: once requests transition through
triage and deferral states, measuring how long a request has sat unanswered
becomes impossible if you only have a mutation timestamp.
The state machine governing these requests (#37) models all five legal edges
and rigidly enforces twenty illegal transitions in Core domain logic. When a
provider team accepts a breaking interface request, the acceptance must
atomically spawn the provider’s backing implementation task. We enforced this
transactionally in Postgres: the work item is inserted first, and the interface
request updates its disposition with a foreign key to spawned_work_item_id in
the same unit of work. An “accepted request pointing at nothing” is
physically impossible at the schema level.
To surface this work cleanly, the project-hub-ui board added a provider
ledger view with aging buckets (#21, #22), alongside deep linking support
for canonical work item keys at /items/:key (#18). On the backend, we also
landed the Gitea issues connector (#35) to map repository issues into the
canonical workspace with derived ownership, and integrated a pgvector
embedding store (#41) backed by llm-proxy to enable semantic search across
portfolio tasks.
Lessons learned
- Make illegal lifecycle states physically unstorable in the schema. Relying
on application-layer orchestration to ensure an accepted contract request
creates a work item will eventually fail under network timeouts or concurrent
races. A foreign key constraint requiring
spawned_work_item_idon accepted records turns an orchestration invariant into an unbreachable database rule. - Never conflate creation timestamps with mutation timestamps when tracking
aging. As soon as an entity moves through review, comment, or triage stages,
updated_atmoves with it. If aging reports (“requests waiting over 30 days”) rely on updated timestamps, triaging a stuck item paradoxically resets its urgency clock. - Abstract contract test suites keep in-memory twins from drifting. When supporting an in-memory test double alongside real Postgres storage, write the verification suite once as an abstract base class. Running the exact same 100+ assertions against both ensures the in-memory double reflects real transactional semantics without hidden drift.
- Lint design tokens in CI before shipping cross-package frontend bundles. Consuming design systems across separate npm repositories often fails silently when a referenced CSS variable name shifts. Adding an explicit undefined-token lint step in the pipeline prevents unstyled cards and broken theme tokens from ever hitting production.