Setup repo: migrate architecture specs, code stubs, and tasks from alkhub_ts

Copy architecture docs, ADRs, storage domain specs, research, reviews,
and 56 storage architecture tasks from the alkhub_ts monorepo. Adapt for
standalone @alkdev/hub repo structure (src/ not packages/hub/).

Sanitize all sensitive information:
- Replace private IPs (10.0.0.1) with localhost defaults
- Remove internal server hostnames (dev1, ns528096)
- Replace /workspace/ private paths with npm package references
- Remove hardcoded credentials from examples
- Rewrite infrastructure.md without private network details

Add Deno project scaffolding: deno.json (pinned deps), .gitignore,
AGENTS.md, entry point. Migrate existing code stubs (crypto, config
types, logger) with updated import paths.
This commit is contained in:
2026-05-25 10:56:32 +00:00
parent 3e3f12d2d5
commit 2b63cda1c7
120 changed files with 11714 additions and 2 deletions

View File

@@ -0,0 +1,92 @@
---
status: draft
last_updated: 2026-04-19
---
# Table Schemas: Spokes & Operations
Spoke registration and operation specification tables. For cross-cutting reference (cascade behavior, index reference, status enums, relations), see [table-reference.md](./table-reference.md). For design decisions, see [../../../decisions/](../../../decisions/). For spoke architecture, see [../../spoke-runner.md](../../spoke-runner.md).
### `spokes`
Spoke registrations. When a spoke connects to the hub via WebSocket, it calls `hub.register` with its details and operation list. The hub creates a spoke record and registers the operations. When the spoke disconnects, the record is updated with `status: "disconnected"`.
| Column | Type | Notes |
|--------|------|-------|
| commonCols | — | id, metadata, createdAt, updatedAt |
| name | text NOT NULL | Spoke display name |
| status | text NOT NULL | Enum: `connected`, `disconnected`. Default: `connected` |
| spokeType | text NOT NULL | Spoke type: `dev-env`, `client`, `compute` |
| projectId | text | FK → projects.id (nullable — some spokes aren't project-scoped) |
| lastHeartbeat | timestamp with tz | Last heartbeat timestamp |
| hostInfo | jsonb | Host metadata (`{ os, arch, nodeVersion, memory, cpu }`) |
| connectedAt | timestamp with tz | When the spoke connected |
| disconnectedAt | timestamp with tz | When the spoke disconnected (null if still connected) |
**Indexes**: `idx_spokes_project_id` on `(projectId)`, `idx_spokes_status` on `(status)`, `idx_spokes_name` on `(name)` — look up spoke by name, `idx_spokes_active` partial on `(id)` WHERE `status = 'connected'` — efficiently find connected spokes.
**No `reconnecting` status**: Spoke reconnection is handled at the WebSocket layer, not in the database. When a spoke disconnects, its status becomes `disconnected`. When it reconnects, it's a new connection — the spoke row is updated back to `connected` with a new `connectedAt`. Transient reconnection attempts don't need a database state; they're a transport concern.
If monitoring of reconnection attempts is needed, use the call graph (a `hub.register` call from the spoke) or observability events (WebSocket reconnection logs), not a database status.
**No `capabilities` column on spokes**: A spoke's capabilities are its registered operations. Query `operation_registrations` filtered by `providerId` and `status = 'active'` to find what a connected spoke can do. The `operations` table holds the definitions. See ADR-006 in decisions/.
**Relationship to operations and registrations**: When a spoke calls `hub.register` with an operations list, the hub creates or finds `operations` rows (definitions) for each operation, then creates `operation_registrations` rows linking the spoke to those definitions. When the spoke disconnects, registrations are set to `inactive` but definitions persist. See the `operations` and `operation_registrations` tables below.
**Input mapping from `hub.register`**: The `hub.register` operation (see spoke-runner.md) accepts `{ spokeId, operations[], spokeType, project, hardware }`. This maps to the `spokes` table columns as: `spokeId``id`, `spokeType``spokeType`, `project``projectId` (looked up by project identifier), `hardware``hostInfo`. The `name` field may be derived from the spoke's configuration or provided separately. Each operation in `operations[]` maps to an `operations` row (definition, created or found by namespace+name) and an `operation_registrations` row (provider binding, linking the spoke to the definition).
### `operations`
Operation definitions — what an operation IS. These persist independently of spoke connections. Multiple providers can register the same operation (by namespace+name); they share the definition.
| Column | Type | Notes |
|--------|------|-------|
| commonCols | — | id, metadata, createdAt, updatedAt |
| namespace | text NOT NULL | Post-remap identifier (e.g., `dev.{spokeId}.fs.read`) |
| name | text NOT NULL | Operation name within namespace (e.g., `fs.read`, `call`) |
| type | text NOT NULL | `QUERY`, `MUTATION`, `SUBSCRIPTION` |
| description | text | Human-readable description |
| inputSchema | jsonb NOT NULL | TypeBox schema for input |
| outputSchema | jsonb | TypeBox schema for output |
| errorSchemas | jsonb | Array of error type schemas |
| accessControl | jsonb | Access control definition |
| tags | jsonb | String array for search/filter |
**Unique constraint**: `CREATE UNIQUE INDEX unq_operations_namespace_name ON operations (namespace, name)` — operation definitions are unique by namespace+name, regardless of how many providers register them.
**Indexes**: `idx_operations_namespace` on `(namespace)`, `idx_operations_type` on `(type)`.
### `operation_registrations`
Provider registrations — which spoke/client PROVIDES an operation right now. Ephemeral data: these reflect the current runtime state of who can handle a call.
| Column | Type | Notes |
|--------|------|-------|
| commonCols | — | id, metadata, createdAt, updatedAt |
| operationId | text NOT NULL | FK → operations.id (CASCADE — deleting a definition removes all its registrations) |
| providerType | text NOT NULL | `spoke` or `client` — which provider type |
| providerId | text NOT NULL | FK → spokes.id when providerType is `spoke`; FK → clients.id when providerType is `client` |
| preRemapNamespace | text | The original namespace before remapping (e.g., `dev` for `dev.{spokeId}.fs.read`). Stored for traceability. |
| preRemapName | text | The original name before remapping |
| status | text NOT NULL | `active` or `inactive`. Default: `active`. Set to `inactive` on disconnect, re-activated on reconnect. |
| metadata | jsonb | Provider-specific metadata (version, health, latency hints) |
**Unique constraint**: `CREATE UNIQUE INDEX unq_operation_registrations_active ON operation_registrations (operationId, providerType, providerId) WHERE status = 'active'` — only one active registration per provider per operation.
**Indexes**: `idx_operation_registrations_operation_id` on `(operationId)`, `idx_operation_registrations_provider_id` on `(providerId)`, `idx_operation_registrations_status` on `(status)`.
**Spoke registration lifecycle**: When a spoke connects and registers:
1. Creates/updates the `spokes` row
2. For each operation the spoke provides:
- Creates or finds the `operations` row (by namespace+name). If this is a new spoke instance providing a known operation, the definition already exists.
- Creates an `operation_registrations` row linking the spoke to the operation definition, with `status: 'active'` and the pre-remap identifiers.
When a spoke disconnects:
1. Updates the `spokes` row to `status: "disconnected"`
2. Sets all the spoke's `operation_registrations` rows to `status: "inactive"`
3. Aborts in-flight calls via call protocol cascading
4. Operation definitions (in `operations`) are **never deleted on disconnect** — they persist for audit and potential reconnection.
When an admin deletes a spoke row (rare):
1. `operation_registrations` with that `providerId` are CASCADE deleted (ephemeral data, follows D1 cascade policy for ephemeral config)
2. If no other registrations exist for an operation, its definition may be cleaned up separately