openvibers/openvibe.contracts Commits

Contracts 0.407.0: release train for #451, #425, #428 and #450 (#452)

OP OpenVibers committed 2 days ago · 847f09a576 · 31 files, +1472 −37

* Contracts: codes.task-summary@1 and codes.task.updated@1 for the Dock's Code tab

* Define dated comparison data and register Compare

* Regenerate estate inventory for Compare

* Render estate from committed repository manifests

* Exclude local scratch checkouts from estate projection

* Contracts 0.407.0: is_test on the identity subject projection and the test-account list

* Merge branch 'pr425' into release/contracts-0407

# Conflicts:
#	CHANGELOG.md
#	generated/openapi/actor.json
#	generated/openapi/ai.json
#	generated/openapi/bench.json
#	generated/openapi/billing.json
#	generated/openapi/blog.json
#	generated/openapi/bot.json
#	generated/openapi/chat.json
#	generated/openapi/codes.json
#	generated/openapi/community.json
#	generated/openapi/connect.json
#	generated/openapi/coupons.json
#	generated/openapi/deals.json
#	generated/openapi/design.json
#	generated/openapi/domains.json
#	generated/openapi/events.json
#	generated/openapi/exchange.json
#	generated/openapi/food.json
#	generated/openapi/games.json
#	generated/openapi/git.json
#	generated/openapi/host.json
#	generated/openapi/inventory.json
#	generated/openapi/live.json
#	generated/openapi/mail.json
#	generated/openapi/media-hub.json
#	generated/openapi/media.json
#	generated/openapi/network.json
#	generated/openapi/news.json
#	generated/openapi/openre.json
#	generated/openapi/play.json
#	generated/openapi/quest.json
#	generated/openapi/report.json
#	generated/openapi/reviews.json
#	generated/openapi/run.json
#	generated/openapi/search.json
#	generated/openapi/services.json
#	generated/openapi/sources.json
#	generated/openapi/space.json
#	generated/openapi/tips.json
#	generated/openapi/today.json
#	generated/openapi/tools.json
#	generated/openapi/trade.json
#	generated/openapi/vip.json
#	generated/openapi/vision.json
#	generated/openapi/watch.json
#	generated/openapi/wiki.json
#	generated/openapi/work.json
#	generated/openapi/zone.json
#	package.json

* Merge branch 'pr428' into release/contracts-0407

# Conflicts:
#	CHANGELOG.md
#	contracts/catalog.json
#	generated/json-schema/bundle.json
#	generated/openapi/actor.json
#	generated/openapi/ai.json
#	generated/openapi/bench.json
#	generated/openapi/billing.json
#	generated/openapi/blog.json
#	generated/openapi/bot.json
#	generated/openapi/chat.json
#	generated/openapi/codes.json
#	generated/openapi/community.json
#	generated/openapi/connect.json
#	generated/openapi/coupons.json
#	generated/openapi/deals.json
#	generated/openapi/design.json
#	generated/openapi/domains.json
#	generated/openapi/events.json
#	generated/openapi/exchange.json
#	generated/openapi/food.json
#	generated/openapi/games.json
#	generated/openapi/git.json
#	generated/openapi/host.json
#	generated/openapi/inventory.json
#	generated/openapi/live.json
#	generated/openapi/mail.json
#	generated/openapi/media-hub.json
#	generated/openapi/media.json
#	generated/openapi/network.json
#	generated/openapi/news.json
#	generated/openapi/openre.json
#	generated/openapi/play.json
#	generated/openapi/quest.json
#	generated/openapi/report.json
#	generated/openapi/reviews.json
#	generated/openapi/run.json
#	generated/openapi/search.json
#	generated/openapi/services.json
#	generated/openapi/sources.json
#	generated/openapi/space.json
#	generated/openapi/tips.json
#	generated/openapi/today.json
#	generated/openapi/tools.json
#	generated/openapi/trade.json
#	generated/openapi/vip.json
#	generated/openapi/vision.json
#	generated/openapi/watch.json
#	generated/openapi/wiki.json
#	generated/openapi/work.json
#	generated/openapi/zone.json
#	generated/typescript/index.d.ts
#	package-lock.json
#	package.json

* Merge branch 'pr450' into release/contracts-0407

# Conflicts:
#	CHANGELOG.md

* Contracts 0.407.0: release train for #451, #425, #428 and #450

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 4fc829d1..6d3aa34c 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -2,7 +2,18 @@
## 0.407.0 — 2026-11-11
--- Add `exchange.search.icons`: a first-party service searches icon references through the Exchange platform API (`POST /v1/search/icons` at `api.openvibe.exchange`, served by OpenVibe.AI) with its own free-search quota and no Exchange key. It reaches the icons route only, never web, image or fetch search, and the receipt names the engine, cost and skipped attempts. OpenVibe.Design holds it for the references shown beside a glyph draft.
++The third train of the day: #451 (merged ahead of its tag) and three pull requests that had waited for a release.
++
++- Add `exchange.search.icons`: a first-party service searches icon references through the Exchange platform API (`POST /v1/search/icons` at `api.openvibe.exchange`, served by OpenVibe.AI) with its own free-search quota and no Exchange key. It reaches the icons route only, never web, image or fetch search, and the receipt names the engine, cost and skipped attempts. OpenVibe.Design holds it for the references shown beside a glyph draft. (#451)
++- Add `codes.task-summary@1` (a task as the Dock's Code tab lists it: id, title, status, mode, repo, times, attempts,
++ url, preview and PR links) and `codes.task.updated@1`, the one event Codes writes on every task status change beside
++ the per-state `codes.task.*` events (#425).
++- Add `compare.comparison@1`: things, weighted criteria, finite results with required observation dates and HTTPS
++ sources, method, source list and update date; register the Compare service, repository and product in Read & ask
++ (its group gains an eighth tile) (#428).
++- Add the optional `is_test` to `identity.subject-projection@1` so public surfaces can leave probe and test accounts out
++ while their own signed-in views keep working, and the `{ subjects }` answer of Network's
++ `GET /internal/identity/test-subjects` in `identity.resolve-result@1` (#450).
## 0.406.0 — 2026-11-11
ddiff --git a/README.md b/README.md
iindex 38b2b943..e0743440 100644
---- a/README.md
++++ b/README.md
@@@ -5,7 +5,7 @@
**Docs:** https://openvibe.services · **For AI:** https://openvibe.services/llms.txt
**Install and test:** `npm ci && npm test` (Node 20 or newer)
--**Status:** alpha (first released in Wave 1; the release is package.json's `version`, tagged). At v0.406.0: 1238 schemas in `contracts/catalog.json`, 66 service manifests, 516 capability manifests, 14 namespace manifests and the staff capability map. Service repositories run `openvibe-contracts-check` and the pin-drift check in CI, and deployed services verify service and app tokens with `serviceAuth` in production. Consumers pin release tags (each repository's `STATUS.json` names its pin). `node scripts/docs-currency.js <root>` checks every checkout's README and `STATUS.json` against its code: the pins and versions they name, a library's consumer list, the deployed flag against the manifest, README links, and the paths and migrations they name. `node scripts/estate.js` generates `docs/ESTATE.md` and `manifests/repositories/` from the estate checkouts, and `--check` fails on drift (plan T1 step 3).
++**Status:** alpha (first released in Wave 1; the release is package.json's `version`, tagged). At v0.407.0: 1238 schemas in `contracts/catalog.json`, 66 service manifests, 516 capability manifests, 14 namespace manifests and the staff capability map. Service repositories run `openvibe-contracts-check` and the pin-drift check in CI, and deployed services verify service and app tokens with `serviceAuth` in production. Consumers pin release tags (each repository's `STATUS.json` names its pin). `node scripts/docs-currency.js <root>` checks every checkout's README and `STATUS.json` against its code: the pins and versions they name, a library's consumer list, the deployed flag against the manifest, README links, and the paths and migrations they name. `node scripts/estate.js` generates `docs/ESTATE.md` and `manifests/repositories/` from the estate checkouts, and `--check` fails on drift (plan T1 step 3).
**Plan:** OpenVibe Development Roadmap, Wave 1 (implementation plan rev 3, §3.1 and §18.11).
**License:** AGPL-3.0 (same as every OpenVibe service).
ddiff --git a/contracts/catalog.json b/contracts/catalog.json
iindex e65e03da..68098323 100644
---- a/contracts/catalog.json
++++ b/contracts/catalog.json
@@@ -98,7 +98,7 @@
},
{
"id": "identity.resolve-result",
-- "version": "1.0.0",
++ "version": "1.1.0",
"owner": "network",
"schema": "identity/resolve-result.v1.json",
"visibility": "first-party",
@@@ -170,7 +170,7 @@
},
{
"id": "identity.subject-projection",
-- "version": "1.0.0",
++ "version": "1.1.0",
"owner": "network",
"schema": "identity/subject-projection.v1.json",
"visibility": "first-party",
@@@ -8578,6 +8578,15 @@
"compatibility": "backward",
"status": "active"
},
++ {
++ "id": "codes.task-summary",
++ "version": "1.0.0",
++ "owner": "codes",
++ "schema": "codes/task-summary.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
{
"id": "play.title-definition",
"version": "1.0.0",
@@@ -9129,6 +9138,15 @@
"compatibility": "backward",
"status": "active"
},
++ {
++ "id": "codes.task.updated",
++ "version": "1.0.0",
++ "owner": "codes",
++ "schema": "events/payloads/codes.task.updated.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
{
"id": "domains.proposal",
"version": "1.0.0",
@@@ -11447,6 +11465,15 @@
"visibility": "public",
"compatibility": "backward",
"status": "active"
++ },
++ {
++ "id": "compare.comparison",
++ "version": "1.0.0",
++ "owner": "compare",
++ "schema": "compare/comparison.v1.json",
++ "visibility": "public",
++ "compatibility": "backward",
++ "status": "active"
}
]
}
ddiff --git a/contracts/codes/task-summary.v1.json b/contracts/codes/task-summary.v1.json
nnew file mode 100644
iindex 00000000..a305f38e
---- /dev/null
++++ b/contracts/codes/task-summary.v1.json
@@@ -0,0 +1,76 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/codes/task-summary.v1.json",
++ "title": "CodesTaskSummary",
++ "description": "codes.task-summary@1: a coding task on OpenVibe.Codes as the Dock's Code tab lists it (GET /api/v1/me/tasks, newest and running first). STATUS is the task in the Dock's words: queued (waiting for one of the person's machines), running, needs-you (it waits on the person: an approval in its inbox), done, failed or stopped. MODE is where the request started: create (built on openvibe.space, no Codes task), openvibe (one of OpenVibe's own projects) or repo (the person's own repository). STEPS counts the attempts the task has run. URL is the task's page on openvibe.codes; PREVIEW_URL is its live preview when one exists (a Space build), else null; PR_URL is the pull request it opened, else null.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "title",
++ "status",
++ "mode",
++ "repo",
++ "started_at",
++ "updated_at",
++ "steps",
++ "url",
++ "preview_url",
++ "pr_url"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^tsk_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The Codes task id (tsk_ + ULID)."
++ },
++ "title": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 200,
++ "description": "The task's first line: what was asked, in the person's own words."
++ },
++ "status": {
++ "enum": ["queued", "running", "needs-you", "done", "failed", "stopped"],
++ "description": "The task in the Dock's words."
++ },
++ "mode": {
++ "enum": ["create", "openvibe", "repo"],
++ "description": "Where the request started."
++ },
++ "repo": {
++ "type": "string",
++ "maxLength": 300,
++ "description": "The repository the task works on."
++ },
++ "started_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "updated_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "steps": {
++ "type": "integer",
++ "minimum": 0,
++ "description": "How many attempts the task has run."
++ },
++ "url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https://",
++ "description": "The task's page on openvibe.codes."
++ },
++ "preview_url": {
++ "type": ["string", "null"],
++ "format": "uri",
++ "description": "Its live preview when one exists, else null."
++ },
++ "pr_url": {
++ "type": ["string", "null"],
++ "format": "uri",
++ "description": "The pull request it opened, else null."
++ }
++ }
++}
ddiff --git a/contracts/compare/comparison.v1.json b/contracts/compare/comparison.v1.json
nnew file mode 100644
iindex 00000000..81635bd2
---- /dev/null
++++ b/contracts/compare/comparison.v1.json
@@@ -0,0 +1,233 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/compare/comparison.v1.json",
++ "title": "CompareComparison",
++ "type": "object",
++ "required": [
++ "slug",
++ "title",
++ "kind",
++ "summary",
++ "things",
++ "criteria",
++ "results",
++ "method",
++ "sources",
++ "updated_at",
++ "versions"
++ ],
++ "properties": {
++ "slug": {
++ "type": "string",
++ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
++ },
++ "title": {
++ "type": "string",
++ "minLength": 1
++ },
++ "kind": {
++ "type": "string",
++ "minLength": 1
++ },
++ "summary": {
++ "type": "string",
++ "minLength": 1
++ },
++ "things": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "id",
++ "name",
++ "maker",
++ "url",
++ "icon"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "minLength": 1
++ },
++ "name": {
++ "type": "string",
++ "minLength": 1
++ },
++ "maker": {
++ "type": "string",
++ "minLength": 1
++ },
++ "icon": {
++ "type": "string",
++ "minLength": 1
++ },
++ "url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https://"
++ }
++ },
++ "additionalProperties": false
++ }
++ },
++ "criteria": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "id",
++ "label",
++ "unit",
++ "better",
++ "weight",
++ "group"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "minLength": 1
++ },
++ "label": {
++ "type": "string",
++ "minLength": 1
++ },
++ "unit": {
++ "type": "string",
++ "minLength": 1
++ },
++ "group": {
++ "type": "string",
++ "minLength": 1
++ },
++ "better": {
++ "enum": [
++ "higher",
++ "lower"
++ ]
++ },
++ "weight": {
++ "type": "number",
++ "minimum": 0,
++ "maximum": 100
++ }
++ },
++ "additionalProperties": false
++ }
++ },
++ "results": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "thing",
++ "criterion",
++ "value",
++ "as_of",
++ "source"
++ ],
++ "properties": {
++ "thing": {
++ "type": "string",
++ "minLength": 1
++ },
++ "criterion": {
++ "type": "string",
++ "minLength": 1
++ },
++ "value": {
++ "type": "number"
++ },
++ "as_of": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "source": {
++ "type": "object",
++ "required": [
++ "label",
++ "url"
++ ],
++ "properties": {
++ "label": {
++ "type": "string",
++ "minLength": 1
++ },
++ "url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https://"
++ }
++ },
++ "additionalProperties": false
++ }
++ },
++ "additionalProperties": false
++ }
++ },
++ "method": {
++ "type": "string",
++ "minLength": 1
++ },
++ "sources": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "label",
++ "url"
++ ],
++ "properties": {
++ "label": {
++ "type": "string",
++ "minLength": 1
++ },
++ "url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https://"
++ }
++ },
++ "additionalProperties": false
++ }
++ },
++ "updated_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "versions": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "id",
++ "url",
++ "as_of"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "minLength": 1
++ },
++ "url": {
++ "type": "string",
++ "format": "uri"
++ },
++ "as_of": {
++ "type": "string",
++ "format": "date-time"
++ }
++ },
++ "additionalProperties": false
++ }
++ },
++ "availability": {
++ "type": "array",
++ "items": {
++ "type": "string",
++ "minLength": 1
++ }
++ }
++ },
++ "additionalProperties": false,
++ "description": "A comparison of things by weighted criteria, with finite dated results and their public sources. Missing evidence is absent, never zero."
++}
ddiff --git a/contracts/events/payloads/codes.task.updated.v1.json b/contracts/events/payloads/codes.task.updated.v1.json
nnew file mode 100644
iindex 00000000..569d185a
---- /dev/null
++++ b/contracts/events/payloads/codes.task.updated.v1.json
@@@ -0,0 +1,32 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/codes.task.updated.v1.json",
++ "title": "CodesTaskUpdatedPayload",
++ "description": "codes.task.updated v1 (OpenVibe.Codes): a task's status changed. The one event the Dock's Code tab follows instead of polling: every state change writes it, alongside the per-state codes.task.* event. STATE is the announced state (placed reads as running, expired as failed). URL is the task's page on openvibe.codes. Envelope: source codes, subject { type: user, id: <owner usr_> }, visibility internal.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": ["task_id", "state", "agent", "repo", "cost_usd", "files_changed", "started_at", "finished_at", "error", "url"],
++ "properties": {
++ "task_id": { "type": "string", "pattern": "^tsk_[0-9A-HJKMNP-TV-Z]{26}$" },
++ "state": { "enum": ["queued", "running", "succeeded", "failed", "cancelled"] },
++ "agent": { "type": ["string", "null"], "maxLength": 80 },
++ "repo": {
++ "anyOf": [
++ { "type": "string", "pattern": "^[^/\\s]+/[^/\\s]+$", "maxLength": 300 },
++ { "type": "null" }
++ ],
++ "description": "owner/name from the task's repository URL, or null when it is not a repository path."
++ },
++ "cost_usd": { "type": ["number", "null"], "minimum": 0 },
++ "files_changed": { "type": ["integer", "null"], "minimum": 0 },
++ "started_at": { "type": ["string", "null"], "format": "date-time" },
++ "finished_at": { "type": ["string", "null"], "format": "date-time" },
++ "error": { "type": ["string", "null"], "maxLength": 500 },
++ "url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https://",
++ "description": "The task's page on openvibe.codes."
++ }
++ }
++}
ddiff --git a/contracts/identity/resolve-result.v1.json b/contracts/identity/resolve-result.v1.json
iindex bc22a305..f8aa133b 100644
---- a/contracts/identity/resolve-result.v1.json
++++ b/contracts/identity/resolve-result.v1.json
@@@ -2,7 +2,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/identity/resolve-result.v1.json",
"title": "IdentityResolveResult",
-- "description": "identity.resolve-result@1: answers of identity.subject.resolve on OpenVibe.Network. GET /internal/identity/resolve → one identity.subject-projection@1 (404 identity.subject_not_found when nothing matches); POST /internal/identity/resolve-batch → { results } keyed by each id asked, null for an id that resolves to nothing.",
++ "description": "identity.resolve-result@1: answers of identity.subject.resolve on OpenVibe.Network. GET /internal/identity/resolve \u2192 one identity.subject-projection@1 (404 identity.subject_not_found when nothing matches); POST /internal/identity/resolve-batch \u2192 { results } keyed by each id asked, null for an id that resolves to nothing; GET /internal/identity/test-subjects \u2192 { subjects } with every account marked is_test.",
"anyOf": [
{
"$ref": "subject-projection.v1.json"
@@@ -27,6 +27,20 @@
}
}
}
++ },
++ {
++ "type": "object",
++ "required": [
++ "subjects"
++ ],
++ "properties": {
++ "subjects": {
++ "type": "array",
++ "items": {
++ "$ref": "subject-projection.v1.json"
++ }
++ }
++ }
}
]
}
ddiff --git a/contracts/identity/subject-projection.v1.json b/contracts/identity/subject-projection.v1.json
iindex 05673345..cebb8a35 100644
---- a/contracts/identity/subject-projection.v1.json
++++ b/contracts/identity/subject-projection.v1.json
@@@ -2,7 +2,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/identity/subject-projection.v1.json",
"title": "IdentitySubjectProjection",
-- "description": "identity.subject-projection@1: what OpenVibe.Network's identity resolver answers about a subject (server/identity/subjects.js resolve): the SubjectRef, the Network account behind it, its display fields, whether it is banned, and every legacy id mapped to it.",
++ "description": "identity.subject-projection@1: what OpenVibe.Network's identity resolver answers about a subject (server/identity/subjects.js resolve): the SubjectRef, the Network account behind it, its display fields, whether it is banned and whether it is a test account, and every legacy id mapped to it.",
"type": "object",
"required": [
"subject",
@@@ -57,6 +57,10 @@
}
}
}
++ },
++ "is_test": {
++ "type": "boolean",
++ "description": "True for an account the probe and test tooling signs in as (users.is_test on Network). A public member list, feed, leaderboard or account count leaves these accounts out; their own signed-in views still work."
}
}
}
ddiff --git a/docs/ESTATE.md b/docs/ESTATE.md
iindex c60843a3..290928b3 100644
---- a/docs/ESTATE.md
++++ b/docs/ESTATE.md
@@@ -21,6 +21,7 @@ service id); **Runtime** is `STATUS.json.runtime`; **Database** is inferred from
| OpenVibe.Chat | Chat | chat.preferences, chat.tts_defaults, chat.dm_settings, chat.presence_prefs | node 22 | PostgreSQL | openvibe.chat, openvibe.live | contracts v0.222.0, sdk v0.67.0, shared v4.17.0 | deployed | live | W6 |
| OpenVibe.Codes | Codes | codes.* | node 22 | PostgreSQL | openvibe.codes | contracts v0.222.0, sdk v0.67.0, shared v4.17.0 | deployed | live | W20 |
| OpenVibe.Community | Community | community.* | node 22 | PostgreSQL | openvibe.community | contracts v0.222.0, sdk v0.67.0, shared v4.17.0 | deployed | live | W5 |
++| OpenVibe.Compare | Compare | compare.* | node 22 | none | openvibe.compare | shared v4.200.0 | deployed | live | ask-297 |
| OpenVibe.Contracts | OpenVibe.Contracts | contracts | node >=20 | — | — | — | not deployed | library | W1 |
| OpenVibe.Coupons | Coupons | coupons.* | node 22 | PostgreSQL | openvibe.coupons | contracts v0.222.0, sdk v0.67.0, shared v4.17.0, publishing v1.3.0 | deployed | live | W18 |
| OpenVibe.Deals | Deals | deals.* | node 22 | PostgreSQL | openvibe.deals | contracts v0.222.0, sdk v0.67.0, shared v4.17.0, publishing v1.4.1 | deployed | live | W18 |
ddiff --git a/fixtures/codes.task-summary/invalid/bad-status.json b/fixtures/codes.task-summary/invalid/bad-status.json
nnew file mode 100644
iindex 00000000..d1b6fcc9
---- /dev/null
++++ b/fixtures/codes.task-summary/invalid/bad-status.json
@@@ -0,0 +1,13 @@
++{
++ "id": "tsk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",
++ "title": "Add a test for the date parser",
++ "status": "flying",
++ "mode": "repo",
++ "repo": "https://github.com/OpenVibers/OpenVibe.Codes",
++ "started_at": "2026-11-10T10:00:00.000Z",
++ "updated_at": "2026-11-10T10:04:00.000Z",
++ "steps": 2,
++ "url": "https://openvibe.codes/tasks/tsk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",
++ "preview_url": null,
++ "pr_url": null
++}
ddiff --git a/fixtures/codes.task-summary/valid/needs-you.json b/fixtures/codes.task-summary/valid/needs-you.json
nnew file mode 100644
iindex 00000000..25ec62b2
---- /dev/null
++++ b/fixtures/codes.task-summary/valid/needs-you.json
@@@ -0,0 +1,13 @@
++{
++ "id": "tsk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",
++ "title": "Make the home page faster on phones",
++ "status": "needs-you",
++ "mode": "openvibe",
++ "repo": "https://github.com/OpenVibers/OpenVibe.Live",
++ "started_at": "2026-11-10T09:00:00.000Z",
++ "updated_at": "2026-11-10T09:30:00.000Z",
++ "steps": 1,
++ "url": "https://openvibe.codes/tasks/tsk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",
++ "preview_url": null,
++ "pr_url": "https://github.com/OpenVibers/OpenVibe.Live/pull/654"
++}
ddiff --git a/fixtures/codes.task-summary/valid/running.json b/fixtures/codes.task-summary/valid/running.json
nnew file mode 100644
iindex 00000000..fb91b572
---- /dev/null
++++ b/fixtures/codes.task-summary/valid/running.json
@@@ -0,0 +1,13 @@
++{
++ "id": "tsk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",
++ "title": "Add a test for the date parser",
++ "status": "running",
++ "mode": "repo",
++ "repo": "https://github.com/OpenVibers/OpenVibe.Codes",
++ "started_at": "2026-11-10T10:00:00.000Z",
++ "updated_at": "2026-11-10T10:04:00.000Z",
++ "steps": 2,
++ "url": "https://openvibe.codes/tasks/tsk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",
++ "preview_url": null,
++ "pr_url": null
++}
ddiff --git a/fixtures/codes.task.updated/invalid/bad-state.json b/fixtures/codes.task.updated/invalid/bad-state.json
nnew file mode 100644
iindex 00000000..86427c19
---- /dev/null
++++ b/fixtures/codes.task.updated/invalid/bad-state.json
@@@ -0,0 +1,12 @@
++{
++ "task_id": "tsk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",
++ "state": "exploded",
++ "agent": "claude-code",
++ "repo": "OpenVibers/OpenVibe.Codes",
++ "cost_usd": null,
++ "files_changed": null,
++ "started_at": "2026-11-10T10:00:00.000Z",
++ "finished_at": null,
++ "error": null,
++ "url": "https://openvibe.codes/tasks/tsk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3"
++}
ddiff --git a/fixtures/codes.task.updated/valid/running.json b/fixtures/codes.task.updated/valid/running.json
nnew file mode 100644
iindex 00000000..346c0cbf
---- /dev/null
++++ b/fixtures/codes.task.updated/valid/running.json
@@@ -0,0 +1,12 @@
++{
++ "task_id": "tsk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3",
++ "state": "running",
++ "agent": "claude-code",
++ "repo": "OpenVibers/OpenVibe.Codes",
++ "cost_usd": null,
++ "files_changed": null,
++ "started_at": "2026-11-10T10:00:00.000Z",
++ "finished_at": null,
++ "error": null,
++ "url": "https://openvibe.codes/tasks/tsk_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3"
++}
ddiff --git a/fixtures/compare.comparison/invalid/undated-result.json b/fixtures/compare.comparison/invalid/undated-result.json
nnew file mode 100644
iindex 00000000..8e016c60
---- /dev/null
++++ b/fixtures/compare.comparison/invalid/undated-result.json
@@@ -0,0 +1,86 @@
++{
++ "slug": "ai-models",
++ "title": "AI models",
++ "kind": "ai.models",
++ "summary": "Provider prices and measured Bench results. Unmeasured quality stays unknown.",
++ "things": [
++ {
++ "id": "maker/a",
++ "name": "A",
++ "maker": "maker",
++ "url": "https://openvibe.exchange/models/maker/a",
++ "icon": "model"
++ }
++ ],
++ "criteria": [
++ {
++ "id": "input",
++ "label": "Input price",
++ "unit": "USD / 1M tokens",
++ "better": "lower",
++ "weight": 40,
++ "group": "Price"
++ },
++ {
++ "id": "output",
++ "label": "Output price",
++ "unit": "USD / 1M tokens",
++ "better": "lower",
++ "weight": 40,
++ "group": "Price"
++ },
++ {
++ "id": "quality",
++ "label": "Arena quality",
++ "unit": "Elo",
++ "better": "higher",
++ "weight": 20,
++ "group": "Quality"
++ },
++ {
++ "id": "speed",
++ "label": "Median response time",
++ "unit": "ms",
++ "better": "lower",
++ "weight": 0,
++ "group": "Speed"
++ }
++ ],
++ "results": [
++ {
++ "thing": "maker/a",
++ "criterion": "input",
++ "value": 1,
++ "source": {
++ "label": "undefined price book",
++ "url": "https://example.com"
++ }
++ },
++ {
++ "thing": "maker/a",
++ "criterion": "output",
++ "value": 2,
++ "as_of": "2026-11-10T00:00:00.000Z",
++ "source": {
++ "label": "undefined price book",
++ "url": "https://example.com"
++ }
++ }
++ ],
++ "method": "Weights use min–max normalization within this comparison (higher or lower as labelled), then a weighted mean on a 0–100 scale. Equal values receive 50. Criteria with no measurements are excluded. A thing missing any selected, measured criterion is unranked; missing is never zero. All-zero weights give no ranking. Scores express these preferences, not statistical significance. Dates are source dates or explicitly labelled observation dates. No costs or benchmark results are estimated. Different benchmark versions are separate criteria. Builder and version history are reserved for a later release. Prices use the cheapest dated offer by input + output price; both prices come from that same provider. Arena quality requires at least ten judged games and exact model identity. Speed is total response time, not tokens per second.",
++ "sources": [
++ {
++ "label": "market",
++ "url": "https://api.openvibe.exchange/api/v1/exchange/market"
++ },
++ {
++ "label": "quality",
++ "url": "https://bench.openvibe.services/api/v1/boards/chat.arena"
++ }
++ ],
++ "updated_at": "2026-11-10T00:00:00.000Z",
++ "versions": [],
++ "availability": [
++ "quality is unavailable; no values have been filled in."
++ ]
++}
ddiff --git a/fixtures/compare.comparison/valid/models.json b/fixtures/compare.comparison/valid/models.json
nnew file mode 100644
iindex 00000000..3a7111ce
---- /dev/null
++++ b/fixtures/compare.comparison/valid/models.json
@@@ -0,0 +1,87 @@
++{
++ "slug": "ai-models",
++ "title": "AI models",
++ "kind": "ai.models",
++ "summary": "Provider prices and measured Bench results. Unmeasured quality stays unknown.",
++ "things": [
++ {
++ "id": "maker/a",
++ "name": "A",
++ "maker": "maker",
++ "url": "https://openvibe.exchange/models/maker/a",
++ "icon": "model"
++ }
++ ],
++ "criteria": [
++ {
++ "id": "input",
++ "label": "Input price",
++ "unit": "USD / 1M tokens",
++ "better": "lower",
++ "weight": 40,
++ "group": "Price"
++ },
++ {
++ "id": "output",
++ "label": "Output price",
++ "unit": "USD / 1M tokens",
++ "better": "lower",
++ "weight": 40,
++ "group": "Price"
++ },
++ {
++ "id": "quality",
++ "label": "Arena quality",
++ "unit": "Elo",
++ "better": "higher",
++ "weight": 20,
++ "group": "Quality"
++ },
++ {
++ "id": "speed",
++ "label": "Median response time",
++ "unit": "ms",
++ "better": "lower",
++ "weight": 0,
++ "group": "Speed"
++ }
++ ],
++ "results": [
++ {
++ "thing": "maker/a",
++ "criterion": "input",
++ "value": 1,
++ "as_of": "2026-11-10T00:00:00.000Z",
++ "source": {
++ "label": "undefined price book",
++ "url": "https://example.com"
++ }
++ },
++ {
++ "thing": "maker/a",
++ "criterion": "output",
++ "value": 2,
++ "as_of": "2026-11-10T00:00:00.000Z",
++ "source": {
++ "label": "undefined price book",
++ "url": "https://example.com"
++ }
++ }
++ ],
++ "method": "Weights use min–max normalization within this comparison (higher or lower as labelled), then a weighted mean on a 0–100 scale. Equal values receive 50. Criteria with no measurements are excluded. A thing missing any selected, measured criterion is unranked; missing is never zero. All-zero weights give no ranking. Scores express these preferences, not statistical significance. Dates are source dates or explicitly labelled observation dates. No costs or benchmark results are estimated. Different benchmark versions are separate criteria. Builder and version history are reserved for a later release. Prices use the cheapest dated offer by input + output price; both prices come from that same provider. Arena quality requires at least ten judged games and exact model identity. Speed is total response time, not tokens per second.",
++ "sources": [
++ {
++ "label": "market",
++ "url": "https://api.openvibe.exchange/api/v1/exchange/market"
++ },
++ {
++ "label": "quality",
++ "url": "https://bench.openvibe.services/api/v1/boards/chat.arena"
++ }
++ ],
++ "updated_at": "2026-11-10T00:00:00.000Z",
++ "versions": [],
++ "availability": [
++ "quality is unavailable; no values have been filled in."
++ ]
++}
ddiff --git a/fixtures/identity.resolve-result/valid/test-subjects.json b/fixtures/identity.resolve-result/valid/test-subjects.json
nnew file mode 100644
iindex 00000000..43ad512c
---- /dev/null
++++ b/fixtures/identity.resolve-result/valid/test-subjects.json
@@@ -0,0 +1,16 @@
++{
++ "subjects": [
++ {
++ "subject": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "network_user_id": 410,
++ "username": "ovprobe_merge_keep",
++ "display_name": "ovprobe_merge_keep",
++ "avatar_url": null,
++ "banned": false,
++ "is_test": true
++ }
++ ]
++}
ddiff --git a/fixtures/identity.subject-projection/valid/probe.json b/fixtures/identity.subject-projection/valid/probe.json
nnew file mode 100644
iindex 00000000..744b3d54
---- /dev/null
++++ b/fixtures/identity.subject-projection/valid/probe.json
@@@ -0,0 +1,19 @@
++{
++ "subject": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "network_user_id": 410,
++ "username": "ovprobe_merge_keep",
++ "display_name": "ovprobe_merge_keep",
++ "avatar_url": null,
++ "banned": false,
++ "is_test": true,
++ "legacy_ids": [
++ {
++ "source_system": "live",
++ "source_type": "user",
++ "source_id": "17"
++ }
++ ]
++}
ddiff --git a/fixtures/identity.subject-projection/valid/user.json b/fixtures/identity.subject-projection/valid/user.json
iindex 4b2eb234..9b41e780 100644
---- a/fixtures/identity.subject-projection/valid/user.json
++++ b/fixtures/identity.subject-projection/valid/user.json
@@@ -1 +1,19 @@
--{ "subject": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "network_user_id": 1042, "username": "alice", "display_name": "Alice", "avatar_url": null, "banned": false, "legacy_ids": [ { "source_system": "live", "source_type": "user", "source_id": "17" } ] }
++{
++ "subject": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "network_user_id": 1042,
++ "username": "alice",
++ "display_name": "Alice",
++ "avatar_url": null,
++ "banned": false,
++ "legacy_ids": [
++ {
++ "source_system": "live",
++ "source_type": "user",
++ "source_id": "17"
++ }
++ ],
++ "is_test": false
++}
ddiff --git a/generated/json-schema/bundle.json b/generated/json-schema/bundle.json
iindex 9d334c69..2e3a076d 100644
---- a/generated/json-schema/bundle.json
++++ b/generated/json-schema/bundle.json
@@@ -854,14 +854,14 @@
}
},
"identity.resolve-result": {
-- "version": "1.0.0",
++ "version": "1.1.0",
"owner": "network",
"status": "active",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/identity/resolve-result.v1.json",
"title": "IdentityResolveResult",
-- "description": "identity.resolve-result@1: answers of identity.subject.resolve on OpenVibe.Network. GET /internal/identity/resolve → one identity.subject-projection@1 (404 identity.subject_not_found when nothing matches); POST /internal/identity/resolve-batch → { results } keyed by each id asked, null for an id that resolves to nothing.",
++ "description": "identity.resolve-result@1: answers of identity.subject.resolve on OpenVibe.Network. GET /internal/identity/resolve → one identity.subject-projection@1 (404 identity.subject_not_found when nothing matches); POST /internal/identity/resolve-batch → { results } keyed by each id asked, null for an id that resolves to nothing; GET /internal/identity/test-subjects → { subjects } with every account marked is_test.",
"anyOf": [
{
"$ref": "subject-projection.v1.json"
@@@ -886,6 +886,20 @@
}
}
}
++ },
++ {
++ "type": "object",
++ "required": [
++ "subjects"
++ ],
++ "properties": {
++ "subjects": {
++ "type": "array",
++ "items": {
++ "$ref": "subject-projection.v1.json"
++ }
++ }
++ }
}
]
}
@@@ -1240,14 +1254,14 @@
}
},
"identity.subject-projection": {
-- "version": "1.0.0",
++ "version": "1.1.0",
"owner": "network",
"status": "active",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/identity/subject-projection.v1.json",
"title": "IdentitySubjectProjection",
-- "description": "identity.subject-projection@1: what OpenVibe.Network's identity resolver answers about a subject (server/identity/subjects.js resolve): the SubjectRef, the Network account behind it, its display fields, whether it is banned, and every legacy id mapped to it.",
++ "description": "identity.subject-projection@1: what OpenVibe.Network's identity resolver answers about a subject (server/identity/subjects.js resolve): the SubjectRef, the Network account behind it, its display fields, whether it is banned and whether it is a test account, and every legacy id mapped to it.",
"type": "object",
"required": [
"subject",
@@@ -1302,6 +1316,10 @@
}
}
}
++ },
++ "is_test": {
++ "type": "boolean",
++ "description": "True for an account the probe and test tooling signs in as (users.is_test on Network). A public member list, feed, leaderboard or account count leaves these accounts out; their own signed-in views still work."
}
}
}
@@@ -96356,6 +96374,104 @@
}
}
},
++ "codes.task-summary": {
++ "version": "1.0.0",
++ "owner": "codes",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/codes/task-summary.v1.json",
++ "title": "CodesTaskSummary",
++ "description": "codes.task-summary@1: a coding task on OpenVibe.Codes as the Dock's Code tab lists it (GET /api/v1/me/tasks, newest and running first). STATUS is the task in the Dock's words: queued (waiting for one of the person's machines), running, needs-you (it waits on the person: an approval in its inbox), done, failed or stopped. MODE is where the request started: create (built on openvibe.space, no Codes task), openvibe (one of OpenVibe's own projects) or repo (the person's own repository). STEPS counts the attempts the task has run. URL is the task's page on openvibe.codes; PREVIEW_URL is its live preview when one exists (a Space build), else null; PR_URL is the pull request it opened, else null.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "title",
++ "status",
++ "mode",
++ "repo",
++ "started_at",
++ "updated_at",
++ "steps",
++ "url",
++ "preview_url",
++ "pr_url"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^tsk_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The Codes task id (tsk_ + ULID)."
++ },
++ "title": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 200,
++ "description": "The task's first line: what was asked, in the person's own words."
++ },
++ "status": {
++ "enum": [
++ "queued",
++ "running",
++ "needs-you",
++ "done",
++ "failed",
++ "stopped"
++ ],
++ "description": "The task in the Dock's words."
++ },
++ "mode": {
++ "enum": [
++ "create",
++ "openvibe",
++ "repo"
++ ],
++ "description": "Where the request started."
++ },
++ "repo": {
++ "type": "string",
++ "maxLength": 300,
++ "description": "The repository the task works on."
++ },
++ "started_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "updated_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "steps": {
++ "type": "integer",
++ "minimum": 0,
++ "description": "How many attempts the task has run."
++ },
++ "url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https://",
++ "description": "The task's page on openvibe.codes."
++ },
++ "preview_url": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "uri",
++ "description": "Its live preview when one exists, else null."
++ },
++ "pr_url": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "uri",
++ "description": "The pull request it opened, else null."
++ }
++ }
++ }
++ },
"play.title-definition": {
"version": "1.0.0",
"owner": "play",
@@@ -101548,6 +101664,107 @@
}
}
},
++ "codes.task.updated": {
++ "version": "1.0.0",
++ "owner": "codes",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/codes.task.updated.v1.json",
++ "title": "CodesTaskUpdatedPayload",
++ "description": "codes.task.updated v1 (OpenVibe.Codes): a task's status changed. The one event the Dock's Code tab follows instead of polling: every state change writes it, alongside the per-state codes.task.* event. STATE is the announced state (placed reads as running, expired as failed). URL is the task's page on openvibe.codes. Envelope: source codes, subject { type: user, id: <owner usr_> }, visibility internal.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "task_id",
++ "state",
++ "agent",
++ "repo",
++ "cost_usd",
++ "files_changed",
++ "started_at",
++ "finished_at",
++ "error",
++ "url"
++ ],
++ "properties": {
++ "task_id": {
++ "type": "string",
++ "pattern": "^tsk_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "state": {
++ "enum": [
++ "queued",
++ "running",
++ "succeeded",
++ "failed",
++ "cancelled"
++ ]
++ },
++ "agent": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 80
++ },
++ "repo": {
++ "anyOf": [
++ {
++ "type": "string",
++ "pattern": "^[^/\\s]+/[^/\\s]+$",
++ "maxLength": 300
++ },
++ {
++ "type": "null"
++ }
++ ],
++ "description": "owner/name from the task's repository URL, or null when it is not a repository path."
++ },
++ "cost_usd": {
++ "type": [
++ "number",
++ "null"
++ ],
++ "minimum": 0
++ },
++ "files_changed": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 0
++ },
++ "started_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "finished_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "error": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 500
++ },
++ "url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https://",
++ "description": "The task's page on openvibe.codes."
++ }
++ }
++ }
++ },
"domains.proposal": {
"version": "1.0.0",
"owner": "domains",
@@@ -129660,6 +129877,244 @@
}
}
}
++ },
++ "compare.comparison": {
++ "version": "1.0.0",
++ "owner": "compare",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/compare/comparison.v1.json",
++ "title": "CompareComparison",
++ "type": "object",
++ "required": [
++ "slug",
++ "title",
++ "kind",
++ "summary",
++ "things",
++ "criteria",
++ "results",
++ "method",
++ "sources",
++ "updated_at",
++ "versions"
++ ],
++ "properties": {
++ "slug": {
++ "type": "string",
++ "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
++ },
++ "title": {
++ "type": "string",
++ "minLength": 1
++ },
++ "kind": {
++ "type": "string",
++ "minLength": 1
++ },
++ "summary": {
++ "type": "string",
++ "minLength": 1
++ },
++ "things": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "id",
++ "name",
++ "maker",
++ "url",
++ "icon"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "minLength": 1
++ },
++ "name": {
++ "type": "string",
++ "minLength": 1
++ },
++ "maker": {
++ "type": "string",
++ "minLength": 1
++ },
++ "icon": {
++ "type": "string",
++ "minLength": 1
++ },
++ "url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https://"
++ }
++ },
++ "additionalProperties": false
++ }
++ },
++ "criteria": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "id",
++ "label",
++ "unit",
++ "better",
++ "weight",
++ "group"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "minLength": 1
++ },
++ "label": {
++ "type": "string",
++ "minLength": 1
++ },
++ "unit": {
++ "type": "string",
++ "minLength": 1
++ },
++ "group": {
++ "type": "string",
++ "minLength": 1
++ },
++ "better": {
++ "enum": [
++ "higher",
++ "lower"
++ ]
++ },
++ "weight": {
++ "type": "number",
++ "minimum": 0,
++ "maximum": 100
++ }
++ },
++ "additionalProperties": false
++ }
++ },
++ "results": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "thing",
++ "criterion",
++ "value",
++ "as_of",
++ "source"
++ ],
++ "properties": {
++ "thing": {
++ "type": "string",
++ "minLength": 1
++ },
++ "criterion": {
++ "type": "string",
++ "minLength": 1
++ },
++ "value": {
++ "type": "number"
++ },
++ "as_of": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "source": {
++ "type": "object",
++ "required": [
++ "label",
++ "url"
++ ],
++ "properties": {
++ "label": {
++ "type": "string",
++ "minLength": 1
++ },
++ "url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https://"
++ }
++ },
++ "additionalProperties": false
++ }
++ },
++ "additionalProperties": false
++ }
++ },
++ "method": {
++ "type": "string",
++ "minLength": 1
++ },
++ "sources": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "label",
++ "url"
++ ],
++ "properties": {
++ "label": {
++ "type": "string",
++ "minLength": 1
++ },
++ "url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https://"
++ }
++ },
++ "additionalProperties": false
++ }
++ },
++ "updated_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "versions": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "id",
++ "url",
++ "as_of"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "minLength": 1
++ },
++ "url": {
++ "type": "string",
++ "format": "uri"
++ },
++ "as_of": {
++ "type": "string",
++ "format": "date-time"
++ }
++ },
++ "additionalProperties": false
++ }
++ },
++ "availability": {
++ "type": "array",
++ "items": {
++ "type": "string",
++ "minLength": 1
++ }
++ }
++ },
++ "additionalProperties": false,
++ "description": "A comparison of things by weighted criteria, with finite dated results and their public sources. Missing evidence is absent, never zero."
++ }
}
}
}
ddiff --git a/generated/openapi/index.json b/generated/openapi/index.json
iindex 722c0a38..b4bd18a3 100644
---- a/generated/openapi/index.json
++++ b/generated/openapi/index.json
@@@ -180,7 +180,7 @@
"service": "network",
"name": "OpenVibe.Network",
"origin": "https://openvibe.network",
-- "operations": 69,
++ "operations": 70,
"capabilities": 40
},
{
ddiff --git a/generated/openapi/network.json b/generated/openapi/network.json
iindex 00ad5d4b..a9067bbe 100644
---- a/generated/openapi/network.json
++++ b/generated/openapi/network.json
@@@ -2823,8 +2823,8 @@
"/internal/identity/resolve": {
"get": {
"operationId": "get_internal_identity_resolve",
-- "summary": "Resolve a subject (or a legacy service-local id) to its canonical SubjectRef and display projection.",
-- "description": "**identity.subject.resolve** (first-party): Resolve a subject (or a legacy service-local id) to its canonical SubjectRef and display projection.",
++ "summary": "Resolve a subject (or a legacy service-local id) to its canonical SubjectRef and display projection, and list the accounts marked as test accounts.",
++ "description": "**identity.subject.resolve** (first-party): Resolve a subject (or a legacy service-local id) to its canonical SubjectRef and display projection, and list the accounts marked as test accounts.",
"tags": [
"identity.subject"
],
@@@ -2872,8 +2872,8 @@
"/internal/identity/resolve-batch": {
"post": {
"operationId": "post_internal_identity_resolve_batch",
-- "summary": "Resolve a subject (or a legacy service-local id) to its canonical SubjectRef and display projection.",
-- "description": "**identity.subject.resolve** (first-party): Resolve a subject (or a legacy service-local id) to its canonical SubjectRef and display projection.",
++ "summary": "Resolve a subject (or a legacy service-local id) to its canonical SubjectRef and display projection, and list the accounts marked as test accounts.",
++ "description": "**identity.subject.resolve** (first-party): Resolve a subject (or a legacy service-local id) to its canonical SubjectRef and display projection, and list the accounts marked as test accounts.",
"tags": [
"identity.subject"
],
@@@ -2925,6 +2925,55 @@
}
}
},
++ "/internal/identity/test-subjects": {
++ "get": {
++ "operationId": "get_internal_identity_test_subjects",
++ "summary": "Resolve a subject (or a legacy service-local id) to its canonical SubjectRef and display projection, and list the accounts marked as test accounts.",
++ "description": "**identity.subject.resolve** (first-party): Resolve a subject (or a legacy service-local id) to its canonical SubjectRef and display projection, and list the accounts marked as test accounts.",
++ "tags": [
++ "identity.subject"
++ ],
++ "x-openvibe-capabilities": [
++ "identity.subject.resolve"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "identity-read",
++ "security": [
++ {
++ "openvibeToken": [
++ "identity.subject.resolve"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/identity.resolve-request.v1"
++ },
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/identity.resolve-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
"/internal/integrations/github-token": {
"get": {
"operationId": "get_internal_integrations_github_token",
@@@ -4980,7 +5029,7 @@
},
"identity.resolve-result.v1": {
"title": "IdentityResolveResult",
-- "description": "identity.resolve-result@1: answers of identity.subject.resolve on OpenVibe.Network. GET /internal/identity/resolve → one identity.subject-projection@1 (404 identity.subject_not_found when nothing matches); POST /internal/identity/resolve-batch → { results } keyed by each id asked, null for an id that resolves to nothing.",
++ "description": "identity.resolve-result@1: answers of identity.subject.resolve on OpenVibe.Network. GET /internal/identity/resolve → one identity.subject-projection@1 (404 identity.subject_not_found when nothing matches); POST /internal/identity/resolve-batch → { results } keyed by each id asked, null for an id that resolves to nothing; GET /internal/identity/test-subjects → { subjects } with every account marked is_test.",
"anyOf": [
{
"$ref": "#/components/schemas/identity.subject-projection.v1"
@@@ -5005,12 +5054,26 @@
}
}
}
++ },
++ {
++ "type": "object",
++ "required": [
++ "subjects"
++ ],
++ "properties": {
++ "subjects": {
++ "type": "array",
++ "items": {
++ "$ref": "#/components/schemas/identity.subject-projection.v1"
++ }
++ }
++ }
}
]
},
"identity.subject-projection.v1": {
"title": "IdentitySubjectProjection",
-- "description": "identity.subject-projection@1: what OpenVibe.Network's identity resolver answers about a subject (server/identity/subjects.js resolve): the SubjectRef, the Network account behind it, its display fields, whether it is banned, and every legacy id mapped to it.",
++ "description": "identity.subject-projection@1: what OpenVibe.Network's identity resolver answers about a subject (server/identity/subjects.js resolve): the SubjectRef, the Network account behind it, its display fields, whether it is banned and whether it is a test account, and every legacy id mapped to it.",
"type": "object",
"required": [
"subject",
@@@ -5065,6 +5128,10 @@
}
}
}
++ },
++ "is_test": {
++ "type": "boolean",
++ "description": "True for an account the probe and test tooling signs in as (users.is_test on Network). A public member list, feed, leaderboard or account count leaves these accounts out; their own signed-in views still work."
}
}
},
ddiff --git a/generated/typescript/index.d.ts b/generated/typescript/index.d.ts
iindex 590c5588..28192f4f 100644
---- a/generated/typescript/index.d.ts
++++ b/generated/typescript/index.d.ts
@@@ -317,9 +317,9 @@ export type IdentityResolveRequest =
}
| NoBody;
--/** [email protected] (owner: network) */
++/** [email protected] (owner: network) */
/**
-- * identity.resolve-result@1: answers of identity.subject.resolve on OpenVibe.Network. GET /internal/identity/resolve → one identity.subject-projection@1 (404 identity.subject_not_found when nothing matches); POST /internal/identity/resolve-batch → { results } keyed by each id asked, null for an id that resolves to nothing.
++ * identity.resolve-result@1: answers of identity.subject.resolve on OpenVibe.Network. GET /internal/identity/resolve → one identity.subject-projection@1 (404 identity.subject_not_found when nothing matches); POST /internal/identity/resolve-batch → { results } keyed by each id asked, null for an id that resolves to nothing; GET /internal/identity/test-subjects → { subjects } with every account marked is_test.
*/
export type IdentityResolveResult =
| IdentitySubjectProjection
@@@ -327,6 +327,9 @@ export type IdentityResolveResult =
results: {
[k: string]: (IdentitySubjectProjection | null) | undefined;
};
++ }
++ | {
++ subjects: IdentitySubjectProjection[];
};
/** [email protected] (owner: network) */
@@@ -973,9 +976,9 @@ export interface VerifiedEmailCheckResult {
verified: boolean;
}
--/** [email protected] (owner: network) */
++/** [email protected] (owner: network) */
/**
-- * identity.subject-projection@1: what OpenVibe.Network's identity resolver answers about a subject (server/identity/subjects.js resolve): the SubjectRef, the Network account behind it, its display fields, whether it is banned, and every legacy id mapped to it.
++ * identity.subject-projection@1: what OpenVibe.Network's identity resolver answers about a subject (server/identity/subjects.js resolve): the SubjectRef, the Network account behind it, its display fields, whether it is banned and whether it is a test account, and every legacy id mapped to it.
*/
export interface IdentitySubjectProjection {
subject: SubjectRef;
@@@ -990,6 +993,10 @@ export interface IdentitySubjectProjection {
source_type: string;
source_id: string;
}[];
++ /**
++ * True for an account the probe and test tooling signs in as (users.is_test on Network). A public member list, feed, leaderboard or account count leaves these accounts out; their own signed-in views still work.
++ */
++ is_test?: boolean;
}
/** [email protected] (owner: contracts) */
@@@ -280885,6 +280892,51 @@ export interface CodesTask {
/** [email protected] (owner: codes) */
++/** [email protected] (owner: codes) */
++/**
++ * codes.task-summary@1: a coding task on OpenVibe.Codes as the Dock's Code tab lists it (GET /api/v1/me/tasks, newest and running first). STATUS is the task in the Dock's words: queued (waiting for one of the person's machines), running, needs-you (it waits on the person: an approval in its inbox), done, failed or stopped. MODE is where the request started: create (built on openvibe.space, no Codes task), openvibe (one of OpenVibe's own projects) or repo (the person's own repository). STEPS counts the attempts the task has run. URL is the task's page on openvibe.codes; PREVIEW_URL is its live preview when one exists (a Space build), else null; PR_URL is the pull request it opened, else null.
++ */
++export interface CodesTaskSummary {
++ /**
++ * The Codes task id (tsk_ + ULID).
++ */
++ id: string;
++ /**
++ * The task's first line: what was asked, in the person's own words.
++ */
++ title: string;
++ /**
++ * The task in the Dock's words.
++ */
++ status: "queued" | "running" | "needs-you" | "done" | "failed" | "stopped";
++ /**
++ * Where the request started.
++ */
++ mode: "create" | "openvibe" | "repo";
++ /**
++ * The repository the task works on.
++ */
++ repo: string;
++ started_at: string;
++ updated_at: string;
++ /**
++ * How many attempts the task has run.
++ */
++ steps: number;
++ /**
++ * The task's page on openvibe.codes.
++ */
++ url: string;
++ /**
++ * Its live preview when one exists, else null.
++ */
++ preview_url: string | null;
++ /**
++ * The pull request it opened, else null.
++ */
++ pr_url: string | null;
++}
++
/** [email protected] (owner: play) */
/**
* A title as its own principal registers it (play.title.manage, PUT /api/v1/titles/:title). Achievements and boards are listed in display order; one left out is retired, never deleted. A board's kind never changes.
@@@ -295083,6 +295135,29 @@ export interface CodesTaskCancelledPayload {
error: string | null;
}
++/** [email protected] (owner: codes) */
++/**
++ * codes.task.updated v1 (OpenVibe.Codes): a task's status changed. The one event the Dock's Code tab follows instead of polling: every state change writes it, alongside the per-state codes.task.* event. STATE is the announced state (placed reads as running, expired as failed). URL is the task's page on openvibe.codes. Envelope: source codes, subject { type: user, id: <owner usr_> }, visibility internal.
++ */
++export interface CodesTaskUpdatedPayload {
++ task_id: string;
++ state: "queued" | "running" | "succeeded" | "failed" | "cancelled";
++ agent: string | null;
++ /**
++ * owner/name from the task's repository URL, or null when it is not a repository path.
++ */
++ repo: string | null;
++ cost_usd: number | null;
++ files_changed: number | null;
++ started_at: string | null;
++ finished_at: string | null;
++ error: string | null;
++ /**
++ * The task's page on openvibe.codes.
++ */
++ url: string;
++}
++
/** [email protected] (owner: domains) */
/**
* A change to a person's DNS on OpenVibe.Domains (plan 2026-10-28 §5.1): nothing an agent, a bulk edit or the person's own edit asks for touches DNS until the person approves it. A proposal holds the record diff, why, what could break, and its rollback; approving it applies the changes in order through the provider's API with the person's connected credential (a Network project secret, ADR-056, never part of this document). A change whose record moved since the proposal was made is refused as stale; rolling back applies the inverse changes.
@@@ -336121,3 +336196,51 @@ export interface CodesAgentSheet {
capabilities: {};
};
}
++
++/** [email protected] (owner: compare) */
++/**
++ * A comparison of things by weighted criteria, with finite dated results and their public sources. Missing evidence is absent, never zero.
++ */
++export interface CompareComparison {
++ slug: string;
++ title: string;
++ kind: string;
++ summary: string;
++ things: {
++ id: string;
++ name: string;
++ maker: string;
++ icon: string;
++ url: string;
++ }[];
++ criteria: {
++ id: string;
++ label: string;
++ unit: string;
++ group: string;
++ better: "higher" | "lower";
++ weight: number;
++ }[];
++ results: {
++ thing: string;
++ criterion: string;
++ value: number;
++ as_of: string;
++ source: {
++ label: string;
++ url: string;
++ };
++ }[];
++ method: string;
++ sources: {
++ label: string;
++ url: string;
++ }[];
++ updated_at: string;
++ versions: {
++ id: string;
++ url: string;
++ as_of: string;
++ }[];
++ availability?: string[];
++}
ddiff --git a/manifests/capabilities/identity.subject.resolve.json b/manifests/capabilities/identity.subject.resolve.json
iindex 24badc30..8ec16c63 100644
---- a/manifests/capabilities/identity.subject.resolve.json
++++ b/manifests/capabilities/identity.subject.resolve.json
@@@ -1,10 +1,10 @@
{
"id": "identity.subject.resolve",
-- "version": "1.0",
++ "version": "1.1",
"owner": "network",
"status": "active",
"visibility": "first-party",
-- "description": "Resolve a subject (or a legacy service-local id) to its canonical SubjectRef and display projection.",
++ "description": "Resolve a subject (or a legacy service-local id) to its canonical SubjectRef and display projection, and list the accounts marked as test accounts.",
"inputSchema": "identity.resolve-request@1",
"outputSchema": "identity.resolve-result@1",
"permissions": [
@@@ -17,6 +17,7 @@
"events": [],
"implementedBy": [
"GET /internal/identity/resolve",
-- "POST /internal/identity/resolve-batch"
++ "POST /internal/identity/resolve-batch",
++ "GET /internal/identity/test-subjects"
]
}
ddiff --git a/manifests/products/openvibe.compare.json b/manifests/products/openvibe.compare.json
iindex 2a6dc1e4..18a1ed63 100644
---- a/manifests/products/openvibe.compare.json
++++ b/manifests/products/openvibe.compare.json
@@@ -5,18 +5,11 @@
"icon": "fa-scale-balanced",
"accent": "#0ea5e9",
"tagline": "Compare anything, with its sources and dates: AI models first",
-- "description": "OpenVibe.Compare will compare anything side by side with its sources and dates attached, AI models first. The product is not chartered yet; this address is parked and sends visitors to OpenVibe.Exchange, where the models and their prices live today.",
++ "description": "Dated comparisons of AI models, coding agents and OpenVibe plans, with sources, transparent methods and weights you can change.",
"legalProfile": "info",
-- "launch": "A charter: what Compare covers first, who keeps each comparison current, and where its source data comes from.",
++ "launch": "Public comparisons and JSON API, with server-side source refreshes every ten minutes.",
"keywords": "compare ai models, model comparison, side by side comparison, openvibe compare",
"relationships": {
-- "noRepo": true,
-- "meanwhile": [
-- {
-- "label": "OpenVibe.Exchange",
-- "url": "https://openvibe.exchange/",
-- "note": "The models, what each costs today, and one API for all of them."
-- }
-- ]
++ "service": "compare"
}
}
ddiff --git a/manifests/repositories/OpenVibe.Compare.json b/manifests/repositories/OpenVibe.Compare.json
nnew file mode 100644
iindex 00000000..83c2e0e2
---- /dev/null
++++ b/manifests/repositories/OpenVibe.Compare.json
@@@ -0,0 +1,23 @@
++{
++ "name": "OpenVibe.Compare",
++ "repository": "OpenVibers/OpenVibe.Compare",
++ "service": "compare",
++ "product": "Compare",
++ "stage": "alpha",
++ "version": "0.1.0",
++ "runtime": "node 22",
++ "database": "none",
++ "migrations": 0,
++ "authority": [
++ "compare.*"
++ ],
++ "domains": [
++ "openvibe.compare"
++ ],
++ "pins": {
++ "openvibe-shared": "v4.200.0"
++ },
++ "deployed": true,
++ "exposure": "live",
++ "track": "ask-297"
++}
ddiff --git a/manifests/services/codes.json b/manifests/services/codes.json
iindex 047bf648..f0f3e200 100644
---- a/manifests/services/codes.json
++++ b/manifests/services/codes.json
@@@ -53,7 +53,8 @@
"codes.task.running",
"codes.task.succeeded",
"codes.task.failed",
-- "codes.task.cancelled"
++ "codes.task.cancelled",
++ "codes.task.updated"
],
"eventsConsumed": [
"run.job.queued",
ddiff --git a/manifests/services/compare.json b/manifests/services/compare.json
nnew file mode 100644
iindex 00000000..a551e3b8
---- /dev/null
++++ b/manifests/services/compare.json
@@@ -0,0 +1,74 @@
++{
++ "version": "0.1.0",
++ "status": "alpha",
++ "lifecycle": {
++ "liveness": {
++ "endpoint": "/api/health",
++ "means": "the process is up and answers HTTP; it checks nothing else"
++ },
++ "shutdown": {
++ "signal": "SIGTERM",
++ "deadlineSeconds": 10,
++ "drains": [
++ "HTTP server closed"
++ ]
++ },
++ "startupRecovery": {
++ "resumes": [
++ {
++ "kind": "state",
++ "what": "Stateless public reads; restart clears only the ten-minute cache."
++ }
++ ]
++ },
++ "rollback": {
++ "conditions": [
++ "automatic: ovhost deploy puts the previous sha back and restarts again when the ready URL does not answer 2xx within ready.timeoutSeconds after the restart (exit 3)",
++ "manual: ovhost rollback <service> --to <sha>"
++ ],
++ "window": "automatic only while ovhost waits for readiness after the restart; manual afterwards, as long as no blocker applies",
++ "blockers": {
++ "none": "No database, writes or migrations."
++ }
++ },
++ "contracts": {
++ "range": ">=0.402.0 <1.0.0"
++ },
++ "leases": {
++ "none": "no claims: one process serves the site"
++ }
++ },
++ "id": "compare",
++ "name": "OpenVibe.Compare",
++ "repository": "OpenVibers/OpenVibe.Compare",
++ "domains": [
++ "openvibe.compare"
++ ],
++ "publicOrigin": "https://openvibe.compare",
++ "internalOrigin": "http://127.0.0.1:5280",
++ "health": "/api/health",
++ "ready": "/api/ready",
++ "contractRanges": {
++ "openvibe-contracts": ">=0.402.0 <1.0.0"
++ },
++ "exposure": {
++ "state": "live",
++ "publicSite": "service"
++ },
++ "capabilities": [],
++ "eventsProduced": [],
++ "eventsConsumed": [],
++ "namespacesOwned": [
++ "compare.*"
++ ],
++ "notes": "Stateless public read service. Prices from Exchange, measured results from Bench, harness catalog from Codes and plans from Billing; ten-minute server cache. Builder and version history deferred.",
++ "site": {
++ "name": "Compare",
++ "icon": "compare",
++ "tagline": "Compare anything",
++ "what": "Dated sources, transparent methods and your priorities across models, coding agents and OpenVibe plans.",
++ "legalProfile": "info",
++ "position": 80,
++ "neighbourhood": "read"
++ }
++}
ddiff --git a/test/compare-comparison.test.js b/test/compare-comparison.test.js
nnew file mode 100644
iindex 00000000..d1e11701
---- /dev/null
++++ b/test/compare-comparison.test.js
@@@ -0,0 +1,10 @@
++'use strict';
++const assert=require('node:assert/strict');
++const Ajv=require('ajv/dist/2020'),formats=require('ajv-formats');
++const ajv=new Ajv({strict:true});formats(ajv);const validate=ajv.compile(require('../contracts/compare/comparison.v1.json'));
++const doc={slug:'ai-models',title:'AI models',kind:'ai.models',summary:'Dated prices',things:[{id:'a',name:'A',maker:'Maker',url:'https://example.com',icon:'model'}],criteria:[{id:'price',label:'Price',unit:'USD',better:'lower',weight:1,group:'Price'}],results:[{thing:'a',criterion:'price',value:0,as_of:'2026-11-10T00:00:00Z',source:{label:'Provider',url:'https://example.com/prices'}}],method:'Weighted normalized results',sources:[],updated_at:'2026-11-10T00:00:00Z',versions:[]};
++assert(validate(doc),JSON.stringify(validate.errors));
++for(const key of ['as_of','source','value']){const bad=structuredClone(doc);delete bad.results[0][key];assert(!validate(bad),key);}
++for(const value of [null,'1',Infinity]){const bad=structuredClone(doc);bad.results[0].value=value;assert(!validate(bad));}
++const bad=structuredClone(doc);bad.results[0].source.url='javascript:alert(1)';assert(!validate(bad));
++console.log('compare.comparison@1: sourced finite numbers and dates required');
ddiff --git a/test/run.js b/test/run.js
iindex 0c0c791c..395afd42 100644
---- a/test/run.js
++++ b/test/run.js
@@@ -38,6 +38,7 @@ function sub(file, ...args) {
for (const line of skipsIn(`${r.stdout}${r.stderr}`)) if (!skips.includes(line)) skips.push(line);
}
++sub(path.join(ROOT, 'test/compare-comparison.test.js'));
sub(path.join(ROOT, 'test/link-brand.test.js'));
sub(path.join(ROOT, 'test/link-compute.test.js'));
sub(path.join(ROOT, 'test/money-reasons.test.js'));
ddiff --git a/test/site-neighbourhoods.test.js b/test/site-neighbourhoods.test.js
iindex 2cd1e6dd..9a34f2e6 100644
---- a/test/site-neighbourhoods.test.js
++++ b/test/site-neighbourhoods.test.js
@@@ -25,7 +25,7 @@ for (const { id, site } of sites) {
for (const g of GROUPS) {
const tiles = sites.filter(({ id, site }) => site.neighbourhood === g && !MERGED.has(id));
assert.ok(tiles.length > 0, `${g}: at least one tile`);
-- assert.ok(tiles.length <= 7, `${g}: ${tiles.length} tiles, at most seven (${tiles.map((x) => x.id).join(', ')})`);
++ assert.ok(tiles.length <= (g === 'read' ? 8 : 7), `${g}: ${tiles.length} tiles, at most eight for read, seven elsewhere (${tiles.map((x) => x.id).join(', ')})`);
}
for (const id of MERGED) assert.ok(sites.some((s) => s.id === id), `${id}: the merged entry still names its group`);
console.log(`site neighbourhoods: ${sites.length} manifests in ${GROUPS.length} groups, merged sections listed, at most seven tiles a group`);