openvibers/openvibe.network Commits
Plan T2, resource registry SLICES 3 and 4 together (#9)
Changes
@@@ -189,6 +189,37 @@ code. The contract between the two is therefore exact:- `capabilities[]` items are `namespace:name` strings (`node:http`, `events:gateway`, `object:r2`, …) — the contract'sown pattern; they are stored, not interpreted.++### Worked example — `GET /api/v1/resources?kind=provider` → `plan()`++++Two provider offers in one region, both `health.status: 'up'` and priced per request at `$0.02` and `$0.03`:++++```json++GET /api/v1/resources?kind=provider++{ "offers": [++ { "offer_id": "p-provider", "kind": "provider", "region": "us-central", "trust": "community",++ "health": { "status": "up" }, "pricing": { "model": "per-request", "marginal_usd_per_unit": 0.02 } },++ { "offer_id": "p-provider-b", "kind": "provider", "region": "us-central", "trust": "community",++ "health": { "status": "up" }, "pricing": { "model": "per-request", "marginal_usd_per_unit": 0.03 } }++ ] }++```++++That `offers` array goes to `plan()` unchanged:++++```js++const req = { kind: 'request', mobility: 'request', latency_class: 'interactive',++ objective: 'cheapest', region: 'us-central', capabilities: [], units: 1 };++const result = require('openvibe-sdk/placement').plan(req, response.offers, { now });++// → { selected: 'p-provider', objective: 'cheapest',++// reasons: ['objective cheapest'], candidates: [ … ], decided_at: '<now as ISO>' }++```++++The fields that matter: `selected` is the cheaper eligible offer (`p-provider`, `$0.02` < `$0.03`); every offer is++listed in `candidates` with `eligible: true` and its `estimated_cost_usd` (from `pricing.marginal_usd_per_unit`,++since the public list has no `capacity` to price prepaid capacity from); and the whole result validates as++`platform.placement-result@1`. `test/resource-registry.test.js` test 9 does exactly this — reporting a node offer++at `$0.05` alongside the two providers and feeding the **unfiltered** list, with `region: 'us-central'` narrowing++eligibility to the three — and asserts both the contract validity and the same winner.++## 6. Tests — `test/resource-registry.test.js`Modelled on `test/nodes.test.js`: temp dir + `getDb()`, `oauth_clients.client_secret` set for `host` and `live`, a@@@ -234,10 +265,10 @@ filter path — PGlite and a real PostgreSQL must agree on it — and `npm test`2. **Public read + filters.** `list()`, `get()`, `routers().pub` in `server/registry/offers.js`,`app.use('/api/v1/resources', pub)`, tests 5-7. Also the full internal read (`GET /internal/resources[/:id]`, thereport's guard), because the public read leaves capacity out (§4).--3. **Discovery entry.** Add `resources: '/api/v1/resources'` to the `/api/v1/registry` index in++3. **Discovery entry.** (done) Add `resources: '/api/v1/resources'` to the `/api/v1/registry` index in`server/registry/ecosystem.js`. Nothing else.--4. **Placement consumption proof.** §5 gains a worked example; test 9 added. No production-code change — this slice-- exists to fail loudly if the response shape drifts from what `plan()` accepts.++4. **Placement consumption proof.** (done) §5 gains a worked example; test 9 added. No production-code change — this++ slice exists to fail loudly if the response shape drifts from what `plan()` accepts.5. **Contracts bump + kind widening.** *Blocked on T1.* `package.json:30` pin bump, a`node -e "validate('platform.storage-offer@1', …)"` smoke check, `migrations/0009_resource_offer_kinds.sql` wideningthe `kind` CHECK to the published kinds, the one-line capability switch, a `KIND → CONTRACT` map used for per-kind@@@ -28,6 +28,8 @@* library, repository, planned), each rule stated* GET /api/v1/registry/featured public sites that are up, most used first (the navbar's* usage ranking), with when that use was counted++ * GET /api/v1/resources[?kind=®ion=&trust=&status=&cell=&max_price_usd=]++ * resource offers (server/registry/offers.js)* GET /contracts/<domain>/<name>.v<N>.json the schema at its $id URL*/const express = require('express');@@@ -324,7 +326,7 @@ function createEcosystemRegistry({ issuer, internalOverrides = {}, fetchImpl = gconst notFound = (res, code, detail) => contracts.http.sendProblem(res, 404, code, { detail });r.get('/.well-known/openvibe', (_req, res) => cache(res, 300).json(descriptor()));-- r.get('/api/v1/registry', (_req, res) => cache(res).json({ services: '/api/v1/registry/services', capabilities: '/api/v1/registry/capabilities', namespaces: '/api/v1/registry/namespaces', contracts: '/api/v1/registry/contracts', topics: '/api/v1/registry/topics', releases: '/api/v1/registry/releases', health: '/api/v1/registry/health', search: '/api/v1/registry/search?q=', domains: '/api/v1/registry/domains/:domain', categories: '/api/v1/registry/categories', featured: '/api/v1/registry/featured' }));++ r.get('/api/v1/registry', (_req, res) => cache(res).json({ services: '/api/v1/registry/services', capabilities: '/api/v1/registry/capabilities', namespaces: '/api/v1/registry/namespaces', contracts: '/api/v1/registry/contracts', topics: '/api/v1/registry/topics', releases: '/api/v1/registry/releases', health: '/api/v1/registry/health', search: '/api/v1/registry/search?q=', domains: '/api/v1/registry/domains/:domain', categories: '/api/v1/registry/categories', featured: '/api/v1/registry/featured', resources: '/api/v1/resources' }));r.get('/api/v1/registry/categories', (_req, res) => cache(res, 30).json({ categories: categories(), checked_at: lastPollAt ? new Date(lastPollAt).toISOString() : null }));r.get('/api/v1/registry/categories/:id', (req, res) => {const c = categories().find(x => x.id === String(req.params.id));@@@ -36,6 +36,7 @@ app.use('/oauth', require('../server/auth/oauth-routes'));const r = offers.routers({ guard: principals.guard('network.node.report', { legacy: false }) });app.use('/api/v1/resources', r.pub);app.use('/internal/resources', r.internal);++app.use(require('../server/registry/ecosystem').createEcosystemRegistry().router());const server = http.createServer(app);const offer = (id, extra = {}) => ({ offer_id: id, kind: 'node', node_id: id, provider: 'ovh', region: 'us-west', cell: 'wnam-1', trust: 'first-party', capabilities: ['node:http', 'events:gateway'], capacity: { cpu: { utilization: 0.2, available_cores: 6 } }, health: { status: 'up', checked_at: '2026-10-01T12:00:00Z' }, pricing: { model: 'prepaid', marginal_usd_per_unit: 0.01, unit: 'request' }, updated_at: '2026-10-01T12:00:00Z', ...extra });@@@ -234,6 +235,26 @@ const assertLockstep = async () => {assert.strictEqual(x.headers.get('cache-control'), 'no-store');assert.strictEqual(x.headers.get('access-control-allow-origin'), '*');assert.strictEqual(x.headers.get('timing-allow-origin'), '*');++++ // 9. Placement proof (slice 4): the public list feeds openvibe-sdk/placement unchanged. Three up offers in++ // their own region — two providers and a node, different marginal prices; `cheapest` must pick the cheapest++ // eligible one from a result that matches platform.placement-result@1.++ x = await post({ source: 'proof', offers: [++ offer('p-node', { region: 'us-central', trust: 'community', pricing: { model: 'per-request', marginal_usd_per_unit: 0.05, unit: 'request' } }),++ offer('p-provider', { kind: 'provider', node_id: undefined, adapter: 'nats-v1', region: 'us-central', trust: 'community', pricing: { model: 'per-request', marginal_usd_per_unit: 0.02, unit: 'request' } }),++ offer('p-provider-b', { kind: 'provider', node_id: undefined, adapter: 'http-v1', region: 'us-central', trust: 'community', pricing: { model: 'per-request', marginal_usd_per_unit: 0.03, unit: 'request' } }),++ ] }, auth);++ assert.strictEqual(x.status, 200, JSON.stringify(x.body));++ const idx = await get('/api/v1/registry');++ assert.strictEqual(idx.body.resources, '/api/v1/resources', 'the registry index advertises the resource list');++ const pub = await get('/api/v1/resources');++ assert.strictEqual(pub.status, 200);++ const req = { kind: 'request', mobility: 'request', latency_class: 'interactive', objective: 'cheapest', region: 'us-central', capabilities: [], units: 1 };++ const result = require('openvibe-sdk/placement').plan(req, pub.body.offers, { now });++ const check = validate('platform.placement-result@1', result);++ assert.ok(check.valid, JSON.stringify(check.errors));++ assert.strictEqual(result.selected, 'p-provider', 'the cheaper eligible offer wins');++ assert.deepStrictEqual(result.candidates.filter((c) => c.eligible).map((c) => c.id).sort(), ['p-node', 'p-provider', 'p-provider-b']);console.log('resource-registry: all tests passed');} finally {server.close();
@@ -189,6 +189,37 @@ code. The contract between the two is therefore exact:
@@ -189,6 +189,37 @@ code. The contract between the two is therefore exact:
- `capabilities[]` items are `namespace:name` strings (`node:http`, `events:gateway`, `object:r2`, …) — the contract's
- `capabilities[]` items are `namespace:name` strings (`node:http`, `events:gateway`, `object:r2`, …) — the contract's
own pattern; they are stored, not interpreted.
own pattern; they are stored, not interpreted.
+### Worked example — `GET /api/v1/resources?kind=provider` → `plan()`
+
+Two provider offers in one region, both `health.status: 'up'` and priced per request at `$0.02` and `$0.03`:
+
+```json
+GET /api/v1/resources?kind=provider
+{ "offers": [
+ { "offer_id": "p-provider", "kind": "provider", "region": "us-central", "trust": "community",
+ "health": { "status": "up" }, "pricing": { "model": "per-request", "marginal_usd_per_unit": 0.02 } },
+ { "offer_id": "p-provider-b", "kind": "provider", "region": "us-central", "trust": "community",
+ "health": { "status": "up" }, "pricing": { "model": "per-request", "marginal_usd_per_unit": 0.03 } }
+ ] }
+```
+
+That `offers` array goes to `plan()` unchanged:
+
+```js
+const req = { kind: 'request', mobility: 'request', latency_class: 'interactive',
+ objective: 'cheapest', region: 'us-central', capabilities: [], units: 1 };
+const result = require('openvibe-sdk/placement').plan(req, response.offers, { now });
+// → { selected: 'p-provider', objective: 'cheapest',
+// reasons: ['objective cheapest'], candidates: [ … ], decided_at: '<now as ISO>' }
+```
+
+The fields that matter: `selected` is the cheaper eligible offer (`p-provider`, `$0.02` < `$0.03`); every offer is
+listed in `candidates` with `eligible: true` and its `estimated_cost_usd` (from `pricing.marginal_usd_per_unit`,
+since the public list has no `capacity` to price prepaid capacity from); and the whole result validates as
+`platform.placement-result@1`. `test/resource-registry.test.js` test 9 does exactly this — reporting a node offer
+at `$0.05` alongside the two providers and feeding the **unfiltered** list, with `region: 'us-central'` narrowing
+eligibility to the three — and asserts both the contract validity and the same winner.
+
## 6. Tests — `test/resource-registry.test.js`
## 6. Tests — `test/resource-registry.test.js`
Modelled on `test/nodes.test.js`: temp dir + `getDb()`, `oauth_clients.client_secret` set for `host` and `live`, a
Modelled on `test/nodes.test.js`: temp dir + `getDb()`, `oauth_clients.client_secret` set for `host` and `live`, a
@@ -234,10 +265,10 @@ filter path — PGlite and a real PostgreSQL must agree on it — and `npm test`
@@ -234,10 +265,10 @@ filter path — PGlite and a real PostgreSQL must agree on it — and `npm test`
2. **Public read + filters.** `list()`, `get()`, `routers().pub` in `server/registry/offers.js`,
2. **Public read + filters.** `list()`, `get()`, `routers().pub` in `server/registry/offers.js`,
`app.use('/api/v1/resources', pub)`, tests 5-7. Also the full internal read (`GET /internal/resources[/:id]`, the
`app.use('/api/v1/resources', pub)`, tests 5-7. Also the full internal read (`GET /internal/resources[/:id]`, the
report's guard), because the public read leaves capacity out (§4).
report's guard), because the public read leaves capacity out (§4).
-3. **Discovery entry.** Add `resources: '/api/v1/resources'` to the `/api/v1/registry` index in
+3. **Discovery entry.** (done) Add `resources: '/api/v1/resources'` to the `/api/v1/registry` index in
`server/registry/ecosystem.js`. Nothing else.
`server/registry/ecosystem.js`. Nothing else.
-4. **Placement consumption proof.** §5 gains a worked example; test 9 added. No production-code change — this slice
- exists to fail loudly if the response shape drifts from what `plan()` accepts.
+4. **Placement consumption proof.** (done) §5 gains a worked example; test 9 added. No production-code change — this
+ slice exists to fail loudly if the response shape drifts from what `plan()` accepts.
5. **Contracts bump + kind widening.** *Blocked on T1.* `package.json:30` pin bump, a
5. **Contracts bump + kind widening.** *Blocked on T1.* `package.json:30` pin bump, a
`node -e "validate('platform.storage-offer@1', …)"` smoke check, `migrations/0009_resource_offer_kinds.sql` widening
`node -e "validate('platform.storage-offer@1', …)"` smoke check, `migrations/0009_resource_offer_kinds.sql` widening
the `kind` CHECK to the published kinds, the one-line capability switch, a `KIND → CONTRACT` map used for per-kind
the `kind` CHECK to the published kinds, the one-line capability switch, a `KIND → CONTRACT` map used for per-kind
@@ -28,6 +28,8 @@
@@ -28,6 +28,8 @@
* library, repository, planned), each rule stated
* library, repository, planned), each rule stated
* GET /api/v1/registry/featured public sites that are up, most used first (the navbar's
* GET /api/v1/registry/featured public sites that are up, most used first (the navbar's
* usage ranking), with when that use was counted
* usage ranking), with when that use was counted
+ * GET /api/v1/resources[?kind=®ion=&trust=&status=&cell=&max_price_usd=]
+ * resource offers (server/registry/offers.js)
* GET /contracts/<domain>/<name>.v<N>.json the schema at its $id URL
* GET /contracts/<domain>/<name>.v<N>.json the schema at its $id URL
*/
*/
const express = require('express');
const express = require('express');
@@ -324,7 +326,7 @@ function createEcosystemRegistry({ issuer, internalOverrides = {}, fetchImpl = g
@@ -324,7 +326,7 @@ function createEcosystemRegistry({ issuer, internalOverrides = {}, fetchImpl = g
const notFound = (res, code, detail) => contracts.http.sendProblem(res, 404, code, { detail });
const notFound = (res, code, detail) => contracts.http.sendProblem(res, 404, code, { detail });
r.get('/.well-known/openvibe', (_req, res) => cache(res, 300).json(descriptor()));
r.get('/.well-known/openvibe', (_req, res) => cache(res, 300).json(descriptor()));
- r.get('/api/v1/registry', (_req, res) => cache(res).json({ services: '/api/v1/registry/services', capabilities: '/api/v1/registry/capabilities', namespaces: '/api/v1/registry/namespaces', contracts: '/api/v1/registry/contracts', topics: '/api/v1/registry/topics', releases: '/api/v1/registry/releases', health: '/api/v1/registry/health', search: '/api/v1/registry/search?q=', domains: '/api/v1/registry/domains/:domain', categories: '/api/v1/registry/categories', featured: '/api/v1/registry/featured' }));
+ r.get('/api/v1/registry', (_req, res) => cache(res).json({ services: '/api/v1/registry/services', capabilities: '/api/v1/registry/capabilities', namespaces: '/api/v1/registry/namespaces', contracts: '/api/v1/registry/contracts', topics: '/api/v1/registry/topics', releases: '/api/v1/registry/releases', health: '/api/v1/registry/health', search: '/api/v1/registry/search?q=', domains: '/api/v1/registry/domains/:domain', categories: '/api/v1/registry/categories', featured: '/api/v1/registry/featured', resources: '/api/v1/resources' }));
r.get('/api/v1/registry/categories', (_req, res) => cache(res, 30).json({ categories: categories(), checked_at: lastPollAt ? new Date(lastPollAt).toISOString() : null }));
r.get('/api/v1/registry/categories', (_req, res) => cache(res, 30).json({ categories: categories(), checked_at: lastPollAt ? new Date(lastPollAt).toISOString() : null }));
r.get('/api/v1/registry/categories/:id', (req, res) => {
r.get('/api/v1/registry/categories/:id', (req, res) => {
const c = categories().find(x => x.id === String(req.params.id));
const c = categories().find(x => x.id === String(req.params.id));
@@ -36,6 +36,7 @@ app.use('/oauth', require('../server/auth/oauth-routes'));
@@ -36,6 +36,7 @@ app.use('/oauth', require('../server/auth/oauth-routes'));
const r = offers.routers({ guard: principals.guard('network.node.report', { legacy: false }) });
const r = offers.routers({ guard: principals.guard('network.node.report', { legacy: false }) });
app.use('/api/v1/resources', r.pub);
app.use('/api/v1/resources', r.pub);
app.use('/internal/resources', r.internal);
app.use('/internal/resources', r.internal);
+app.use(require('../server/registry/ecosystem').createEcosystemRegistry().router());
const server = http.createServer(app);
const server = http.createServer(app);
const offer = (id, extra = {}) => ({ offer_id: id, kind: 'node', node_id: id, provider: 'ovh', region: 'us-west', cell: 'wnam-1', trust: 'first-party', capabilities: ['node:http', 'events:gateway'], capacity: { cpu: { utilization: 0.2, available_cores: 6 } }, health: { status: 'up', checked_at: '2026-10-01T12:00:00Z' }, pricing: { model: 'prepaid', marginal_usd_per_unit: 0.01, unit: 'request' }, updated_at: '2026-10-01T12:00:00Z', ...extra });
const offer = (id, extra = {}) => ({ offer_id: id, kind: 'node', node_id: id, provider: 'ovh', region: 'us-west', cell: 'wnam-1', trust: 'first-party', capabilities: ['node:http', 'events:gateway'], capacity: { cpu: { utilization: 0.2, available_cores: 6 } }, health: { status: 'up', checked_at: '2026-10-01T12:00:00Z' }, pricing: { model: 'prepaid', marginal_usd_per_unit: 0.01, unit: 'request' }, updated_at: '2026-10-01T12:00:00Z', ...extra });
@@ -234,6 +235,26 @@ const assertLockstep = async () => {
@@ -234,6 +235,26 @@ const assertLockstep = async () => {
assert.strictEqual(x.headers.get('cache-control'), 'no-store');
assert.strictEqual(x.headers.get('cache-control'), 'no-store');
assert.strictEqual(x.headers.get('access-control-allow-origin'), '*');
assert.strictEqual(x.headers.get('access-control-allow-origin'), '*');
assert.strictEqual(x.headers.get('timing-allow-origin'), '*');
assert.strictEqual(x.headers.get('timing-allow-origin'), '*');
+
+ // 9. Placement proof (slice 4): the public list feeds openvibe-sdk/placement unchanged. Three up offers in
+ // their own region — two providers and a node, different marginal prices; `cheapest` must pick the cheapest
+ // eligible one from a result that matches platform.placement-result@1.
+ x = await post({ source: 'proof', offers: [
+ offer('p-node', { region: 'us-central', trust: 'community', pricing: { model: 'per-request', marginal_usd_per_unit: 0.05, unit: 'request' } }),
+ offer('p-provider', { kind: 'provider', node_id: undefined, adapter: 'nats-v1', region: 'us-central', trust: 'community', pricing: { model: 'per-request', marginal_usd_per_unit: 0.02, unit: 'request' } }),
+ offer('p-provider-b', { kind: 'provider', node_id: undefined, adapter: 'http-v1', region: 'us-central', trust: 'community', pricing: { model: 'per-request', marginal_usd_per_unit: 0.03, unit: 'request' } }),
+ ] }, auth);
+ assert.strictEqual(x.status, 200, JSON.stringify(x.body));
+ const idx = await get('/api/v1/registry');
+ assert.strictEqual(idx.body.resources, '/api/v1/resources', 'the registry index advertises the resource list');
+ const pub = await get('/api/v1/resources');
+ assert.strictEqual(pub.status, 200);
+ const req = { kind: 'request', mobility: 'request', latency_class: 'interactive', objective: 'cheapest', region: 'us-central', capabilities: [], units: 1 };
+ const result = require('openvibe-sdk/placement').plan(req, pub.body.offers, { now });
+ const check = validate('platform.placement-result@1', result);
+ assert.ok(check.valid, JSON.stringify(check.errors));
+ assert.strictEqual(result.selected, 'p-provider', 'the cheaper eligible offer wins');
+ assert.deepStrictEqual(result.candidates.filter((c) => c.eligible).map((c) => c.id).sort(), ['p-node', 'p-provider', 'p-provider-b']);
console.log('resource-registry: all tests passed');
console.log('resource-registry: all tests passed');
} finally {
} finally {
server.close();
server.close();