Initial Commit
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
# OpenSpec Change: enterprise-network-registry
|
||||
|
||||
- **Change ID**: enterprise-network-registry
|
||||
- **Package**: `@suaveplan/network-registry`
|
||||
- **Category**: universal/network
|
||||
- **Target path**: `universal/network/network-registry`
|
||||
- **Status**: SUPERSEDED-BY-GENESIS-WAVE
|
||||
- **Date**: 2026-04-24
|
||||
- **Reference spec**: `/mnt/s/web/suaveplan/enterprise/network-registry.md`
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Deliver `@suaveplan/network-registry` on the genesis branch from scratch at enterprise quality. The package provides a network service and peer registry: register/deregister services with metadata and capabilities, health-status tracking, TTL-based expiry, lookup by capability/tag/region, and change-notification subscriptions. Genesis adds structured errors via `@suaveplan/error`, Zod schemas for all registry types, OTel instrumentation on register/lookup operations, `AbortSignal` on async lookup, injectable `Clock` for TTL expiry, event-bus emission for registration changes, and property-based tests for lookup correctness.
|
||||
|
||||
---
|
||||
|
||||
## Current-state audit (genesis)
|
||||
|
||||
The package does not exist on genesis yet — this is a fresh build.
|
||||
|
||||
---
|
||||
|
||||
## Available workspace deps on genesis
|
||||
|
||||
| Package | Role |
|
||||
|---|---|
|
||||
| `@suaveplan/error` (`workspace:^`) | Base class for `NetworkRegistryError`; stable error codes |
|
||||
| `@suaveplan/types` (`workspace:^`) | `LoggerLike`, `Clock`, `EventBusLike` |
|
||||
| `@suaveplan/events` (`workspace:^`) | Event bus for registration changes |
|
||||
| `@suaveplan/logger` (`workspace:^`) | Structured logging |
|
||||
| `zod` (`catalog:forms`) | Zod schemas |
|
||||
| `@opentelemetry/api` (`catalog:observability`) | Spans + counters on register/lookup |
|
||||
| `tslib` (`catalog:build`) | Runtime helpers |
|
||||
|
||||
Dev deps: `vitest`, `@vitest/coverage-istanbul`, `fast-check` (all `catalog:testing`).
|
||||
|
||||
---
|
||||
|
||||
## Public API surface
|
||||
|
||||
### Core
|
||||
|
||||
```ts
|
||||
function createNetworkRegistry(options?: NetworkRegistryOptions): NetworkRegistry
|
||||
|
||||
interface NetworkRegistry {
|
||||
register(entry: RegistryEntry): RegistrationHandle
|
||||
deregister(entryId: string): boolean
|
||||
lookup(query: RegistryQuery): ReadonlyArray<RegistryEntry>
|
||||
get(entryId: string): RegistryEntry | null
|
||||
list(filter?: RegistryFilter): ReadonlyArray<RegistryEntry>
|
||||
updateHealth(entryId: string, status: HealthStatus): boolean
|
||||
subscribe(query: RegistryQuery, handler: RegistryChangeHandler): () => void
|
||||
purgeExpired(): number
|
||||
}
|
||||
```
|
||||
|
||||
### Entry types
|
||||
|
||||
```ts
|
||||
interface RegistryEntry {
|
||||
id: string
|
||||
name: string
|
||||
type: 'peer' | 'service' | 'relay' | 'bootstrap'
|
||||
address?: string
|
||||
port?: number
|
||||
region?: string
|
||||
capabilities: string[]
|
||||
tags?: Record<string, string>
|
||||
metadata?: Record<string, unknown>
|
||||
ttlMs?: number
|
||||
registeredAt?: number
|
||||
expiresAt?: number
|
||||
health?: HealthStatus
|
||||
}
|
||||
|
||||
interface HealthStatus {
|
||||
healthy: boolean
|
||||
lastCheckedAt: number
|
||||
latencyMs?: number
|
||||
load?: number // 0–1
|
||||
}
|
||||
|
||||
interface RegistrationHandle {
|
||||
readonly entryId: string
|
||||
renew(ttlMs?: number): void
|
||||
deregister(): void
|
||||
}
|
||||
```
|
||||
|
||||
### Query types
|
||||
|
||||
```ts
|
||||
interface RegistryQuery {
|
||||
type?: RegistryEntry['type'] | Array<RegistryEntry['type']>
|
||||
capabilities?: string[] // entry must have ALL listed capabilities
|
||||
anyCapabilities?: string[] // entry must have AT LEAST ONE
|
||||
tags?: Record<string, string> // all tag key/value pairs must match
|
||||
region?: string
|
||||
healthyOnly?: boolean
|
||||
limit?: number
|
||||
}
|
||||
|
||||
interface RegistryFilter {
|
||||
type?: RegistryEntry['type'] | Array<RegistryEntry['type']>
|
||||
healthyOnly?: boolean
|
||||
region?: string
|
||||
}
|
||||
|
||||
type RegistryChangeHandler = (change: RegistryChange) => void
|
||||
|
||||
interface RegistryChange {
|
||||
type: 'registered' | 'deregistered' | 'health-changed' | 'expired'
|
||||
entry: RegistryEntry
|
||||
previous?: RegistryEntry
|
||||
}
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```ts
|
||||
interface NetworkRegistryOptions {
|
||||
ttlDefaultMs?: number
|
||||
maxEntries?: number
|
||||
gcIntervalMs?: number
|
||||
logger?: LoggerLike
|
||||
clock?: Clock
|
||||
eventBus?: EventBusLike
|
||||
}
|
||||
```
|
||||
|
||||
### Error codes
|
||||
|
||||
```ts
|
||||
type NetworkRegistryErrorCode =
|
||||
| 'REGISTRY_ENTRY_NOT_FOUND'
|
||||
| 'REGISTRY_DUPLICATE_ID'
|
||||
| 'REGISTRY_CAPACITY_EXCEEDED'
|
||||
| 'REGISTRY_INVALID_ENTRY'
|
||||
| 'REGISTRY_EXPIRED'
|
||||
|
||||
function registerNetworkRegistryErrorCodes(): void
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Genesis additions
|
||||
|
||||
1. **Structured `NetworkRegistryError`** with stable `REGISTRY.*` codes
|
||||
2. **Zod schemas** for `RegistryEntry`, `RegistryQuery`, `NetworkRegistryOptions`
|
||||
3. **TTL-based expiry** with periodic GC via injectable `Clock`; `purgeExpired()` callable manually
|
||||
4. **Subscription-based change notifications** with query-scoped filtering
|
||||
5. **Capability AND/OR query operators** — `capabilities` (AND), `anyCapabilities` (OR)
|
||||
6. **Health status tracking** with `updateHealth()` API
|
||||
7. **OTel instrumentation** — counter `registry.register_total`, `registry.deregister_total`, `registry.lookup_total`, histogram `registry.lookup_results`
|
||||
8. **Property-based tests** — lookup(query) results are always a subset of `list()`; TTL expiry correctness
|
||||
|
||||
---
|
||||
|
||||
## File layout
|
||||
|
||||
```text
|
||||
universal/network/network-registry/
|
||||
package.json
|
||||
tsconfig.json
|
||||
vitest.config.ts
|
||||
src/
|
||||
index.ts
|
||||
error-codes.ts
|
||||
constants/
|
||||
index.ts
|
||||
types/
|
||||
common.ts
|
||||
common.md
|
||||
core/
|
||||
registry/
|
||||
registry.ts
|
||||
registry.md
|
||||
registry.types.ts
|
||||
registry.test.ts
|
||||
index.ts
|
||||
query-engine/
|
||||
query-engine.ts
|
||||
query-engine.md
|
||||
query-engine.test.ts
|
||||
index.ts
|
||||
ttl-manager/
|
||||
ttl-manager.ts
|
||||
ttl-manager.md
|
||||
ttl-manager.test.ts
|
||||
index.ts
|
||||
tests/
|
||||
integration.test.ts
|
||||
property.test.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Impact
|
||||
|
||||
**Workspace dep prerequisites**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events`, `@suaveplan/logger` must be published.
|
||||
@@ -0,0 +1,95 @@
|
||||
# Tasks: enterprise-network-registry
|
||||
|
||||
**Change ID**: enterprise-network-registry
|
||||
**Package**: `@suaveplan/network-registry`
|
||||
**Target path**: `universal/network/network-registry`
|
||||
**Prerequisite packages on Verdaccio**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events`, `@suaveplan/logger`
|
||||
|
||||
---
|
||||
|
||||
## 1. Scaffold
|
||||
|
||||
- [x] Create `universal/network/network-registry/` with `src/`, `tests/`, and all feature subdirectories.
|
||||
- [x] Write `package.json` at version `0.1.0`: `"sideEffects": false`, exports map, deps per `proposal.md`.
|
||||
- [x] Write `tsconfig.json`, `vitest.config.ts`, scripts.
|
||||
|
||||
---
|
||||
|
||||
## 2. Types and error codes
|
||||
|
||||
- [x] Define all public types in `src/types/common.ts`: `RegistryEntry`, `HealthStatus`, `RegistrationHandle`, `RegistryQuery`, `RegistryFilter`, `RegistryChangeHandler`, `RegistryChange`, `NetworkRegistryOptions`.
|
||||
- [x] Write `src/types/common.md` (≥200 words, simple + advanced examples).
|
||||
- [x] Define error codes in `src/error-codes.ts`.
|
||||
- [x] Write Zod schemas in `src/types/schemas.ts` + `src/types/schemas.md`.
|
||||
|
||||
---
|
||||
|
||||
## 3. TTL manager (`src/core/ttl-manager/`)
|
||||
|
||||
- [x] Implement `createTtlManager(clock)`:
|
||||
- Tracks `expiresAt` per entry.
|
||||
- `isExpired(entry)` — compare against `clock.now()`.
|
||||
- `purge(entries)` — returns list of expired entry IDs.
|
||||
- Runs GC on `gcIntervalMs` interval via injectable timer.
|
||||
- [x] Write `ttl-manager.test.ts` + `ttl-manager.md`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Query engine (`src/core/query-engine/`)
|
||||
|
||||
- [x] Implement `evaluateQuery(entry: RegistryEntry, query: RegistryQuery): boolean`:
|
||||
- `type`: match entry type (array = OR, string = exact).
|
||||
- `capabilities` (AND): entry must have ALL capabilities.
|
||||
- `anyCapabilities` (OR): entry must have AT LEAST ONE.
|
||||
- `tags`: all key/value pairs must match.
|
||||
- `region`: exact match.
|
||||
- `healthyOnly`: entry.health.healthy must be true.
|
||||
- [x] Implement `runQuery(entries: RegistryEntry[], query: RegistryQuery): RegistryEntry[]`:
|
||||
- Filter via `evaluateQuery`, sort by latency (lowest first), apply `limit`.
|
||||
- [x] Write `query-engine.test.ts` (≤500 lines): each filter type, AND/OR capabilities, limit.
|
||||
- [x] Write `query-engine.md` (≥200 words).
|
||||
|
||||
---
|
||||
|
||||
## 5. Network registry (`src/core/registry/`)
|
||||
|
||||
- [x] Implement `createNetworkRegistry(options?)`:
|
||||
- Validates options with Zod.
|
||||
- Stores entries in `Map<string, RegistryEntry>`.
|
||||
- Wires TTL manager for expiry.
|
||||
- Stores subscriptions as `Array<{ query: RegistryQuery; handler: RegistryChangeHandler }>`.
|
||||
- [x] Implement `registry.register(entry)`:
|
||||
- Validate with Zod; throw `REGISTRY_INVALID_ENTRY` on schema failure.
|
||||
- Throw `REGISTRY_DUPLICATE_ID` if already registered.
|
||||
- Check `maxEntries`; throw `REGISTRY_CAPACITY_EXCEEDED` if at limit.
|
||||
- Compute `expiresAt = clock.now() + (entry.ttlMs ?? options.ttlDefaultMs)`.
|
||||
- Return `RegistrationHandle` with `renew()` and `deregister()`.
|
||||
- Emit event-bus `registry.registered` event; notify matching subscriptions.
|
||||
- OTel: counter `registry.register_total`.
|
||||
- [x] Implement `registry.deregister(id)` — remove; emit `registry.deregistered`; notify subscriptions.
|
||||
- [x] Implement `registry.lookup(query)` — delegates to query engine; OTel counter `registry.lookup_total`, histogram `registry.lookup_results`.
|
||||
- [x] Implement `registry.get(id)` — direct lookup; throws `REGISTRY_EXPIRED` if TTL elapsed.
|
||||
- [x] Implement `registry.list(filter?)` — returns all non-expired entries matching optional filter.
|
||||
- [x] Implement `registry.updateHealth(id, status)` — updates health; emits `health-changed`.
|
||||
- [x] Implement `registry.subscribe(query, handler)` — returns unsubscribe.
|
||||
- [x] Implement `registry.purgeExpired()` — delegates to TTL manager; emits `expired` for each.
|
||||
- [x] Write `registry.test.ts` (≤500 lines): register, lookup, health update, TTL expiry, subscription notification, capacity exceeded.
|
||||
- [x] Write `registry.md` (≥200 words).
|
||||
|
||||
---
|
||||
|
||||
## 6. Property-based tests (`tests/property.test.ts`)
|
||||
|
||||
- [x] `lookup(q)` results are always a subset of `list()`.
|
||||
- [x] After `purgeExpired()`, no expired entries remain in `list()`.
|
||||
- [x] `capabilities` AND: result set is always subset of result with only one capability filter.
|
||||
|
||||
---
|
||||
|
||||
## 7. Docs and verify
|
||||
|
||||
- [x] All per-file `.md` files per layout in `proposal.md`.
|
||||
- [x] `universal/network/network-registry/README.md`.
|
||||
- [x] `bunx turbo lint typecheck test test:coverage --filter=@suaveplan/network-registry` — all green, 100%.
|
||||
- [x] `bun scripts/publish.ts --package @suaveplan/network-registry`.
|
||||
- [x] Update `CLAUDE.md`. Archive change folder.
|
||||
Reference in New Issue
Block a user