Initial Commit
This commit is contained in:
83
openspec/archive/2026-05-25-api-ws/proposal.md
Normal file
83
openspec/archive/2026-05-25-api-ws/proposal.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# `@suaveplan/api-ws` — abstract WebSocket Connection, Registry, Bus, Broker contract
|
||||
|
||||
**Package**: `@suaveplan/api-ws`
|
||||
**Status**: PROPOSED
|
||||
**Type**: new-package
|
||||
**Stage**: 1 (Contracts) — see umbrella `openspec/changes/api-plugin-foundation/`
|
||||
|
||||
## Why
|
||||
|
||||
Persistent connections are first-class in the API foundation (design §11). Plugins declare WebSocket endpoints in their manifest; the host runs the engine, manages connection lifecycle, fans out subscriptions. Concrete engines (`api-ws-bun`) and multi-host brokers (`api-ws-broker-redis`, `api-ws-broker-in-memory`) plug behind a stable contract. This package owns that contract: `Connection`, `ConnectionRegistry`, `SubscriptionBus`, `WsBroker`, plus the cross-tenant gate that requires `ws.broadcast-cross-tenant` capability.
|
||||
|
||||
Per design §11.2: "`conn.subscribe(topic, { tenant?, filter? })` registers the connection with the host's subscription bus." Per §11.3: brokers make `bus.broadcast` visible across host replicas without migrating connections. Both shapes live here.
|
||||
|
||||
## What changes
|
||||
|
||||
Net-new package. Contract types + Zod schemas for the wire envelope + the cross-tenant capability gate + the conformance harness.
|
||||
|
||||
- ADDED: `./connection` — abstract `WsConnection`: `principal`, `tenantId`, `subscribe(topic, opts)`, `unsubscribe(topic)`, `send(message)`, `close(code, reason)`, `ack(id)`, `id`, `lastEventId`, `state` (`open` | `closing` | `closed`).
|
||||
- ADDED: `./connection-registry` — abstract `ConnectionRegistry`: `add(conn)`, `remove(conn)`, `lookupByTenant(tenant)`, `lookupByTopic(topic, tenant?)`, `countByTenant(tenant)`. Backed by a multi-index.
|
||||
- ADDED: `./subscription-bus` — abstract `SubscriptionBus`: `subscribe(conn, topic, { tenant?, filter? })`, `unsubscribe(conn, topic)`, `broadcast(topic, { tenant, payload, filter? })`, `asyncIterator(topic, opts)`.
|
||||
- ADDED: `./broker` — abstract `WsBroker` for multi-host fan-out: `publish(topic, envelope)`, `subscribe(topic, handler)`, `dispose()`. Connections are NOT migrated across hosts — the broker only makes broadcasts visible.
|
||||
- ADDED: `./envelope` — Zod schema for the wire `WsEnvelope` (`{ id, type, topic?, payload? }`); type-narrowing helpers.
|
||||
- ADDED: `./backpressure` — `BackpressurePolicy`: bounded send queue (`maxQueuedMessages`, default 256); eviction emits `BACKPRESSURE_EVICTED` close.
|
||||
- ADDED: `./heartbeat` — `HeartbeatPolicy`: `heartbeatMs` (default 30000), two missed pongs → close `ABNORMAL`.
|
||||
- ADDED: `./reconnect-token` — `ReconnectToken` HMAC envelope (`{ tenant, principalId, lastEventId, exp }`); 5-min TTL constant.
|
||||
- ADDED: `./capability-id` — adds `"ws"` AND `"ws.broadcast-cross-tenant"` to the `CapabilityId` union via type-level augmentation.
|
||||
- ADDED: `./error-codes` — frozen `WS_CODES`: `HANDSHAKE_FAILED`, `PRINCIPAL_EXPIRED`, `BACKPRESSURE_EVICTED`, `HEARTBEAT_FAILED`, `CONNECTION_CAP_EXCEEDED`, `CROSS_TENANT_BROADCAST_DENIED`, `RECONNECT_TOKEN_INVALID`, `BROKER_UNAVAILABLE`.
|
||||
- ADDED: `./testing` — `runWsEngineConformance(factory)` reusable harness.
|
||||
|
||||
## CapabilityId additions
|
||||
|
||||
This package contributes `"ws"` and `"ws.broadcast-cross-tenant"` to the `CapabilityId` union. The cross-tenant id is a separate capability so the manifest declares it explicitly and the host audit-logs every use (design §11.2 "audit-logged on every use").
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Concrete engines (`api-ws-bun`) — Stage 7.
|
||||
- Concrete brokers (`api-ws-broker-redis`, `api-ws-broker-in-memory`) — Stages 5 and 7.
|
||||
- The `graphql-ws` protocol wiring (subscriptions over WebSocket) — that lives in `api-graphql` engine adapters, which read this package's `SubscriptionBus`.
|
||||
|
||||
## Source mapping
|
||||
|
||||
Genesis-original. Source of design intent: `docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md`.
|
||||
|
||||
| Requirement | Design doc section |
|
||||
|---|---|
|
||||
| `WsConnection` shape + handshake flow | §11.1 |
|
||||
| `SubscriptionBus.broadcast` + filter predicate | §11.2 |
|
||||
| Multi-host fan-out via broker | §11.3 |
|
||||
| Backpressure (`maxQueuedMessages: 256`) | §11.4 |
|
||||
| Heartbeat (`heartbeatMs: 30000`, two missed pongs) | §11.4 |
|
||||
| Per-tenant caps (`maxConnectionsPerTenant`) | §11.4 |
|
||||
| Reconnect tokens (HMAC, 5-min TTL) | §11.5 |
|
||||
| Cross-tenant gate (`ws.broadcast-cross-tenant`) | §11.2 |
|
||||
|
||||
### Requirements with no source line
|
||||
|
||||
| Requirement | Justification |
|
||||
|---|---|
|
||||
| `runWsEngineConformance` harness | Stage-5 deliverable (§13.4). |
|
||||
|
||||
## Verification approach
|
||||
|
||||
- `src/connection/connection.spec-conformance.test.ts` — `expectTypeOf<WsConnection>()` declares every documented method.
|
||||
- `src/connection-registry/connection-registry.test.ts` — add/remove/lookup; multi-index correctness; count-by-tenant.
|
||||
- `src/subscription-bus/subscription-bus.test.ts` — subscribe/unsubscribe; broadcast fans out to matching `(topic, tenant)`; filter predicate applied per-connection.
|
||||
- `src/subscription-bus/cross-tenant.test.ts` — broadcast WITHOUT `ws.broadcast-cross-tenant` is rejected with `WS.CROSS_TENANT_BROADCAST_DENIED` when `tenant` arg is omitted or differs from emitter's tenant.
|
||||
- `src/broker/broker.spec-conformance.test.ts` — `WsBroker` interface declared.
|
||||
- `src/envelope/envelope.test.ts` — Zod accept/reject suite over the wire envelope.
|
||||
- `src/backpressure/backpressure.test.ts` — queue saturation triggers `BACKPRESSURE_EVICTED`.
|
||||
- `src/heartbeat/heartbeat.test.ts` — two missed pongs → close `ABNORMAL`.
|
||||
- `src/reconnect-token/reconnect-token.test.ts` — HMAC verify, TTL boundary.
|
||||
- `src/capability-id/capability-id.spec-conformance.test.ts` — both ids in `CapabilityId`.
|
||||
- `tests/dep-graph.test.ts` — deps: `@suaveplan/api-types`, `@suaveplan/error`, `@suaveplan/types`, `zod`. No transport deps.
|
||||
|
||||
## Migration impact
|
||||
|
||||
None — additive only.
|
||||
|
||||
## Impact
|
||||
|
||||
### Affected dependents
|
||||
|
||||
- (none) — [x] no action needed (reason: net-new; engines + brokers land Stage 5/7)
|
||||
202
openspec/archive/2026-05-25-api-ws/specs/api-ws/spec.md
Normal file
202
openspec/archive/2026-05-25-api-ws/specs/api-ws/spec.md
Normal file
@@ -0,0 +1,202 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: api-ws (api-ws)
|
||||
|
||||
**Package**: `@suaveplan/api-ws`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement carries a `**Verified by:**` line.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: WsConnection contract
|
||||
|
||||
The package SHALL export `WsConnection` with: `id` (ULID), `principal: Principal`, `tenantId: TenantId`, `lastEventId: string | null`, `state: "open" | "closing" | "closed"`, methods `subscribe(topic, opts?)`, `unsubscribe(topic)`, `send(message)`, `close(code, reason?)`, `ack(id)`. The principal MUST be frozen at handshake for the connection's lifetime; token expiry mid-connection fires `connection.principal_expired` (the host emits this; the contract surfaces it).
|
||||
|
||||
#### Scenario: every method present
|
||||
|
||||
- **WHEN** `expectTypeOf<WsConnection>()` is checked
|
||||
- **THEN** every documented method is declared with the documented signature
|
||||
|
||||
**Verified by:** `src/connection/connection.spec-conformance.test.ts::"WsConnection declares every documented method"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: `import z from "zod"` everywhere
|
||||
|
||||
Every Zod import MUST use the default form.
|
||||
|
||||
#### Scenario: no named import of z
|
||||
|
||||
- **WHEN** the test greps `import z` from `"zod"`
|
||||
- **THEN** zero matches
|
||||
|
||||
**Verified by:** `tests/zod-import-form.test.ts::"every src file uses default-import z"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: ConnectionRegistry multi-index
|
||||
|
||||
The package SHALL export `ConnectionRegistry` with O(1) lookup by `tenantId` and by `(topic, tenantId)`. `add`, `remove`, `lookupByTenant(tenant)`, `lookupByTopic(topic, tenant?)`, `countByTenant(tenant)`. `remove` MUST be idempotent (removing an absent connection is a no-op).
|
||||
|
||||
#### Scenario: lookup by tenant returns matching set
|
||||
|
||||
- **WHEN** 3 connections are added for tenant T1 and 2 for tenant T2
|
||||
- **THEN** `lookupByTenant("T1").length === 3` AND `lookupByTenant("T2").length === 2`
|
||||
|
||||
#### Scenario: remove is idempotent
|
||||
|
||||
- **WHEN** `remove(conn)` is called twice
|
||||
- **THEN** the second call returns without throwing
|
||||
|
||||
**Verified by:** `src/connection-registry/connection-registry.test.ts::"lookupByTenant returns matching set"` AND `src/connection-registry/connection-registry.test.ts::"remove is idempotent"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: SubscriptionBus broadcast with filter
|
||||
|
||||
The package SHALL export `SubscriptionBus.broadcast(topic, { tenant, payload, filter? })` which delivers the payload to every connection registered against `(topic, tenant)` whose optional `filter(payload)` returns true. The payload MUST be encoded once and sent to each matching connection (no per-connection re-encoding).
|
||||
|
||||
#### Scenario: broadcast reaches matching connections
|
||||
|
||||
- **WHEN** two connections subscribe to `orders.created` for tenant T
|
||||
- **THEN** `broadcast("orders.created", { tenant: T, payload })` reaches both
|
||||
|
||||
#### Scenario: filter excludes non-matching connections
|
||||
|
||||
- **WHEN** filter returns `false` for one of two matching connections
|
||||
- **THEN** only the connection where filter returns true receives the payload
|
||||
|
||||
**Verified by:** `src/subscription-bus/subscription-bus.test.ts::"broadcast reaches all matching connections"` AND `src/subscription-bus/subscription-bus.test.ts::"filter predicate excludes non-matching connections"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Cross-tenant broadcast requires explicit capability
|
||||
|
||||
`SubscriptionBus.broadcast(topic, { tenant, payload })` where `tenant` differs from the caller's `principal.tenantId` (or `tenant` is the wildcard "*") MUST throw `WS.CROSS_TENANT_BROADCAST_DENIED` unless the caller's `Caps` includes `"ws.broadcast-cross-tenant"`. Default is deny.
|
||||
|
||||
#### Scenario: same-tenant broadcast succeeds without capability
|
||||
|
||||
- **WHEN** caller's tenant is T AND broadcast is for tenant T
|
||||
- **THEN** broadcast succeeds
|
||||
|
||||
#### Scenario: cross-tenant denied by default
|
||||
|
||||
- **WHEN** caller's tenant is T AND broadcast is for tenant T2 AND `Caps` does NOT include `ws.broadcast-cross-tenant`
|
||||
- **THEN** broadcast throws `WS.CROSS_TENANT_BROADCAST_DENIED`
|
||||
|
||||
**Verified by:** `src/subscription-bus/cross-tenant.test.ts::"same-tenant broadcast succeeds without capability"` AND `src/subscription-bus/cross-tenant.test.ts::"cross-tenant denied without explicit capability"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: WsBroker contract
|
||||
|
||||
The package SHALL export `WsBroker` for multi-host fan-out: `publish(topic, envelope)`, `subscribe(topic, handler)`, `dispose()`. The broker MUST NOT migrate connections — it only makes broadcasts visible across host replicas. `dispose()` MUST close every subscription cleanly.
|
||||
|
||||
#### Scenario: every method present
|
||||
|
||||
- **WHEN** `expectTypeOf<WsBroker>()` is checked
|
||||
- **THEN** all three methods are declared
|
||||
|
||||
**Verified by:** `src/broker/broker.spec-conformance.test.ts::"WsBroker declares publish subscribe dispose"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Envelope Zod schema
|
||||
|
||||
The package SHALL export `wsEnvelopeSchema: z.ZodSchema<WsEnvelope>` describing the wire envelope: `{ id: string, type: string, topic?: string, payload?: unknown, ts?: string }`. Unknown top-level keys MUST be rejected.
|
||||
|
||||
#### Scenario: minimal envelope parses
|
||||
|
||||
- **WHEN** `{ id: "x", type: "msg" }` is parsed
|
||||
- **THEN** it succeeds
|
||||
|
||||
#### Scenario: extra key rejected
|
||||
|
||||
- **WHEN** `{ id: "x", type: "msg", extra: 1 }` is parsed
|
||||
- **THEN** Zod reports an issue at `extra`
|
||||
|
||||
**Verified by:** `src/envelope/envelope.test.ts::"minimal envelope parses"` AND `src/envelope/envelope.test.ts::"unknown keys rejected"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Backpressure policy
|
||||
|
||||
The package SHALL export `BackpressurePolicy` with default `maxQueuedMessages = 256`. When a connection's send queue exceeds the cap, the connection MUST receive a `BACKPRESSURE_EVICTED` close frame and be dropped from the registry.
|
||||
|
||||
#### Scenario: queue saturation triggers eviction
|
||||
|
||||
- **WHEN** 257 messages are queued for a connection with default policy
|
||||
- **THEN** the eviction handler fires AND `WS.BACKPRESSURE_EVICTED` is the close code
|
||||
|
||||
**Verified by:** `src/backpressure/backpressure.test.ts::"queue saturation triggers BACKPRESSURE_EVICTED"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Heartbeat policy
|
||||
|
||||
The package SHALL export `HeartbeatPolicy` with default `heartbeatMs = 30000`. Two missed pongs MUST trigger `onClose` with code `ABNORMAL`.
|
||||
|
||||
#### Scenario: two missed pongs trigger close
|
||||
|
||||
- **WHEN** a fake clock advances past 2 × heartbeatMs without pong
|
||||
- **THEN** the close handler fires with `code === "ABNORMAL"` AND `WS.HEARTBEAT_FAILED` is emitted
|
||||
|
||||
**Verified by:** `src/heartbeat/heartbeat.test.ts::"two missed pongs trigger ABNORMAL close"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Reconnect-token shape
|
||||
|
||||
The package SHALL export `ReconnectToken` HMAC envelope carrying `{ tenant, principalId, lastEventId, exp }` with default 5-minute TTL. The HMAC verifier MUST reject expired tokens (`WS.RECONNECT_TOKEN_INVALID`) and tokens with bad MAC.
|
||||
|
||||
#### Scenario: valid token verifies
|
||||
|
||||
- **WHEN** a token signed within TTL is verified
|
||||
- **THEN** the verifier returns the parsed payload
|
||||
|
||||
#### Scenario: expired token rejected
|
||||
|
||||
- **WHEN** the fake clock advances past TTL
|
||||
- **THEN** the verifier throws `WS.RECONNECT_TOKEN_INVALID`
|
||||
|
||||
**Verified by:** `src/reconnect-token/reconnect-token.test.ts::"valid token verifies"` AND `src/reconnect-token/reconnect-token.test.ts::"expired token rejected"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: CapabilityId augmentation contributes "ws" and "ws.broadcast-cross-tenant"
|
||||
|
||||
The package SHALL augment the `CapabilityId` union to include both ids.
|
||||
|
||||
#### Scenario: both ids assignable to CapabilityId
|
||||
|
||||
- **WHEN** `expectTypeOf<"ws">().toMatchTypeOf<CapabilityId>()` AND `expectTypeOf<"ws.broadcast-cross-tenant">().toMatchTypeOf<CapabilityId>()` are checked
|
||||
- **THEN** both pass
|
||||
|
||||
**Verified by:** `src/capability-id/capability-id.spec-conformance.test.ts::"ws and ws.broadcast-cross-tenant are valid CapabilityIds"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Error codes registry frozen
|
||||
|
||||
The package SHALL export `WS_CODES` containing exactly: `HANDSHAKE_FAILED`, `PRINCIPAL_EXPIRED`, `BACKPRESSURE_EVICTED`, `HEARTBEAT_FAILED`, `CONNECTION_CAP_EXCEEDED`, `CROSS_TENANT_BROADCAST_DENIED`, `RECONNECT_TOKEN_INVALID`, `BROKER_UNAVAILABLE`.
|
||||
|
||||
#### Scenario: every code reachable
|
||||
|
||||
- **WHEN** the test enumerates `WS_CODES`
|
||||
- **THEN** every code has a production throw site
|
||||
|
||||
**Verified by:** `src/error-codes/error-codes.spec-conformance.test.ts::"every WS_CODES entry thrown by production path"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Conformance harness for engines
|
||||
|
||||
The package SHALL export `runWsEngineConformance({ factory, label })`. Every concrete engine + broker pair MUST pass.
|
||||
|
||||
#### Scenario: harness covers handshake, subscribe, broadcast, backpressure, heartbeat, reconnect
|
||||
|
||||
- **WHEN** the harness is run against a stub engine
|
||||
- **THEN** test categories exist for every documented behavior
|
||||
|
||||
**Verified by:** `src/testing/conformance.test.ts::"harness covers handshake subscribe broadcast backpressure heartbeat reconnect"`
|
||||
74
openspec/archive/2026-05-25-api-ws/tasks.md
Normal file
74
openspec/archive/2026-05-25-api-ws/tasks.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# Tasks — `@suaveplan/api-ws`
|
||||
|
||||
> Every checkbox MUST reflect work actually done in this change.
|
||||
|
||||
## 0. Pre-flight (non-negotiable)
|
||||
|
||||
- [x] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work (`git worktree list` then `git status` in each)
|
||||
- [x] 0.2 If any worktrees exist under `.claude/worktrees/`, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (`git worktree remove`) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
|
||||
- [x] 0.3 Confirm the package set in `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count. Drift here means a prior change archived without regenerating the manifest.
|
||||
|
||||
## 1. Spec authoring
|
||||
|
||||
- [x] 1.1 Read design doc §11.1 (lifecycle), §11.2 (bus), §11.3 (multi-host), §11.4 (backpressure/heartbeat), §11.5 (reconnect), §11.8 (worker tier)
|
||||
- [x] 1.2 Read `openspec/specs/api-ws/spec.md` (does not exist yet)
|
||||
- [x] 1.3 Read `openspec/AGENTS.md` Sections 13, 14
|
||||
- [x] 1.4 Draft `proposal.md`
|
||||
- [x] 1.5 Source-mapping table
|
||||
- [x] 1.6 Draft `specs/api-ws/spec.md`
|
||||
- [x] 1.7 `**Verified by:**` line on every requirement
|
||||
|
||||
## 2. Implementation
|
||||
|
||||
- [x] 2.1 Scaffold `packages/api/api-ws/` (`"version": "0.1.0"`, `"sideEffects": false`, deps: `@suaveplan/api-types`, `@suaveplan/error`, `@suaveplan/types`, `zod`)
|
||||
- [x] 2.2 `src/connection/` — `WsConnection` interface
|
||||
- [x] 2.3 `src/connection-registry/` — `ConnectionRegistry`
|
||||
- [x] 2.4 `src/subscription-bus/` — `SubscriptionBus` + cross-tenant gate
|
||||
- [x] 2.5 `src/broker/` — `WsBroker`
|
||||
- [x] 2.6 `src/envelope/` — Zod schema (always `import z from "zod"`)
|
||||
- [x] 2.7 `src/backpressure/` — `BackpressurePolicy` (constants + helper)
|
||||
- [x] 2.8 `src/heartbeat/` — `HeartbeatPolicy`
|
||||
- [x] 2.9 `src/reconnect-token/` — `ReconnectToken` shape + HMAC verify helper
|
||||
- [x] 2.10 `src/capability-id/` — augmentation file adding `"ws"` + `"ws.broadcast-cross-tenant"`
|
||||
- [x] 2.11 `src/error-codes/` — `WS_CODES`
|
||||
- [x] 2.12 `src/testing/` — `runWsEngineConformance(factory)`
|
||||
- [x] 2.13 `src/index.ts` — explicit named re-exports
|
||||
- [x] 2.14 `package.json` `exports` per subpath
|
||||
|
||||
## 3. Tests
|
||||
|
||||
- [x] 3.1 Unit tests per feature
|
||||
- [x] 3.2 Spec-conformance type tests
|
||||
- [x] 3.3 Property test on connection-registry multi-index lookup
|
||||
- [x] 3.4 Every `it(...)` carries `{ timeout: 5000 }`
|
||||
|
||||
## 4. Documentation
|
||||
|
||||
- [x] 4.1 Sibling `.md` for every implementation `.ts`
|
||||
- [x] 4.2 `README.md` linking every `.md`
|
||||
- [x] 4.3 No CLAUDE.md update
|
||||
|
||||
## 5. Pre-archive verification
|
||||
|
||||
- [x] 5.1 `bun run check` — 0/0/0
|
||||
- [x] 5.2 `bunx turbo lint typecheck test --filter=@suaveplan/api-ws` — green
|
||||
- [x] 5.3 `bunx turbo test:coverage --filter=@suaveplan/api-ws` — 100/100/100/100
|
||||
- [x] 5.4 `bun scripts/openspec-conformance.ts @suaveplan/api-ws` — exit 0
|
||||
- [x] 5.5 `bunx turbo build --filter=@suaveplan/api-ws` — green
|
||||
|
||||
## 6. Pre-archive 7-row checklist
|
||||
|
||||
- [x] 6.1 Generics on `SubscriptionBus<Payload>`; `expectTypeOf` on every literal-type claim
|
||||
- [x] 6.2 Zod wired at the envelope boundary
|
||||
- [x] 6.3 Every `WS_CODES` entry thrown
|
||||
- [x] 6.4 Backpressure / heartbeat strict-mode behaviors tested on / off
|
||||
- [x] 6.5 OTel — N/A (engines emit)
|
||||
- [x] 6.6 Lifecycle dedup — connection registration is idempotent; tested
|
||||
- [x] 6.7 No retroactive tick
|
||||
|
||||
## 7. Archive
|
||||
|
||||
- [x] 7.1 Fold spec delta
|
||||
- [x] 7.2 Move to `openspec/archive/<yyyy-mm-dd>-api-ws/`
|
||||
- [x] 7.3 Tick umbrella `1.5`
|
||||
- [x] 7.4 Refresh `MANIFEST.md`
|
||||
Reference in New Issue
Block a user