openvibers/openvibe.contracts Commits
Capability schemas (WS-C task 4): common.no-body@1 and common.binary@1 conventions, media.file-upload@1, Media and Tools capabilities complete; a ratchet fails on any active capability without schemas outside compatibility/capability-schema-gaps.json (162, only shrinking) (unreleased)
Changes
@@@ -6,7 +6,7 @@ the notes were in the tag and commit messages (`git tag -n1`).## Unreleased--Capability schemas (roadmap WS-C task 4), starting with Search: **`search.query@1`** (the query string of `GET /api/v1/search` and `/suggest`: q, owner, type, lang, facets, limit, cursor, `facet.<key>`), **`search.query-result@1`** (a page of hits the caller may see, `next_cursor`, facet counts; never an ACL or a total) and **`search.write-result@1`** (`PUT /api/v1/documents/…`: applied or unchanged; stale and conflicting revisions are 409 problems). `search.query.run` and `search.query.delegate` name the first two; `search.document.write` names the third as its output. Fixtures for each. Additive.++Capability schemas (roadmap WS-C task 4), starting with Search: **`search.query@1`** (the query string of `GET /api/v1/search` and `/suggest`: q, owner, type, lang, facets, limit, cursor, `facet.<key>`), **`search.query-result@1`** (a page of hits the caller may see, `next_cursor`, facet counts; never an ACL or a total) and **`search.write-result@1`** (`PUT /api/v1/documents/…`: applied or unchanged; stale and conflicting revisions are 409 problems). `search.query.run` and `search.query.delegate` name the first two; `search.document.write` names the third as its output. Fixtures for each. Two conventions so every capability can name schemas: **`common.no-body@1`** (a request whose parameters are the path and query) and **`common.binary@1`** (a response of bytes); **`media.file-upload@1`** is the multipart body of `POST /api/v1/:app/files`. The Media and Tools capabilities name them. **A ratchet**: `test/capability-schemas.test.js` fails on an active capability without an input or output schema unless it is in `compatibility/capability-schema-gaps.json` (162 today), and on a listed gap that has been filled; the list only shrinks. Additive.## 0.49.0 — 2026-09-25nnew file mode 100644@@@ -0,0 +1,1122 @@++{++ "_about": "Active capabilities still without an input or output schema (roadmap WS-C task 4). test/capability-schemas.test.js fails on any active capability missing a schema that is not listed here, and on a listed one that has since gained it (remove it from this list). The list only shrinks: fill a capability's schemas, then delete its row. A request with no body names common.no-body@1; a response of bytes names common.binary@1.",++ "gaps": [++ {++ "id": "ai.provider.manage",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "ai.run.read",++ "missing": [++ "inputSchema"++ ]++ },++ {++ "id": "ai.usage.read",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "ai.workflow.manage",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "billing.balance.read",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "billing.cashout.manage",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "billing.cashout.request",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "billing.entitlement.check",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "billing.intent.create",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "billing.ledger.admin",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "billing.subscription.manage",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "billing.transfer.create",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "blog.blog.configure",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "blog.blog.create",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "blog.feed.read",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "blog.member.manage",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "blog.post.create",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "blog.post.delete",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "blog.post.publish",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "blog.post.read",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "blog.post.schedule",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "blog.post.unpublish",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "blog.post.update",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "blog.theme.set",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "chat.message.send",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "codes.release.manage",++ "missing": [++ "outputSchema"++ ]++ },++ {++ "id": "codes.release.read",++ "missing": [++ "inputSchema"++ ]++ },++ {++ "id": "community.comment.moderate",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "community.paste.create",++ "missing": [++ "inputSchema"++ ]++ },++ {++ "id": "community.paste.moderate",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "community.paste.write",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "community.post.create",++ "missing": [++ "inputSchema"++ ]++ },++ {++ "id": "community.pulse.read",++ "missing": [++ "inputSchema"++ ]++ },++ {++ "id": "community.pulse.write",++ "missing": [++ "outputSchema"++ ]++ },++ {++ "id": "community.space.read",++ "missing": [++ "inputSchema"++ ]++ },++ {++ "id": "community.thread.read",++ "missing": [++ "inputSchema"++ ]++ },++ {++ "id": "coupons.coupon.lookup",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "coupons.coupon.submit",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "coupons.merchant.manage",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "coupons.merchant.resolve",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "coupons.report.create",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "coupons.status.update",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "deals.flag.create",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "deals.offer.expire",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "deals.offer.merge",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "deals.offer.moderate",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "deals.offer.submit",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "deals.offer.update",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "deals.product.resolve",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "deals.vote.remove",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "deals.vote.set",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "deals.watch.create",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "deals.watch.delete",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "deals.watch.read",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "events.app.publish",++ "missing": [++ "outputSchema"++ ]++ },++ {++ "id": "events.app.read",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "events.app.subscribe",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "events.delivery.admin",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "events.event.publish",++ "missing": [++ "outputSchema"++ ]++ },++ {++ "id": "events.event.read",++ "missing": [++ "inputSchema"++ ]++ },++ {++ "id": "events.subscription.manage",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "games.mod.manage",++ "missing": [++ "outputSchema"++ ]++ },++ {++ "id": "games.mod.read",++ "missing": [++ "inputSchema"++ ]++ },++ {++ "id": "games.prop.place",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "games.world.announce",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "host.deploy.create",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "host.domain.manage",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "host.site.manage",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "live.channel.read",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "live.discovery.read",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "live.follower.read",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "live.stream.read",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "live.tips_delivery.write",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "network.blocks.read",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "network.coins.credit",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "network.coins.debit",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "network.coins.transfer",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "network.integration.github.read",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "network.modules.read",++ "missing": [++ "inputSchema"++ ]++ },++ {++ "id": "network.notifications.push",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "network.staff.read",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "news.cluster.manage",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "news.cluster.read",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "news.perspective.update",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "news.source.attach",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "news.story.create",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "news.story.publish",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "news.story.read",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "news.story.retract",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "news.story.revise",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "news.timeline.update",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "news.topic.manage",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "openre.key.rotate",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "openre.output.read",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "openre.output.write",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "openre.session.end",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "openre.session.read",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "openre.stream.read",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "openre.stream.write",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "reviews.correction.submit",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "reviews.entity.manage",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "reviews.entity.merge",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "reviews.entity.resolve",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "reviews.entity.split",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "reviews.signal.import",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "reviews.summary.propose",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "reviews.summary.publish",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "sources.item.read",++ "missing": [++ "inputSchema"++ ]++ },++ {++ "id": "sources.source.manage",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "sources.source.read",++ "missing": [++ "inputSchema"++ ]++ },++ {++ "id": "tips.checkout.create",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "tips.goal.close",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "tips.goal.create",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "tips.goal.update",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "tips.interaction.get",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "tips.interaction.list",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "tips.interaction.moderate",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "tips.interaction.record",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "tips.media_request.create",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "tips.overlay.config.get",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "tips.overlay.config.update",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "tips.overlay.token.create",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "tips.overlay.token.revoke",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "tips.profile.get",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "tips.profile.update",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "tips.simulation.run",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "tips.superchat.create",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "tips.tts.request",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "trade.alert.create",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "trade.alert.delete",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "trade.alert.read",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "trade.context.propose",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "trade.context.read",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "trade.instrument.manage",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "trade.instrument.resolve",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "trade.observation.write",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "trade.watchlist.create",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "trade.watchlist.delete",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "trade.watchlist.read",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "trade.watchlist.update",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "vip.creator.members.list",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "vip.entitlement.check",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "vip.membership.checkout",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "vip.membership.status",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "vip.perk.create",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "vip.perk.list",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "vip.perk.update",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "vip.plan.archive",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "vip.plan.create",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "vip.plan.list",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "vip.plan.update",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "vip.resource.policy.evaluate",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "vip.resource.policy.get",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "vip.resource.policy.set",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "wiki.citation.attach",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "wiki.page.create",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "wiki.page.read",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "wiki.revision.propose",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "wiki.revision.publish",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "wiki.revision.revert",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "wiki.search.query",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ },++ {++ "id": "wiki.space.create",++ "missing": [++ "inputSchema",++ "outputSchema"++ ]++ }++ ]++}@@@ -136,6 +136,33 @@"status": "active","adr": "ADR-016"},++ {++ "id": "common.no-body",++ "version": "1.0.0",++ "owner": "common",++ "schema": "common/no-body.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "common.binary",++ "version": "1.0.0",++ "owner": "common",++ "schema": "common/binary.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "media.file-upload",++ "version": "1.0.0",++ "owner": "media",++ "schema": "media/file-upload.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },{"id": "search.index-document","version": "1.0.0",nnew file mode 100644@@@ -0,0 +1,8 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/common/binary.v1.json",++ "title": "Binary",++ "description": "common.binary@1: the response is the resource's bytes, not JSON — Content-Type names the media type, Content-Length (or Range with 206) the size. An error is still an application/problem+json body. A declaration for capabilities that serve files (roadmap WS-C task 4); there is nothing to validate but the headers.",++ "type": "string",++ "contentEncoding": "binary"++}nnew file mode 100644@@@ -0,0 +1,8 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/common/no-body.v1.json",++ "title": "NoBody",++ "description": "common.no-body@1: the capability's request carries no body; its parameters are the path and query of the routes in implementedBy (an id, a page cursor). An input schema that says so explicitly, so every active capability names one (roadmap WS-C task 4).",++ "type": "object",++ "maxProperties": 0++}nnew file mode 100644@@@ -0,0 +1,12 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/media/file-upload.v1.json",++ "title": "MediaFileUpload",++ "description": "media.file-upload@1: the multipart/form-data body of POST /api/v1/:app/files (media.object.upload, namespace :app): one part named `file` with the bytes and its filename and type. The tenant's quota is checked against the bytes; the answer is media.media-ref@1.",++ "type": "object",++ "required": ["file"],++ "additionalProperties": true,++ "properties": {++ "file": { "type": "string", "contentEncoding": "binary", "description": "The multipart part named file (filename and Content-Type from the part)." }++ }++}nnew file mode 100644@@@ -0,0 +1 @@++{ "data": "x" }nnew file mode 100644@@@ -0,0 +1 @@++"<bytes>"nnew file mode 100644@@@ -0,0 +1 @@++{ "id": "x" }nnew file mode 100644@@@ -0,0 +1 @@++{}nnew file mode 100644@@@ -0,0 +1 @@++{ "name": "x.png" }nnew file mode 100644@@@ -0,0 +1 @@++{ "file": "<bytes of screenshot.png>" }@@@ -1584,6 +1584,55 @@}}},++ "common.no-body": {++ "version": "1.0.0",++ "owner": "common",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/common/no-body.v1.json",++ "title": "NoBody",++ "description": "common.no-body@1: the capability's request carries no body; its parameters are the path and query of the routes in implementedBy (an id, a page cursor). An input schema that says so explicitly, so every active capability names one (roadmap WS-C task 4).",++ "type": "object",++ "maxProperties": 0++ }++ },++ "common.binary": {++ "version": "1.0.0",++ "owner": "common",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/common/binary.v1.json",++ "title": "Binary",++ "description": "common.binary@1: the response is the resource's bytes, not JSON — Content-Type names the media type, Content-Length (or Range with 206) the size. An error is still an application/problem+json body. A declaration for capabilities that serve files (roadmap WS-C task 4); there is nothing to validate but the headers.",++ "type": "string",++ "contentEncoding": "binary"++ }++ },++ "media.file-upload": {++ "version": "1.0.0",++ "owner": "media",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/media/file-upload.v1.json",++ "title": "MediaFileUpload",++ "description": "media.file-upload@1: the multipart/form-data body of POST /api/v1/:app/files (media.object.upload, namespace :app): one part named `file` with the bytes and its filename and type. The tenant's quota is checked against the bytes; the answer is media.media-ref@1.",++ "type": "object",++ "required": [++ "file"++ ],++ "additionalProperties": true,++ "properties": {++ "file": {++ "type": "string",++ "contentEncoding": "binary",++ "description": "The multipart part named file (filename and Content-Type from the part)."++ }++ }++ }++ },"search.index-document": {"version": "1.0.0","owner": "search",@@@ -601,6 +601,30 @@ export interface ReleaseManifest {metrics_url?: string | null;}++/** [email protected] (owner: common) */++/**++ * common.no-body@1: the capability's request carries no body; its parameters are the path and query of the routes in implementedBy (an id, a page cursor). An input schema that says so explicitly, so every active capability names one (roadmap WS-C task 4).++ */++export interface NoBody {}++++/** [email protected] (owner: common) */++/**++ * common.binary@1: the response is the resource's bytes, not JSON — Content-Type names the media type, Content-Length (or Range with 206) the size. An error is still an application/problem+json body. A declaration for capabilities that serve files (roadmap WS-C task 4); there is nothing to validate but the headers.++ */++export type Binary = string;++++/** [email protected] (owner: media) */++/**++ * media.file-upload@1: the multipart/form-data body of POST /api/v1/:app/files (media.object.upload, namespace :app): one part named `file` with the bytes and its filename and type. The tenant's quota is checked against the bytes; the answer is media.media-ref@1.++ */++export interface MediaFileUpload {++ /**++ * The multipart part named file (filename and Content-Type from the part).++ */++ file: string;++ [k: string]: unknown | undefined;++}++/** [email protected] (owner: search) *//*** What an owning service tells OpenVibe.Search about one of its resources (roadmap §15.12, §4.2 A). The owner stays the source of truth; Search keeps a revision-ordered, permission-aware copy for discovery. A document with deleted=true is a tombstone: it removes the resource from every result and wins over any upsert with the same or an older revision.@@@ -17,5 +17,6 @@"implementedBy": ["GET /api/v1/:app/files/:key","GET /f/:key"-- ]++ ],++ "outputSchema": "common.binary@1"}@@@ -19,5 +19,6 @@],"implementedBy": ["POST /api/v1/:app/files"-- ]++ ],++ "inputSchema": "media.file-upload@1"}@@@ -16,5 +16,6 @@"events": [],"implementedBy": ["DELETE /api/v1/jobs/:id"-- ]++ ],++ "inputSchema": "common.no-body@1"}@@@ -18,5 +18,6 @@"GET /api/v1/jobs/:id","GET /api/v1/jobs/:id/events","GET /api/v1/jobs/:id/files/:n"-- ]++ ],++ "inputSchema": "common.no-body@1"}@@@ -18,5 +18,6 @@"GET /api/v1/tools","GET /api/v1/tools/:id","GET /api/v1/tools/:id/schema"-- ]++ ],++ "inputSchema": "common.no-body@1"}nnew file mode 100644@@@ -0,0 +1,31 @@++'use strict';++// Capability schemas (roadmap WS-C task 4): every active capability names an input and an output++// schema that resolve in the catalog, except the recorded gaps in compatibility/capability-schema-gaps.json,++// a list that only shrinks: a new active capability without schemas fails, and so does a listed gap that++// has been filled (take it off the list).++const assert = require('assert');++const fs = require('fs');++const path = require('path');++const { schema } = require('..');++++const ROOT = path.join(__dirname, '..');++const dir = path.join(ROOT, 'manifests', 'capabilities');++const gaps = new Map(JSON.parse(fs.readFileSync(path.join(ROOT, 'compatibility', 'capability-schema-gaps.json'), 'utf8')).gaps.map((g) => [g.id, g.missing]));++const problems = [];++let active = 0, complete = 0;++for (const f of fs.readdirSync(dir).sort()) {++ const c = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8'));++ if (c.status !== 'active') continue;++ active++;++ const missing = ['inputSchema', 'outputSchema'].filter((k) => !c[k]);++ for (const k of ['inputSchema', 'outputSchema']) {++ if (c[k] && !schema(c[k])) problems.push(`${c.id}: ${k} ${c[k]} is not in the catalog`);++ }++ const listed = gaps.get(c.id);++ if (!missing.length) { complete++; if (listed) problems.push(`${c.id}: has both schemas now — remove it from capability-schema-gaps.json`); continue; }++ if (!listed) problems.push(`${c.id}: active without ${missing.join(' and ')} (add the schema, or record the gap)`);++ else if (missing.some((k) => !listed.includes(k))) problems.push(`${c.id}: lost its ${missing.filter((k) => !listed.includes(k)).join(', ')}`);++}++for (const id of gaps.keys()) if (!fs.existsSync(path.join(dir, `${id}.json`))) problems.push(`${id}: listed as a gap but no such capability`);++assert.deepStrictEqual(problems, []);++console.log(`capability schemas: ${complete}/${active} active capabilities complete; ${gaps.size} recorded gap(s)`);@@@ -632,6 +632,9 @@ await assert.rejects(failing.getToken(), /401: invalid_client/);fs.rmSync(tmp, { recursive: true, force: true });}++// ── Capability schemas (WS-C task 4): a ratchet over compatibility/capability-schema-gaps.json ──++execFileSync(process.execPath, [path.join(__dirname, 'capability-schemas.test.js')], { stdio: 'inherit' });++// ── Generated output and compatibility gate ──────────────────────────────execFileSync(process.execPath, [path.join(ROOT, 'scripts/generate.js'), '--check'], { stdio: 'inherit' });execFileSync(process.execPath, [path.join(ROOT, 'scripts/compat.js')], { stdio: 'inherit' });
@@ -6,7 +6,7 @@ the notes were in the tag and commit messages (`git tag -n1`).
@@ -6,7 +6,7 @@ the notes were in the tag and commit messages (`git tag -n1`).
## Unreleased
## Unreleased
-Capability schemas (roadmap WS-C task 4), starting with Search: **`search.query@1`** (the query string of `GET /api/v1/search` and `/suggest`: q, owner, type, lang, facets, limit, cursor, `facet.<key>`), **`search.query-result@1`** (a page of hits the caller may see, `next_cursor`, facet counts; never an ACL or a total) and **`search.write-result@1`** (`PUT /api/v1/documents/…`: applied or unchanged; stale and conflicting revisions are 409 problems). `search.query.run` and `search.query.delegate` name the first two; `search.document.write` names the third as its output. Fixtures for each. Additive.
+Capability schemas (roadmap WS-C task 4), starting with Search: **`search.query@1`** (the query string of `GET /api/v1/search` and `/suggest`: q, owner, type, lang, facets, limit, cursor, `facet.<key>`), **`search.query-result@1`** (a page of hits the caller may see, `next_cursor`, facet counts; never an ACL or a total) and **`search.write-result@1`** (`PUT /api/v1/documents/…`: applied or unchanged; stale and conflicting revisions are 409 problems). `search.query.run` and `search.query.delegate` name the first two; `search.document.write` names the third as its output. Fixtures for each. Two conventions so every capability can name schemas: **`common.no-body@1`** (a request whose parameters are the path and query) and **`common.binary@1`** (a response of bytes); **`media.file-upload@1`** is the multipart body of `POST /api/v1/:app/files`. The Media and Tools capabilities name them. **A ratchet**: `test/capability-schemas.test.js` fails on an active capability without an input or output schema unless it is in `compatibility/capability-schema-gaps.json` (162 today), and on a listed gap that has been filled; the list only shrinks. Additive.
## 0.49.0 — 2026-09-25
## 0.49.0 — 2026-09-25
new file mode 100644
new file mode 100644
@@ -0,0 +1,1122 @@
@@ -0,0 +1,1122 @@
+{
+ "_about": "Active capabilities still without an input or output schema (roadmap WS-C task 4). test/capability-schemas.test.js fails on any active capability missing a schema that is not listed here, and on a listed one that has since gained it (remove it from this list). The list only shrinks: fill a capability's schemas, then delete its row. A request with no body names common.no-body@1; a response of bytes names common.binary@1.",
+ "gaps": [
+ {
+ "id": "ai.provider.manage",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "ai.run.read",
+ "missing": [
+ "inputSchema"
+ ]
+ },
+ {
+ "id": "ai.usage.read",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "ai.workflow.manage",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "billing.balance.read",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "billing.cashout.manage",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "billing.cashout.request",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "billing.entitlement.check",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "billing.intent.create",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "billing.ledger.admin",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "billing.subscription.manage",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "billing.transfer.create",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "blog.blog.configure",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "blog.blog.create",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "blog.feed.read",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "blog.member.manage",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "blog.post.create",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "blog.post.delete",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "blog.post.publish",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "blog.post.read",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "blog.post.schedule",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "blog.post.unpublish",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "blog.post.update",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "blog.theme.set",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "chat.message.send",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "codes.release.manage",
+ "missing": [
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "codes.release.read",
+ "missing": [
+ "inputSchema"
+ ]
+ },
+ {
+ "id": "community.comment.moderate",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "community.paste.create",
+ "missing": [
+ "inputSchema"
+ ]
+ },
+ {
+ "id": "community.paste.moderate",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "community.paste.write",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "community.post.create",
+ "missing": [
+ "inputSchema"
+ ]
+ },
+ {
+ "id": "community.pulse.read",
+ "missing": [
+ "inputSchema"
+ ]
+ },
+ {
+ "id": "community.pulse.write",
+ "missing": [
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "community.space.read",
+ "missing": [
+ "inputSchema"
+ ]
+ },
+ {
+ "id": "community.thread.read",
+ "missing": [
+ "inputSchema"
+ ]
+ },
+ {
+ "id": "coupons.coupon.lookup",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "coupons.coupon.submit",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "coupons.merchant.manage",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "coupons.merchant.resolve",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "coupons.report.create",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "coupons.status.update",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "deals.flag.create",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "deals.offer.expire",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "deals.offer.merge",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "deals.offer.moderate",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "deals.offer.submit",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "deals.offer.update",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "deals.product.resolve",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "deals.vote.remove",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "deals.vote.set",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "deals.watch.create",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "deals.watch.delete",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "deals.watch.read",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "events.app.publish",
+ "missing": [
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "events.app.read",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "events.app.subscribe",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "events.delivery.admin",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "events.event.publish",
+ "missing": [
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "events.event.read",
+ "missing": [
+ "inputSchema"
+ ]
+ },
+ {
+ "id": "events.subscription.manage",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "games.mod.manage",
+ "missing": [
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "games.mod.read",
+ "missing": [
+ "inputSchema"
+ ]
+ },
+ {
+ "id": "games.prop.place",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "games.world.announce",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "host.deploy.create",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "host.domain.manage",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "host.site.manage",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "live.channel.read",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "live.discovery.read",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "live.follower.read",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "live.stream.read",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "live.tips_delivery.write",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "network.blocks.read",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "network.coins.credit",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "network.coins.debit",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "network.coins.transfer",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "network.integration.github.read",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "network.modules.read",
+ "missing": [
+ "inputSchema"
+ ]
+ },
+ {
+ "id": "network.notifications.push",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "network.staff.read",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "news.cluster.manage",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "news.cluster.read",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "news.perspective.update",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "news.source.attach",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "news.story.create",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "news.story.publish",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "news.story.read",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "news.story.retract",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "news.story.revise",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "news.timeline.update",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "news.topic.manage",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "openre.key.rotate",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "openre.output.read",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "openre.output.write",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "openre.session.end",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "openre.session.read",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "openre.stream.read",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "openre.stream.write",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "reviews.correction.submit",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "reviews.entity.manage",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "reviews.entity.merge",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "reviews.entity.resolve",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "reviews.entity.split",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "reviews.signal.import",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "reviews.summary.propose",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "reviews.summary.publish",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "sources.item.read",
+ "missing": [
+ "inputSchema"
+ ]
+ },
+ {
+ "id": "sources.source.manage",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "sources.source.read",
+ "missing": [
+ "inputSchema"
+ ]
+ },
+ {
+ "id": "tips.checkout.create",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "tips.goal.close",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "tips.goal.create",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "tips.goal.update",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "tips.interaction.get",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "tips.interaction.list",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "tips.interaction.moderate",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "tips.interaction.record",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "tips.media_request.create",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "tips.overlay.config.get",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "tips.overlay.config.update",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "tips.overlay.token.create",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "tips.overlay.token.revoke",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "tips.profile.get",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "tips.profile.update",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "tips.simulation.run",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "tips.superchat.create",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "tips.tts.request",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "trade.alert.create",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "trade.alert.delete",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "trade.alert.read",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "trade.context.propose",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "trade.context.read",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "trade.instrument.manage",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "trade.instrument.resolve",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "trade.observation.write",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "trade.watchlist.create",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "trade.watchlist.delete",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "trade.watchlist.read",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "trade.watchlist.update",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "vip.creator.members.list",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "vip.entitlement.check",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "vip.membership.checkout",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "vip.membership.status",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "vip.perk.create",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "vip.perk.list",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "vip.perk.update",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "vip.plan.archive",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "vip.plan.create",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "vip.plan.list",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "vip.plan.update",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "vip.resource.policy.evaluate",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "vip.resource.policy.get",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "vip.resource.policy.set",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "wiki.citation.attach",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "wiki.page.create",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "wiki.page.read",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "wiki.revision.propose",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "wiki.revision.publish",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "wiki.revision.revert",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "wiki.search.query",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ },
+ {
+ "id": "wiki.space.create",
+ "missing": [
+ "inputSchema",
+ "outputSchema"
+ ]
+ }
+ ]
+}
@@ -136,6 +136,33 @@
@@ -136,6 +136,33 @@
"status": "active",
"status": "active",
"adr": "ADR-016"
"adr": "ADR-016"
},
},
+ {
+ "id": "common.no-body",
+ "version": "1.0.0",
+ "owner": "common",
+ "schema": "common/no-body.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "common.binary",
+ "version": "1.0.0",
+ "owner": "common",
+ "schema": "common/binary.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "media.file-upload",
+ "version": "1.0.0",
+ "owner": "media",
+ "schema": "media/file-upload.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
{
{
"id": "search.index-document",
"id": "search.index-document",
"version": "1.0.0",
"version": "1.0.0",
new file mode 100644
new file mode 100644
@@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/common/binary.v1.json",
+ "title": "Binary",
+ "description": "common.binary@1: the response is the resource's bytes, not JSON — Content-Type names the media type, Content-Length (or Range with 206) the size. An error is still an application/problem+json body. A declaration for capabilities that serve files (roadmap WS-C task 4); there is nothing to validate but the headers.",
+ "type": "string",
+ "contentEncoding": "binary"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/common/no-body.v1.json",
+ "title": "NoBody",
+ "description": "common.no-body@1: the capability's request carries no body; its parameters are the path and query of the routes in implementedBy (an id, a page cursor). An input schema that says so explicitly, so every active capability names one (roadmap WS-C task 4).",
+ "type": "object",
+ "maxProperties": 0
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/media/file-upload.v1.json",
+ "title": "MediaFileUpload",
+ "description": "media.file-upload@1: the multipart/form-data body of POST /api/v1/:app/files (media.object.upload, namespace :app): one part named `file` with the bytes and its filename and type. The tenant's quota is checked against the bytes; the answer is media.media-ref@1.",
+ "type": "object",
+ "required": ["file"],
+ "additionalProperties": true,
+ "properties": {
+ "file": { "type": "string", "contentEncoding": "binary", "description": "The multipart part named file (filename and Content-Type from the part)." }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "data": "x" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+"<bytes>"
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "id": "x" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{}
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "name": "x.png" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "file": "<bytes of screenshot.png>" }
@@ -1584,6 +1584,55 @@
@@ -1584,6 +1584,55 @@
}
}
}
}
},
},
+ "common.no-body": {
+ "version": "1.0.0",
+ "owner": "common",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/common/no-body.v1.json",
+ "title": "NoBody",
+ "description": "common.no-body@1: the capability's request carries no body; its parameters are the path and query of the routes in implementedBy (an id, a page cursor). An input schema that says so explicitly, so every active capability names one (roadmap WS-C task 4).",
+ "type": "object",
+ "maxProperties": 0
+ }
+ },
+ "common.binary": {
+ "version": "1.0.0",
+ "owner": "common",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/common/binary.v1.json",
+ "title": "Binary",
+ "description": "common.binary@1: the response is the resource's bytes, not JSON — Content-Type names the media type, Content-Length (or Range with 206) the size. An error is still an application/problem+json body. A declaration for capabilities that serve files (roadmap WS-C task 4); there is nothing to validate but the headers.",
+ "type": "string",
+ "contentEncoding": "binary"
+ }
+ },
+ "media.file-upload": {
+ "version": "1.0.0",
+ "owner": "media",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/media/file-upload.v1.json",
+ "title": "MediaFileUpload",
+ "description": "media.file-upload@1: the multipart/form-data body of POST /api/v1/:app/files (media.object.upload, namespace :app): one part named `file` with the bytes and its filename and type. The tenant's quota is checked against the bytes; the answer is media.media-ref@1.",
+ "type": "object",
+ "required": [
+ "file"
+ ],
+ "additionalProperties": true,
+ "properties": {
+ "file": {
+ "type": "string",
+ "contentEncoding": "binary",
+ "description": "The multipart part named file (filename and Content-Type from the part)."
+ }
+ }
+ }
+ },
"search.index-document": {
"search.index-document": {
"version": "1.0.0",
"version": "1.0.0",
"owner": "search",
"owner": "search",
@@ -601,6 +601,30 @@ export interface ReleaseManifest {
@@ -601,6 +601,30 @@ export interface ReleaseManifest {
metrics_url?: string | null;
metrics_url?: string | null;
}
}
+/** [email protected] (owner: common) */
+/**
+ * common.no-body@1: the capability's request carries no body; its parameters are the path and query of the routes in implementedBy (an id, a page cursor). An input schema that says so explicitly, so every active capability names one (roadmap WS-C task 4).
+ */
+export interface NoBody {}
+
+/** [email protected] (owner: common) */
+/**
+ * common.binary@1: the response is the resource's bytes, not JSON — Content-Type names the media type, Content-Length (or Range with 206) the size. An error is still an application/problem+json body. A declaration for capabilities that serve files (roadmap WS-C task 4); there is nothing to validate but the headers.
+ */
+export type Binary = string;
+
+/** [email protected] (owner: media) */
+/**
+ * media.file-upload@1: the multipart/form-data body of POST /api/v1/:app/files (media.object.upload, namespace :app): one part named `file` with the bytes and its filename and type. The tenant's quota is checked against the bytes; the answer is media.media-ref@1.
+ */
+export interface MediaFileUpload {
+ /**
+ * The multipart part named file (filename and Content-Type from the part).
+ */
+ file: string;
+ [k: string]: unknown | undefined;
+}
+
/** [email protected] (owner: search) */
/** [email protected] (owner: search) */
/**
/**
* What an owning service tells OpenVibe.Search about one of its resources (roadmap §15.12, §4.2 A). The owner stays the source of truth; Search keeps a revision-ordered, permission-aware copy for discovery. A document with deleted=true is a tombstone: it removes the resource from every result and wins over any upsert with the same or an older revision.
* What an owning service tells OpenVibe.Search about one of its resources (roadmap §15.12, §4.2 A). The owner stays the source of truth; Search keeps a revision-ordered, permission-aware copy for discovery. A document with deleted=true is a tombstone: it removes the resource from every result and wins over any upsert with the same or an older revision.
@@ -17,5 +17,6 @@
@@ -17,5 +17,6 @@
"implementedBy": [
"implementedBy": [
"GET /api/v1/:app/files/:key",
"GET /api/v1/:app/files/:key",
"GET /f/:key"
"GET /f/:key"
- ]
+ ],
+ "outputSchema": "common.binary@1"
}
}
@@ -19,5 +19,6 @@
@@ -19,5 +19,6 @@
],
],
"implementedBy": [
"implementedBy": [
"POST /api/v1/:app/files"
"POST /api/v1/:app/files"
- ]
+ ],
+ "inputSchema": "media.file-upload@1"
}
}
@@ -16,5 +16,6 @@
@@ -16,5 +16,6 @@
"events": [],
"events": [],
"implementedBy": [
"implementedBy": [
"DELETE /api/v1/jobs/:id"
"DELETE /api/v1/jobs/:id"
- ]
+ ],
+ "inputSchema": "common.no-body@1"
}
}
@@ -18,5 +18,6 @@
@@ -18,5 +18,6 @@
"GET /api/v1/jobs/:id",
"GET /api/v1/jobs/:id",
"GET /api/v1/jobs/:id/events",
"GET /api/v1/jobs/:id/events",
"GET /api/v1/jobs/:id/files/:n"
"GET /api/v1/jobs/:id/files/:n"
- ]
+ ],
+ "inputSchema": "common.no-body@1"
}
}
@@ -18,5 +18,6 @@
@@ -18,5 +18,6 @@
"GET /api/v1/tools",
"GET /api/v1/tools",
"GET /api/v1/tools/:id",
"GET /api/v1/tools/:id",
"GET /api/v1/tools/:id/schema"
"GET /api/v1/tools/:id/schema"
- ]
+ ],
+ "inputSchema": "common.no-body@1"
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1,31 @@
@@ -0,0 +1,31 @@
+'use strict';
+// Capability schemas (roadmap WS-C task 4): every active capability names an input and an output
+// schema that resolve in the catalog, except the recorded gaps in compatibility/capability-schema-gaps.json,
+// a list that only shrinks: a new active capability without schemas fails, and so does a listed gap that
+// has been filled (take it off the list).
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const { schema } = require('..');
+
+const ROOT = path.join(__dirname, '..');
+const dir = path.join(ROOT, 'manifests', 'capabilities');
+const gaps = new Map(JSON.parse(fs.readFileSync(path.join(ROOT, 'compatibility', 'capability-schema-gaps.json'), 'utf8')).gaps.map((g) => [g.id, g.missing]));
+const problems = [];
+let active = 0, complete = 0;
+for (const f of fs.readdirSync(dir).sort()) {
+ const c = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8'));
+ if (c.status !== 'active') continue;
+ active++;
+ const missing = ['inputSchema', 'outputSchema'].filter((k) => !c[k]);
+ for (const k of ['inputSchema', 'outputSchema']) {
+ if (c[k] && !schema(c[k])) problems.push(`${c.id}: ${k} ${c[k]} is not in the catalog`);
+ }
+ const listed = gaps.get(c.id);
+ if (!missing.length) { complete++; if (listed) problems.push(`${c.id}: has both schemas now — remove it from capability-schema-gaps.json`); continue; }
+ if (!listed) problems.push(`${c.id}: active without ${missing.join(' and ')} (add the schema, or record the gap)`);
+ else if (missing.some((k) => !listed.includes(k))) problems.push(`${c.id}: lost its ${missing.filter((k) => !listed.includes(k)).join(', ')}`);
+}
+for (const id of gaps.keys()) if (!fs.existsSync(path.join(dir, `${id}.json`))) problems.push(`${id}: listed as a gap but no such capability`);
+assert.deepStrictEqual(problems, []);
+console.log(`capability schemas: ${complete}/${active} active capabilities complete; ${gaps.size} recorded gap(s)`);
@@ -632,6 +632,9 @@ await assert.rejects(failing.getToken(), /401: invalid_client/);
@@ -632,6 +632,9 @@ await assert.rejects(failing.getToken(), /401: invalid_client/);
fs.rmSync(tmp, { recursive: true, force: true });
fs.rmSync(tmp, { recursive: true, force: true });
}
}
+// ── Capability schemas (WS-C task 4): a ratchet over compatibility/capability-schema-gaps.json ──
+execFileSync(process.execPath, [path.join(__dirname, 'capability-schemas.test.js')], { stdio: 'inherit' });
+
// ── Generated output and compatibility gate ──────────────────────────────
// ── Generated output and compatibility gate ──────────────────────────────
execFileSync(process.execPath, [path.join(ROOT, 'scripts/generate.js'), '--check'], { stdio: 'inherit' });
execFileSync(process.execPath, [path.join(ROOT, 'scripts/generate.js'), '--check'], { stdio: 'inherit' });
execFileSync(process.execPath, [path.join(ROOT, 'scripts/compat.js')], { stdio: 'inherit' });
execFileSync(process.execPath, [path.join(ROOT, 'scripts/compat.js')], { stdio: 'inherit' });