openvibers/openvibe.contracts Commits

v0.55.0: lifecycle declarations (WS-P task 1): registry.service-manifest@1 gains an optional lifecycle block (liveness endpoint and what it proves; shutdown signal, deadlineSeconds, drains, workers, note; startupRecovery resumes by kind; rollback conditions, window and blockers; contracts.range; leases claims with holder, expiry and fencing; { none: reason } where a part does not apply), filled for all 31 manifests from each service's code and systemd units; tests: every manifest declares all six, running services have a concrete liveness (their health path) and shutdown, libraries none, contracts.range equals contractRanges, every lifecycle field is described; fixtures for a lifecycle, a library and five invalid shapes

OP OpenVibers committed 26 Sept 2026 · 22fd274ee2 · 70 files, +2997 −53


Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex ae3fb16f..0c5cae64 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -4,6 +4,18 @@ All notable changes to `openvibe-contracts`. Releases are git tags (`vX.Y.Z`) th
from `https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/<tag>`. Before v0.30.0,
the notes were in the tag and commit messages (`git tag -n1`).
++## 0.55.0 — 2026-09-26
++
++**Lifecycle declarations** (roadmap WS-P task 1). `registry.service-manifest@1` gains an optional `lifecycle` block, and every one of the 31 manifests fills it from its service's code and systemd units:
++- `liveness`: the endpoint (the manifest's `health`) and what a 200 from it proves;
++- `shutdown`: `signal`, `deadlineSeconds` (the longest the process takes from the signal to exiting, as its own forced-exit timer bounds it), `drains` in order, optional `workers` (worker units that drain on their own, OpenRe's) and a `note` for gaps (the Tools gateway has no SIGTERM handler; Games and Community drain almost nothing);
++- `startupRecovery.resumes`: what the next start picks up, each as outbox, jobs, sessions, consumer, schedule or state;
++- `rollback`: `conditions` (automatic and manual), `window` and `blockers` (forward-only migrations, table rebuilds, authority switches);
++- `contracts.range`: the openvibe-contracts versions accepted, the same as `contractRanges`;
++- `leases.claims`: what is claimed, by whom, when it lapses and how a stale holder is fenced.
++
++A part that does not apply is `{ "none": "<why>" }` (libraries, the Examples repository, retired Realtime, static Sites). `npm test` checks that every manifest declares all six, that a running service's liveness endpoint is its `health` and its shutdown is concrete, that a library declares none for liveness, shutdown and recovery, that `contracts.range` matches `contractRanges`, and that every lifecycle field has a description. `ovhost validate` (OpenVibe.Host) refuses a host service whose lifecycle lacks a field or whose deadline exceeds its unit's TimeoutStopSec. Additive.
++
## 0.54.0 — 2026-09-26
**`media.moderation.action`** (ADR-022, WS-D task 1): Media's staff actions on someone else's media, which are retention holds placed or released and staff deletions or visibility changes. The payload is common.moderation-action@1, produced by media and consumed by network.
ddiff --git a/README.md b/README.md
iindex 5f8f5e04..7f83fd68 100644
---- a/README.md
++++ b/README.md
@@@ -188,6 +188,21 @@ Tools' problem codes are listed with the routes that answer them (`tools.run-req
`503 tools.unavailable` (a program or upstream a tool needs is missing), and, with `Retry-After`,
`429 tools.quota.exceeded` and `503 tools.busy`.
++## Lifecycle declarations (v0.55)
++
++Each service manifest has a `lifecycle` block, filled from the service's code and systemd units (roadmap WS-P task 1). A part that does not apply is `{ "none": "<why>" }`.
++
++| Field | What it says |
++|---|---|
++| `liveness` | `endpoint` (the manifest's `health`) and `means`: what a 200 proves. Readiness stays `ready`. |
++| `shutdown` | `signal`, `deadlineSeconds` (signal to exit, as the process's own forced-exit timer bounds it), `drains` in order, optional `workers` (worker units that drain on their own) and `note` |
++| `startupRecovery` | `resumes`: what the next start picks up, each `{ kind: outbox\|jobs\|sessions\|consumer\|schedule\|state, what }` |
++| `rollback` | `conditions` (automatic and manual), `window`, `blockers` (forward-only migrations, table rebuilds, authority switches) |
++| `contracts` | `range`: the openvibe-contracts versions accepted, equal to `contractRanges['openvibe-contracts']` |
++| `leases` | `claims`: `{ what, holder, expires, fencing }` for each thing claimed so that one owner acts at a time |
++
++`ovhost validate <service>` (OpenVibe.Host) reads the block from the inventory or from this package's manifest. It fails when a field is missing, when `deadlineSeconds` exceeds the unit's TimeoutStopSec or the unit's KillSignal is a different signal, and when the checkout's installed openvibe-contracts is outside `contracts.range`.
++
## Versioning and compatibility
- A contract id is permanent. Minor versions only add optional fields.
ddiff --git a/contracts/registry/service-manifest.v1.json b/contracts/registry/service-manifest.v1.json
iindex a3c4f7a6..cfe7d4b5 100644
---- a/contracts/registry/service-manifest.v1.json
++++ b/contracts/registry/service-manifest.v1.json
@@@ -190,6 +190,405 @@
"type": "string"
}
},
++ "lifecycle": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "liveness",
++ "shutdown",
++ "startupRecovery",
++ "rollback",
++ "contracts",
++ "leases"
++ ],
++ "description": "How the service starts, stops, recovers and is rolled back (roadmap WS-P task 1), taken from its code and its systemd units, never from intent. A part that does not apply is { \"none\": \"<why>\" }. `ovhost validate` refuses a host service whose lifecycle lacks a field, whose shutdown deadline exceeds its unit's TimeoutStopSec, or whose installed openvibe-contracts is outside contracts.range.",
++ "properties": {
++ "liveness": {
++ "description": "The endpoint that says the process is alive, and what a 200 from it proves. Readiness (can it serve?) is `ready`.",
++ "oneOf": [
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "endpoint",
++ "means"
++ ],
++ "properties": {
++ "endpoint": {
++ "type": "string",
++ "pattern": "^/",
++ "description": "A path on internalOrigin; the same as `health`."
++ },
++ "means": {
++ "type": "string",
++ "minLength": 10,
++ "maxLength": 300,
++ "description": "What a 200 proves, and what it does not check."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "none"
++ ],
++ "description": "No liveness endpoint, and why (a library, a static site, a retired service, or code that has none).",
++ "properties": {
++ "none": {
++ "type": "string",
++ "minLength": 3,
++ "maxLength": 300,
++ "description": "Why there is none."
++ }
++ }
++ }
++ ]
++ },
++ "shutdown": {
++ "description": "What happens between the stop signal and the process exiting.",
++ "oneOf": [
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "signal",
++ "deadlineSeconds",
++ "drains"
++ ],
++ "properties": {
++ "signal": {
++ "enum": [
++ "SIGTERM",
++ "SIGINT",
++ "SIGQUIT",
++ "SIGHUP"
++ ],
++ "description": "The signal the unit sends (KillSignal=, systemd's default SIGTERM) and the process handles."
++ },
++ "deadlineSeconds": {
++ "type": "number",
++ "minimum": 0,
++ "maximum": 86400,
++ "description": "The longest the process takes from the signal to exiting, as its own code bounds it (a forced-exit timer); 0 when it exits at once. Must not exceed the unit's TimeoutStopSec, after which systemd kills it mid-drain."
++ },
++ "drains": {
++ "type": "array",
++ "minItems": 1,
++ "items": {
++ "type": "string",
++ "minLength": 3,
++ "maxLength": 240
++ },
++ "description": "What it stops, finishes, flushes or closes before exiting, in order."
++ },
++ "workers": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "deadlineSeconds",
++ "drains"
++ ],
++ "description": "Worker units the service owns that drain on their own (ovhost never restarts them): how long a stop signal lets them run.",
++ "properties": {
++ "deadlineSeconds": {
++ "type": "number",
++ "minimum": 0,
++ "maximum": 604800,
++ "description": "The longest a worker keeps running after the stop signal, as its code bounds it."
++ },
++ "drains": {
++ "type": "array",
++ "minItems": 1,
++ "items": {
++ "type": "string",
++ "minLength": 3,
++ "maxLength": 240
++ },
++ "description": "What a worker keeps serving, and what it ends, before exiting."
++ }
++ }
++ },
++ "note": {
++ "type": "string",
++ "minLength": 3,
++ "maxLength": 400,
++ "description": "Anything the fields above do not say (a gap the code has, a handler that is missing)."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "none"
++ ],
++ "description": "No shutdown sequence, and why (a library, a static site, a retired service, or code that has none).",
++ "properties": {
++ "none": {
++ "type": "string",
++ "minLength": 3,
++ "maxLength": 300,
++ "description": "Why there is none."
++ }
++ }
++ }
++ ]
++ },
++ "startupRecovery": {
++ "description": "Work a stop or a crash interrupted that the next start picks up.",
++ "oneOf": [
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "resumes"
++ ],
++ "properties": {
++ "resumes": {
++ "type": "array",
++ "minItems": 1,
++ "description": "Each thing the process resumes at boot.",
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "kind",
++ "what"
++ ],
++ "properties": {
++ "kind": {
++ "enum": [
++ "outbox",
++ "jobs",
++ "sessions",
++ "consumer",
++ "schedule",
++ "state"
++ ],
++ "description": "outbox: unsent events; jobs: queued or interrupted background work; sessions: client connections or media sessions; consumer: an event or change feed redelivered or read from a stored cursor; schedule: timed work that fell due; state: in-memory state rebuilt from storage."
++ },
++ "what": {
++ "type": "string",
++ "minLength": 10,
++ "maxLength": 300,
++ "description": "What is resumed, and how."
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "none"
++ ],
++ "description": "No startup recovery, and why (a library, a static site, a retired service, or code that has none).",
++ "properties": {
++ "none": {
++ "type": "string",
++ "minLength": 3,
++ "maxLength": 300,
++ "description": "Why there is none."
++ }
++ }
++ }
++ ]
++ },
++ "rollback": {
++ "description": "When a release is rolled back, for how long that happens by itself, and what makes going back unsafe.",
++ "oneOf": [
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "conditions",
++ "window",
++ "blockers"
++ ],
++ "properties": {
++ "conditions": {
++ "type": "array",
++ "minItems": 1,
++ "items": {
++ "type": "string",
++ "minLength": 10,
++ "maxLength": 300
++ },
++ "description": "What triggers a rollback: automatic (readiness not reached after the restart) and manual."
++ },
++ "window": {
++ "type": "string",
++ "minLength": 10,
++ "maxLength": 300,
++ "description": "How long after a deploy the rollback is automatic, and what bounds a manual one."
++ },
++ "blockers": {
++ "description": "Changes a previous release cannot live with (forward-only migrations, table rebuilds, authority switches), or { none } with the reason.",
++ "oneOf": [
++ {
++ "type": "array",
++ "minItems": 1,
++ "items": {
++ "type": "string",
++ "minLength": 10,
++ "maxLength": 300
++ }
++ },
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "none"
++ ],
++ "description": "No rollback blockers, and why (a library, a static site, a retired service, or code that has none).",
++ "properties": {
++ "none": {
++ "type": "string",
++ "minLength": 3,
++ "maxLength": 300,
++ "description": "Why there is none."
++ }
++ }
++ }
++ ]
++ }
++ }
++ },
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "none"
++ ],
++ "description": "No rollback, and why (a library, a static site, a retired service, or code that has none).",
++ "properties": {
++ "none": {
++ "type": "string",
++ "minLength": 3,
++ "maxLength": 300,
++ "description": "Why there is none."
++ }
++ }
++ }
++ ]
++ },
++ "contracts": {
++ "description": "The openvibe-contracts versions the service accepts.",
++ "oneOf": [
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "range"
++ ],
++ "properties": {
++ "range": {
++ "type": "string",
++ "pattern": "^(\\^|>=)?\\d+\\.\\d+\\.\\d+( <\\d+\\.\\d+\\.\\d+)?$",
++ "description": "The accepted versions, the same range as contractRanges['openvibe-contracts']."
++ },
++ "note": {
++ "type": "string",
++ "minLength": 3,
++ "maxLength": 300,
++ "description": "Where the range comes from, when it is not the obvious one."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "none"
++ ],
++ "description": "No contracts range, and why (a library, a static site, a retired service, or code that has none).",
++ "properties": {
++ "none": {
++ "type": "string",
++ "minLength": 3,
++ "maxLength": 300,
++ "description": "Why there is none."
++ }
++ }
++ }
++ ]
++ },
++ "leases": {
++ "description": "What the service claims so that one owner acts at a time, and how a stale owner is fenced.",
++ "oneOf": [
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "claims"
++ ],
++ "properties": {
++ "claims": {
++ "type": "array",
++ "minItems": 1,
++ "description": "Each thing claimed, with its holder, expiry and fencing.",
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "what",
++ "holder",
++ "expires",
++ "fencing"
++ ],
++ "properties": {
++ "what": {
++ "type": "string",
++ "minLength": 3,
++ "maxLength": 200,
++ "description": "What is claimed."
++ },
++ "holder": {
++ "type": "string",
++ "minLength": 3,
++ "maxLength": 200,
++ "description": "Who holds the claim (a process, a worker id, a row)."
++ },
++ "expires": {
++ "type": "string",
++ "minLength": 3,
++ "maxLength": 200,
++ "description": "When the claim lapses and how it is renewed."
++ },
++ "fencing": {
++ "type": "string",
++ "minLength": 3,
++ "maxLength": 300,
++ "description": "How a stale owner is kept from acting after its claim lapsed."
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "none"
++ ],
++ "description": "No leases, and why (a library, a static site, a retired service, or code that has none).",
++ "properties": {
++ "none": {
++ "type": "string",
++ "minLength": 3,
++ "maxLength": 300,
++ "description": "Why there is none."
++ }
++ }
++ }
++ ]
++ }
++ }
++ },
"notes": {
"type": "string"
}
ddiff --git a/fixtures/registry.service-manifest/invalid/lifecycle-claim-without-fencing.json b/fixtures/registry.service-manifest/invalid/lifecycle-claim-without-fencing.json
nnew file mode 100644
iindex 00000000..6bce7a7f
---- /dev/null
++++ b/fixtures/registry.service-manifest/invalid/lifecycle-claim-without-fencing.json
@@@ -0,0 +1,72 @@
++{
++ "version": "0.1.0",
++ "contractRanges": {
++ "openvibe-contracts": "^0.1.0"
++ },
++ "id": "media",
++ "name": "OpenVibe.Media",
++ "status": "beta",
++ "repository": "OpenVibers/OpenVibe.Media",
++ "domains": [
++ "openvibe.media"
++ ],
++ "publicOrigin": "https://openvibe.media",
++ "health": "/healthz",
++ "capabilities": [
++ "media.object.upload",
++ "media.object.read"
++ ],
++ "eventsProduced": [],
++ "eventsConsumed": [],
++ "namespacesOwned": [
++ "media.*"
++ ],
++ "lifecycle": {
++ "liveness": {
++ "endpoint": "/healthz",
++ "means": "the process is up and answers HTTP; it checks nothing else"
++ },
++ "shutdown": {
++ "signal": "SIGTERM",
++ "deadlineSeconds": 70,
++ "drains": [
++ "recordings stopped",
++ "HTTP server closed, then the database"
++ ],
++ "note": "forced exit after 70 s"
++ },
++ "startupRecovery": {
++ "resumes": [
++ {
++ "kind": "jobs",
++ "what": "jobs left running are retried or failed"
++ },
++ {
++ "kind": "outbox",
++ "what": "unsent events go to OpenVibe.Events"
++ }
++ ]
++ },
++ "rollback": {
++ "conditions": [
++ "automatic: readiness not reached after the restart"
++ ],
++ "window": "while the deploy waits for readiness",
++ "blockers": {
++ "none": "the schema code only adds"
++ }
++ },
++ "contracts": {
++ "range": "^0.1.0"
++ },
++ "leases": {
++ "claims": [
++ {
++ "what": "a job row",
++ "holder": "the Media process",
++ "expires": "lease_until, renewed while running"
++ }
++ ]
++ }
++ }
++}
ddiff --git a/fixtures/registry.service-manifest/invalid/lifecycle-no-deadline.json b/fixtures/registry.service-manifest/invalid/lifecycle-no-deadline.json
nnew file mode 100644
iindex 00000000..ee1f5a95
---- /dev/null
++++ b/fixtures/registry.service-manifest/invalid/lifecycle-no-deadline.json
@@@ -0,0 +1,72 @@
++{
++ "version": "0.1.0",
++ "contractRanges": {
++ "openvibe-contracts": "^0.1.0"
++ },
++ "id": "media",
++ "name": "OpenVibe.Media",
++ "status": "beta",
++ "repository": "OpenVibers/OpenVibe.Media",
++ "domains": [
++ "openvibe.media"
++ ],
++ "publicOrigin": "https://openvibe.media",
++ "health": "/healthz",
++ "capabilities": [
++ "media.object.upload",
++ "media.object.read"
++ ],
++ "eventsProduced": [],
++ "eventsConsumed": [],
++ "namespacesOwned": [
++ "media.*"
++ ],
++ "lifecycle": {
++ "liveness": {
++ "endpoint": "/healthz",
++ "means": "the process is up and answers HTTP; it checks nothing else"
++ },
++ "shutdown": {
++ "signal": "SIGTERM",
++ "drains": [
++ "recordings stopped",
++ "HTTP server closed, then the database"
++ ],
++ "note": "forced exit after 70 s"
++ },
++ "startupRecovery": {
++ "resumes": [
++ {
++ "kind": "jobs",
++ "what": "jobs left running are retried or failed"
++ },
++ {
++ "kind": "outbox",
++ "what": "unsent events go to OpenVibe.Events"
++ }
++ ]
++ },
++ "rollback": {
++ "conditions": [
++ "automatic: readiness not reached after the restart"
++ ],
++ "window": "while the deploy waits for readiness",
++ "blockers": {
++ "none": "the schema code only adds"
++ }
++ },
++ "contracts": {
++ "range": "^0.1.0"
++ },
++ "leases": {
++ "claims": [
++ {
++ "what": "a job row",
++ "holder": "the Media process",
++ "expires": "lease_until, renewed while running",
++ "fencing": "status must still be running"
++ }
++ ]
++ }
++ }
++}
ddiff --git a/fixtures/registry.service-manifest/invalid/lifecycle-no-leases.json b/fixtures/registry.service-manifest/invalid/lifecycle-no-leases.json
nnew file mode 100644
iindex 00000000..fc921dfd
---- /dev/null
++++ b/fixtures/registry.service-manifest/invalid/lifecycle-no-leases.json
@@@ -0,0 +1,63 @@
++{
++ "version": "0.1.0",
++ "contractRanges": {
++ "openvibe-contracts": "^0.1.0"
++ },
++ "id": "media",
++ "name": "OpenVibe.Media",
++ "status": "beta",
++ "repository": "OpenVibers/OpenVibe.Media",
++ "domains": [
++ "openvibe.media"
++ ],
++ "publicOrigin": "https://openvibe.media",
++ "health": "/healthz",
++ "capabilities": [
++ "media.object.upload",
++ "media.object.read"
++ ],
++ "eventsProduced": [],
++ "eventsConsumed": [],
++ "namespacesOwned": [
++ "media.*"
++ ],
++ "lifecycle": {
++ "liveness": {
++ "endpoint": "/healthz",
++ "means": "the process is up and answers HTTP; it checks nothing else"
++ },
++ "shutdown": {
++ "signal": "SIGTERM",
++ "deadlineSeconds": 70,
++ "drains": [
++ "recordings stopped",
++ "HTTP server closed, then the database"
++ ],
++ "note": "forced exit after 70 s"
++ },
++ "startupRecovery": {
++ "resumes": [
++ {
++ "kind": "jobs",
++ "what": "jobs left running are retried or failed"
++ },
++ {
++ "kind": "outbox",
++ "what": "unsent events go to OpenVibe.Events"
++ }
++ ]
++ },
++ "rollback": {
++ "conditions": [
++ "automatic: readiness not reached after the restart"
++ ],
++ "window": "while the deploy waits for readiness",
++ "blockers": {
++ "none": "the schema code only adds"
++ }
++ },
++ "contracts": {
++ "range": "^0.1.0"
++ }
++ }
++}
ddiff --git a/fixtures/registry.service-manifest/invalid/lifecycle-none-without-reason.json b/fixtures/registry.service-manifest/invalid/lifecycle-none-without-reason.json
nnew file mode 100644
iindex 00000000..2b6e3c32
---- /dev/null
++++ b/fixtures/registry.service-manifest/invalid/lifecycle-none-without-reason.json
@@@ -0,0 +1,66 @@
++{
++ "version": "0.1.0",
++ "contractRanges": {
++ "openvibe-contracts": "^0.1.0"
++ },
++ "id": "media",
++ "name": "OpenVibe.Media",
++ "status": "beta",
++ "repository": "OpenVibers/OpenVibe.Media",
++ "domains": [
++ "openvibe.media"
++ ],
++ "publicOrigin": "https://openvibe.media",
++ "health": "/healthz",
++ "capabilities": [
++ "media.object.upload",
++ "media.object.read"
++ ],
++ "eventsProduced": [],
++ "eventsConsumed": [],
++ "namespacesOwned": [
++ "media.*"
++ ],
++ "lifecycle": {
++ "liveness": {
++ "endpoint": "/healthz",
++ "means": "the process is up and answers HTTP; it checks nothing else"
++ },
++ "shutdown": {
++ "signal": "SIGTERM",
++ "deadlineSeconds": 70,
++ "drains": [
++ "recordings stopped",
++ "HTTP server closed, then the database"
++ ],
++ "note": "forced exit after 70 s"
++ },
++ "startupRecovery": {
++ "resumes": [
++ {
++ "kind": "jobs",
++ "what": "jobs left running are retried or failed"
++ },
++ {
++ "kind": "outbox",
++ "what": "unsent events go to OpenVibe.Events"
++ }
++ ]
++ },
++ "rollback": {
++ "conditions": [
++ "automatic: readiness not reached after the restart"
++ ],
++ "window": "while the deploy waits for readiness",
++ "blockers": {
++ "none": "the schema code only adds"
++ }
++ },
++ "contracts": {
++ "range": "^0.1.0"
++ },
++ "leases": {
++ "none": ""
++ }
++ }
++}
ddiff --git a/fixtures/registry.service-manifest/invalid/lifecycle-unknown-recovery-kind.json b/fixtures/registry.service-manifest/invalid/lifecycle-unknown-recovery-kind.json
nnew file mode 100644
iindex 00000000..6209fb99
---- /dev/null
++++ b/fixtures/registry.service-manifest/invalid/lifecycle-unknown-recovery-kind.json
@@@ -0,0 +1,73 @@
++{
++ "version": "0.1.0",
++ "contractRanges": {
++ "openvibe-contracts": "^0.1.0"
++ },
++ "id": "media",
++ "name": "OpenVibe.Media",
++ "status": "beta",
++ "repository": "OpenVibers/OpenVibe.Media",
++ "domains": [
++ "openvibe.media"
++ ],
++ "publicOrigin": "https://openvibe.media",
++ "health": "/healthz",
++ "capabilities": [
++ "media.object.upload",
++ "media.object.read"
++ ],
++ "eventsProduced": [],
++ "eventsConsumed": [],
++ "namespacesOwned": [
++ "media.*"
++ ],
++ "lifecycle": {
++ "liveness": {
++ "endpoint": "/healthz",
++ "means": "the process is up and answers HTTP; it checks nothing else"
++ },
++ "shutdown": {
++ "signal": "SIGTERM",
++ "deadlineSeconds": 70,
++ "drains": [
++ "recordings stopped",
++ "HTTP server closed, then the database"
++ ],
++ "note": "forced exit after 70 s"
++ },
++ "startupRecovery": {
++ "resumes": [
++ {
++ "kind": "magic",
++ "what": "jobs left running are retried or failed"
++ },
++ {
++ "kind": "outbox",
++ "what": "unsent events go to OpenVibe.Events"
++ }
++ ]
++ },
++ "rollback": {
++ "conditions": [
++ "automatic: readiness not reached after the restart"
++ ],
++ "window": "while the deploy waits for readiness",
++ "blockers": {
++ "none": "the schema code only adds"
++ }
++ },
++ "contracts": {
++ "range": "^0.1.0"
++ },
++ "leases": {
++ "claims": [
++ {
++ "what": "a job row",
++ "holder": "the Media process",
++ "expires": "lease_until, renewed while running",
++ "fencing": "status must still be running"
++ }
++ ]
++ }
++ }
++}
ddiff --git a/fixtures/registry.service-manifest/valid/library-lifecycle.json b/fixtures/registry.service-manifest/valid/library-lifecycle.json
nnew file mode 100644
iindex 00000000..55a378be
---- /dev/null
++++ b/fixtures/registry.service-manifest/valid/library-lifecycle.json
@@@ -0,0 +1,31 @@
++{
++ "id": "sdk-like",
++ "version": "0.1.0",
++ "status": "alpha",
++ "repository": "OpenVibers/OpenVibe.SDK",
++ "domains": [],
++ "capabilities": [],
++ "eventsProduced": [],
++ "eventsConsumed": [],
++ "namespacesOwned": [],
++ "lifecycle": {
++ "liveness": {
++ "none": "a library; nothing runs"
++ },
++ "shutdown": {
++ "none": "a library; nothing runs"
++ },
++ "startupRecovery": {
++ "none": "a library; nothing runs"
++ },
++ "rollback": {
++ "none": "consumers pin tags"
++ },
++ "contracts": {
++ "none": "no manifest range"
++ },
++ "leases": {
++ "none": "a library; nothing runs"
++ }
++ }
++}
ddiff --git a/fixtures/registry.service-manifest/valid/with-lifecycle.json b/fixtures/registry.service-manifest/valid/with-lifecycle.json
nnew file mode 100644
iindex 00000000..954dab46
---- /dev/null
++++ b/fixtures/registry.service-manifest/valid/with-lifecycle.json
@@@ -0,0 +1,73 @@
++{
++ "version": "0.1.0",
++ "contractRanges": {
++ "openvibe-contracts": "^0.1.0"
++ },
++ "id": "media",
++ "name": "OpenVibe.Media",
++ "status": "beta",
++ "repository": "OpenVibers/OpenVibe.Media",
++ "domains": [
++ "openvibe.media"
++ ],
++ "publicOrigin": "https://openvibe.media",
++ "health": "/healthz",
++ "capabilities": [
++ "media.object.upload",
++ "media.object.read"
++ ],
++ "eventsProduced": [],
++ "eventsConsumed": [],
++ "namespacesOwned": [
++ "media.*"
++ ],
++ "lifecycle": {
++ "liveness": {
++ "endpoint": "/healthz",
++ "means": "the process is up and answers HTTP; it checks nothing else"
++ },
++ "shutdown": {
++ "signal": "SIGTERM",
++ "deadlineSeconds": 70,
++ "drains": [
++ "recordings stopped",
++ "HTTP server closed, then the database"
++ ],
++ "note": "forced exit after 70 s"
++ },
++ "startupRecovery": {
++ "resumes": [
++ {
++ "kind": "jobs",
++ "what": "jobs left running are retried or failed"
++ },
++ {
++ "kind": "outbox",
++ "what": "unsent events go to OpenVibe.Events"
++ }
++ ]
++ },
++ "rollback": {
++ "conditions": [
++ "automatic: readiness not reached after the restart"
++ ],
++ "window": "while the deploy waits for readiness",
++ "blockers": {
++ "none": "the schema code only adds"
++ }
++ },
++ "contracts": {
++ "range": "^0.1.0"
++ },
++ "leases": {
++ "claims": [
++ {
++ "what": "a job row",
++ "holder": "the Media process",
++ "expires": "lease_until, renewed while running",
++ "fencing": "status must still be running"
++ }
++ ]
++ }
++ }
++}
ddiff --git a/generated/json-schema/bundle.json b/generated/json-schema/bundle.json
iindex 0a69df25..26ea5e42 100644
---- a/generated/json-schema/bundle.json
++++ b/generated/json-schema/bundle.json
@@@ -747,6 +747,405 @@
"type": "string"
}
},
++ "lifecycle": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "liveness",
++ "shutdown",
++ "startupRecovery",
++ "rollback",
++ "contracts",
++ "leases"
++ ],
++ "description": "How the service starts, stops, recovers and is rolled back (roadmap WS-P task 1), taken from its code and its systemd units, never from intent. A part that does not apply is { \"none\": \"<why>\" }. `ovhost validate` refuses a host service whose lifecycle lacks a field, whose shutdown deadline exceeds its unit's TimeoutStopSec, or whose installed openvibe-contracts is outside contracts.range.",
++ "properties": {
++ "liveness": {
++ "description": "The endpoint that says the process is alive, and what a 200 from it proves. Readiness (can it serve?) is `ready`.",
++ "oneOf": [
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "endpoint",
++ "means"
++ ],
++ "properties": {
++ "endpoint": {
++ "type": "string",
++ "pattern": "^/",
++ "description": "A path on internalOrigin; the same as `health`."
++ },
++ "means": {
++ "type": "string",
++ "minLength": 10,
++ "maxLength": 300,
++ "description": "What a 200 proves, and what it does not check."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "none"
++ ],
++ "description": "No liveness endpoint, and why (a library, a static site, a retired service, or code that has none).",
++ "properties": {
++ "none": {
++ "type": "string",
++ "minLength": 3,
++ "maxLength": 300,
++ "description": "Why there is none."
++ }
++ }
++ }
++ ]
++ },
++ "shutdown": {
++ "description": "What happens between the stop signal and the process exiting.",
++ "oneOf": [
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "signal",
++ "deadlineSeconds",
++ "drains"
++ ],
++ "properties": {
++ "signal": {
++ "enum": [
++ "SIGTERM",
++ "SIGINT",
++ "SIGQUIT",
++ "SIGHUP"
++ ],
++ "description": "The signal the unit sends (KillSignal=, systemd's default SIGTERM) and the process handles."
++ },
++ "deadlineSeconds": {
++ "type": "number",
++ "minimum": 0,
++ "maximum": 86400,
++ "description": "The longest the process takes from the signal to exiting, as its own code bounds it (a forced-exit timer); 0 when it exits at once. Must not exceed the unit's TimeoutStopSec, after which systemd kills it mid-drain."
++ },
++ "drains": {
++ "type": "array",
++ "minItems": 1,
++ "items": {
++ "type": "string",
++ "minLength": 3,
++ "maxLength": 240
++ },
++ "description": "What it stops, finishes, flushes or closes before exiting, in order."
++ },
++ "workers": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "deadlineSeconds",
++ "drains"
++ ],
++ "description": "Worker units the service owns that drain on their own (ovhost never restarts them): how long a stop signal lets them run.",
++ "properties": {
++ "deadlineSeconds": {
++ "type": "number",
++ "minimum": 0,
++ "maximum": 604800,
++ "description": "The longest a worker keeps running after the stop signal, as its code bounds it."
++ },
++ "drains": {
++ "type": "array",
++ "minItems": 1,
++ "items": {
++ "type": "string",
++ "minLength": 3,
++ "maxLength": 240
++ },
++ "description": "What a worker keeps serving, and what it ends, before exiting."
++ }
++ }
++ },
++ "note": {
++ "type": "string",
++ "minLength": 3,
++ "maxLength": 400,
++ "description": "Anything the fields above do not say (a gap the code has, a handler that is missing)."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "none"
++ ],
++ "description": "No shutdown sequence, and why (a library, a static site, a retired service, or code that has none).",
++ "properties": {
++ "none": {
++ "type": "string",
++ "minLength": 3,
++ "maxLength": 300,
++ "description": "Why there is none."
++ }
++ }
++ }
++ ]
++ },
++ "startupRecovery": {
++ "description": "Work a stop or a crash interrupted that the next start picks up.",
++ "oneOf": [
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "resumes"
++ ],
++ "properties": {
++ "resumes": {
++ "type": "array",
++ "minItems": 1,
++ "description": "Each thing the process resumes at boot.",
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "kind",
++ "what"
++ ],
++ "properties": {
++ "kind": {
++ "enum": [
++ "outbox",
++ "jobs",
++ "sessions",
++ "consumer",
++ "schedule",
++ "state"
++ ],
++ "description": "outbox: unsent events; jobs: queued or interrupted background work; sessions: client connections or media sessions; consumer: an event or change feed redelivered or read from a stored cursor; schedule: timed work that fell due; state: in-memory state rebuilt from storage."
++ },
++ "what": {
++ "type": "string",
++ "minLength": 10,
++ "maxLength": 300,
++ "description": "What is resumed, and how."
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "none"
++ ],
++ "description": "No startup recovery, and why (a library, a static site, a retired service, or code that has none).",
++ "properties": {
++ "none": {
++ "type": "string",
++ "minLength": 3,
++ "maxLength": 300,
++ "description": "Why there is none."
++ }
++ }
++ }
++ ]
++ },
++ "rollback": {
++ "description": "When a release is rolled back, for how long that happens by itself, and what makes going back unsafe.",
++ "oneOf": [
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "conditions",
++ "window",
++ "blockers"
++ ],
++ "properties": {
++ "conditions": {
++ "type": "array",
++ "minItems": 1,
++ "items": {
++ "type": "string",
++ "minLength": 10,
++ "maxLength": 300
++ },
++ "description": "What triggers a rollback: automatic (readiness not reached after the restart) and manual."
++ },
++ "window": {
++ "type": "string",
++ "minLength": 10,
++ "maxLength": 300,
++ "description": "How long after a deploy the rollback is automatic, and what bounds a manual one."
++ },
++ "blockers": {
++ "description": "Changes a previous release cannot live with (forward-only migrations, table rebuilds, authority switches), or { none } with the reason.",
++ "oneOf": [
++ {
++ "type": "array",
++ "minItems": 1,
++ "items": {
++ "type": "string",
++ "minLength": 10,
++ "maxLength": 300
++ }
++ },
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "none"
++ ],
++ "description": "No rollback blockers, and why (a library, a static site, a retired service, or code that has none).",
++ "properties": {
++ "none": {
++ "type": "string",
++ "minLength": 3,
++ "maxLength": 300,
++ "description": "Why there is none."
++ }
++ }
++ }
++ ]
++ }
++ }
++ },
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "none"
++ ],
++ "description": "No rollback, and why (a library, a static site, a retired service, or code that has none).",
++ "properties": {
++ "none": {
++ "type": "string",
++ "minLength": 3,
++ "maxLength": 300,
++ "description": "Why there is none."
++ }
++ }
++ }
++ ]
++ },
++ "contracts": {
++ "description": "The openvibe-contracts versions the service accepts.",
++ "oneOf": [
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "range"
++ ],
++ "properties": {
++ "range": {
++ "type": "string",
++ "pattern": "^(\\^|>=)?\\d+\\.\\d+\\.\\d+( <\\d+\\.\\d+\\.\\d+)?$",
++ "description": "The accepted versions, the same range as contractRanges['openvibe-contracts']."
++ },
++ "note": {
++ "type": "string",
++ "minLength": 3,
++ "maxLength": 300,
++ "description": "Where the range comes from, when it is not the obvious one."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "none"
++ ],
++ "description": "No contracts range, and why (a library, a static site, a retired service, or code that has none).",
++ "properties": {
++ "none": {
++ "type": "string",
++ "minLength": 3,
++ "maxLength": 300,
++ "description": "Why there is none."
++ }
++ }
++ }
++ ]
++ },
++ "leases": {
++ "description": "What the service claims so that one owner acts at a time, and how a stale owner is fenced.",
++ "oneOf": [
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "claims"
++ ],
++ "properties": {
++ "claims": {
++ "type": "array",
++ "minItems": 1,
++ "description": "Each thing claimed, with its holder, expiry and fencing.",
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "what",
++ "holder",
++ "expires",
++ "fencing"
++ ],
++ "properties": {
++ "what": {
++ "type": "string",
++ "minLength": 3,
++ "maxLength": 200,
++ "description": "What is claimed."
++ },
++ "holder": {
++ "type": "string",
++ "minLength": 3,
++ "maxLength": 200,
++ "description": "Who holds the claim (a process, a worker id, a row)."
++ },
++ "expires": {
++ "type": "string",
++ "minLength": 3,
++ "maxLength": 200,
++ "description": "When the claim lapses and how it is renewed."
++ },
++ "fencing": {
++ "type": "string",
++ "minLength": 3,
++ "maxLength": 300,
++ "description": "How a stale owner is kept from acting after its claim lapsed."
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "none"
++ ],
++ "description": "No leases, and why (a library, a static site, a retired service, or code that has none).",
++ "properties": {
++ "none": {
++ "type": "string",
++ "minLength": 3,
++ "maxLength": 300,
++ "description": "Why there is none."
++ }
++ }
++ }
++ ]
++ }
++ }
++ },
"notes": {
"type": "string"
}
ddiff --git a/generated/openapi/ai.json b/generated/openapi/ai.json
iindex 317c9e10..af5554e3 100644
---- a/generated/openapi/ai.json
++++ b/generated/openapi/ai.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.AI API",
-- "version": "0.54.0",
-- "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.54.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.55.0",
++ "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.55.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/billing.json b/generated/openapi/billing.json
iindex 607e946e..82f7b238 100644
---- a/generated/openapi/billing.json
++++ b/generated/openapi/billing.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Billing API",
-- "version": "0.54.0",
-- "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.54.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.55.0",
++ "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.55.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/blog.json b/generated/openapi/blog.json
iindex 08da6986..474ff7cb 100644
---- a/generated/openapi/blog.json
++++ b/generated/openapi/blog.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Blog API",
-- "version": "0.54.0",
-- "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.54.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.55.0",
++ "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.55.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/chat.json b/generated/openapi/chat.json
iindex c7d8f733..563d6c67 100644
---- a/generated/openapi/chat.json
++++ b/generated/openapi/chat.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Chat API",
-- "version": "0.54.0",
-- "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.54.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.55.0",
++ "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.55.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/codes.json b/generated/openapi/codes.json
iindex 979652a7..7312247e 100644
---- a/generated/openapi/codes.json
++++ b/generated/openapi/codes.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Codes API",
-- "version": "0.54.0",
-- "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.54.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.55.0",
++ "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.55.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/community.json b/generated/openapi/community.json
iindex 33bea3a7..843a8d29 100644
---- a/generated/openapi/community.json
++++ b/generated/openapi/community.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Community API",
-- "version": "0.54.0",
-- "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.54.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.55.0",
++ "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.55.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/coupons.json b/generated/openapi/coupons.json
iindex 15303dd3..6ff672b6 100644
---- a/generated/openapi/coupons.json
++++ b/generated/openapi/coupons.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Coupons API",
-- "version": "0.54.0",
-- "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.54.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.55.0",
++ "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.55.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/deals.json b/generated/openapi/deals.json
iindex 45b75169..22857258 100644
---- a/generated/openapi/deals.json
++++ b/generated/openapi/deals.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Deals API",
-- "version": "0.54.0",
-- "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.54.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.55.0",
++ "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.55.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/events.json b/generated/openapi/events.json
iindex fa5c51b2..a3f96996 100644
---- a/generated/openapi/events.json
++++ b/generated/openapi/events.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Events API",
-- "version": "0.54.0",
-- "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.54.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.55.0",
++ "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.55.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/games.json b/generated/openapi/games.json
iindex 827fa07a..fcf302ba 100644
---- a/generated/openapi/games.json
++++ b/generated/openapi/games.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Games API",
-- "version": "0.54.0",
-- "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.54.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.55.0",
++ "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.55.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/host.json b/generated/openapi/host.json
iindex ef114cdc..8900a7bc 100644
---- a/generated/openapi/host.json
++++ b/generated/openapi/host.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Host API",
-- "version": "0.54.0",
-- "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.54.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.55.0",
++ "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.55.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/live.json b/generated/openapi/live.json
iindex 325f8170..6c3e6b81 100644
---- a/generated/openapi/live.json
++++ b/generated/openapi/live.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Live API",
-- "version": "0.54.0",
-- "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.54.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.55.0",
++ "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.55.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/media.json b/generated/openapi/media.json
iindex e84bffe0..e888514b 100644
---- a/generated/openapi/media.json
++++ b/generated/openapi/media.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Media API",
-- "version": "0.54.0",
-- "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.54.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.55.0",
++ "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.55.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/network.json b/generated/openapi/network.json
iindex 04bd36bc..99bb70d1 100644
---- a/generated/openapi/network.json
++++ b/generated/openapi/network.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Network API",
-- "version": "0.54.0",
-- "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.54.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.55.0",
++ "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.55.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/news.json b/generated/openapi/news.json
iindex ca8ed494..83e9de40 100644
---- a/generated/openapi/news.json
++++ b/generated/openapi/news.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.News API",
-- "version": "0.54.0",
-- "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.54.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.55.0",
++ "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.55.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/openre.json b/generated/openapi/openre.json
iindex 1f719db0..bc6202c2 100644
---- a/generated/openapi/openre.json
++++ b/generated/openapi/openre.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenRe.Stream API",
-- "version": "0.54.0",
-- "description": "Routes of OpenRe.Stream as the OpenVibe contracts describe them (openvibe-contracts 0.54.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.55.0",
++ "description": "Routes of OpenRe.Stream as the OpenVibe contracts describe them (openvibe-contracts 0.55.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/reviews.json b/generated/openapi/reviews.json
iindex 8e423b37..7bece0dd 100644
---- a/generated/openapi/reviews.json
++++ b/generated/openapi/reviews.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Reviews API",
-- "version": "0.54.0",
-- "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.54.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.55.0",
++ "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.55.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/search.json b/generated/openapi/search.json
iindex be2ba435..4adc239f 100644
---- a/generated/openapi/search.json
++++ b/generated/openapi/search.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Search API",
-- "version": "0.54.0",
-- "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.54.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.55.0",
++ "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.55.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/sources.json b/generated/openapi/sources.json
iindex 3392c4b9..e34f13bf 100644
---- a/generated/openapi/sources.json
++++ b/generated/openapi/sources.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Sources API",
-- "version": "0.54.0",
-- "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.54.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.55.0",
++ "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.55.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/tips.json b/generated/openapi/tips.json
iindex 06c68682..cfeb5513 100644
---- a/generated/openapi/tips.json
++++ b/generated/openapi/tips.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Tips API",
-- "version": "0.54.0",
-- "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.54.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.55.0",
++ "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.55.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/tools.json b/generated/openapi/tools.json
iindex 362367ff..641b8f2e 100644
---- a/generated/openapi/tools.json
++++ b/generated/openapi/tools.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Tools API",
-- "version": "0.54.0",
-- "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.54.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.55.0",
++ "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.55.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/trade.json b/generated/openapi/trade.json
iindex f1c5d5a6..d16ab47b 100644
---- a/generated/openapi/trade.json
++++ b/generated/openapi/trade.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Trade API",
-- "version": "0.54.0",
-- "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.54.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.55.0",
++ "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.55.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/vip.json b/generated/openapi/vip.json
iindex 0d27de45..881d25ce 100644
---- a/generated/openapi/vip.json
++++ b/generated/openapi/vip.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.VIP API",
-- "version": "0.54.0",
-- "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.54.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.55.0",
++ "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.55.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/wiki.json b/generated/openapi/wiki.json
iindex 5f63ac13..c1592178 100644
---- a/generated/openapi/wiki.json
++++ b/generated/openapi/wiki.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Wiki API",
-- "version": "0.54.0",
-- "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.54.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.55.0",
++ "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.55.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/typescript/index.d.ts b/generated/typescript/index.d.ts
iindex eb74b5e3..143d9f20 100644
---- a/generated/typescript/index.d.ts
++++ b/generated/typescript/index.d.ts
@@@ -251,6 +251,223 @@ export interface ServiceManifest {
contractRanges?: {
[k: string]: string | undefined;
};
++ /**
++ * How the service starts, stops, recovers and is rolled back (roadmap WS-P task 1), taken from its code and its systemd units, never from intent. A part that does not apply is { "none": "<why>" }. `ovhost validate` refuses a host service whose lifecycle lacks a field, whose shutdown deadline exceeds its unit's TimeoutStopSec, or whose installed openvibe-contracts is outside contracts.range.
++ */
++ lifecycle?: {
++ /**
++ * The endpoint that says the process is alive, and what a 200 from it proves. Readiness (can it serve?) is `ready`.
++ */
++ liveness:
++ | {
++ /**
++ * A path on internalOrigin; the same as `health`.
++ */
++ endpoint: string;
++ /**
++ * What a 200 proves, and what it does not check.
++ */
++ means: string;
++ }
++ | {
++ /**
++ * Why there is none.
++ */
++ none: string;
++ };
++ /**
++ * What happens between the stop signal and the process exiting.
++ */
++ shutdown:
++ | {
++ /**
++ * The signal the unit sends (KillSignal=, systemd's default SIGTERM) and the process handles.
++ */
++ signal: "SIGTERM" | "SIGINT" | "SIGQUIT" | "SIGHUP";
++ /**
++ * The longest the process takes from the signal to exiting, as its own code bounds it (a forced-exit timer); 0 when it exits at once. Must not exceed the unit's TimeoutStopSec, after which systemd kills it mid-drain.
++ */
++ deadlineSeconds: number;
++ /**
++ * What it stops, finishes, flushes or closes before exiting, in order.
++ *
++ * @minItems 1
++ */
++ drains: [string, ...string[]];
++ /**
++ * Worker units the service owns that drain on their own (ovhost never restarts them): how long a stop signal lets them run.
++ */
++ workers?: {
++ /**
++ * The longest a worker keeps running after the stop signal, as its code bounds it.
++ */
++ deadlineSeconds: number;
++ /**
++ * What a worker keeps serving, and what it ends, before exiting.
++ *
++ * @minItems 1
++ */
++ drains: [string, ...string[]];
++ };
++ /**
++ * Anything the fields above do not say (a gap the code has, a handler that is missing).
++ */
++ note?: string;
++ }
++ | {
++ /**
++ * Why there is none.
++ */
++ none: string;
++ };
++ /**
++ * Work a stop or a crash interrupted that the next start picks up.
++ */
++ startupRecovery:
++ | {
++ /**
++ * Each thing the process resumes at boot.
++ *
++ * @minItems 1
++ */
++ resumes: [
++ {
++ /**
++ * outbox: unsent events; jobs: queued or interrupted background work; sessions: client connections or media sessions; consumer: an event or change feed redelivered or read from a stored cursor; schedule: timed work that fell due; state: in-memory state rebuilt from storage.
++ */
++ kind: "outbox" | "jobs" | "sessions" | "consumer" | "schedule" | "state";
++ /**
++ * What is resumed, and how.
++ */
++ what: string;
++ },
++ ...{
++ /**
++ * outbox: unsent events; jobs: queued or interrupted background work; sessions: client connections or media sessions; consumer: an event or change feed redelivered or read from a stored cursor; schedule: timed work that fell due; state: in-memory state rebuilt from storage.
++ */
++ kind: "outbox" | "jobs" | "sessions" | "consumer" | "schedule" | "state";
++ /**
++ * What is resumed, and how.
++ */
++ what: string;
++ }[]
++ ];
++ }
++ | {
++ /**
++ * Why there is none.
++ */
++ none: string;
++ };
++ /**
++ * When a release is rolled back, for how long that happens by itself, and what makes going back unsafe.
++ */
++ rollback:
++ | {
++ /**
++ * What triggers a rollback: automatic (readiness not reached after the restart) and manual.
++ *
++ * @minItems 1
++ */
++ conditions: [string, ...string[]];
++ /**
++ * How long after a deploy the rollback is automatic, and what bounds a manual one.
++ */
++ window: string;
++ /**
++ * Changes a previous release cannot live with (forward-only migrations, table rebuilds, authority switches), or { none } with the reason.
++ */
++ blockers:
++ | [string, ...string[]]
++ | {
++ /**
++ * Why there is none.
++ */
++ none: string;
++ };
++ }
++ | {
++ /**
++ * Why there is none.
++ */
++ none: string;
++ };
++ /**
++ * The openvibe-contracts versions the service accepts.
++ */
++ contracts:
++ | {
++ /**
++ * The accepted versions, the same range as contractRanges['openvibe-contracts'].
++ */
++ range: string;
++ /**
++ * Where the range comes from, when it is not the obvious one.
++ */
++ note?: string;
++ }
++ | {
++ /**
++ * Why there is none.
++ */
++ none: string;
++ };
++ /**
++ * What the service claims so that one owner acts at a time, and how a stale owner is fenced.
++ */
++ leases:
++ | {
++ /**
++ * Each thing claimed, with its holder, expiry and fencing.
++ *
++ * @minItems 1
++ */
++ claims: [
++ {
++ /**
++ * What is claimed.
++ */
++ what: string;
++ /**
++ * Who holds the claim (a process, a worker id, a row).
++ */
++ holder: string;
++ /**
++ * When the claim lapses and how it is renewed.
++ */
++ expires: string;
++ /**
++ * How a stale owner is kept from acting after its claim lapsed.
++ */
++ fencing: string;
++ },
++ ...{
++ /**
++ * What is claimed.
++ */
++ what: string;
++ /**
++ * Who holds the claim (a process, a worker id, a row).
++ */
++ holder: string;
++ /**
++ * When the claim lapses and how it is renewed.
++ */
++ expires: string;
++ /**
++ * How a stale owner is kept from acting after its claim lapsed.
++ */
++ fencing: string;
++ }[]
++ ];
++ }
++ | {
++ /**
++ * Why there is none.
++ */
++ none: string;
++ };
++ };
notes?: string;
}
ddiff --git a/manifests/services/ai.json b/manifests/services/ai.json
iindex 89b5c0b1..f6b2bf6a 100644
---- a/manifests/services/ai.json
++++ b/manifests/services/ai.json
@@@ -36,5 +36,51 @@
"namespacesOwned": [
"ai.usage_summary"
],
++ "lifecycle": {
++ "liveness": {
++ "endpoint": "/api/health",
++ "means": "the process is up and answers HTTP; it checks nothing else (the database and dependencies are what ready checks)"
++ },
++ "shutdown": {
++ "signal": "SIGTERM",
++ "deadlineSeconds": 10,
++ "drains": [
++ "housekeeping timer, user-module sync and Network key refresh stopped",
++ "open HTTP connections closed (closeAllConnections), then the server",
++ "runs in progress awaited (runs.drain)",
++ "whisper child processes killed",
++ "database closed"
++ ],
++ "note": "the forced exit after 10 s exits 1; the ai.run.* outbox relay is not stopped and ends with the process"
++ },
++ "startupRecovery": {
++ "resumes": [
++ {
++ "kind": "jobs",
++ "what": "runs left queued or running are marked failed with error_code run.interrupted (announced as ai.run.failed); the caller retries them"
++ },
++ {
++ "kind": "outbox",
++ "what": "unsent ai.run.* events in the outbox (server/events.js) go to OpenVibe.Events once the relay starts"
++ }
++ ]
++ },
++ "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 (30 s in host.example.json); manual afterwards, as long as no blocker applies",
++ "blockers": {
++ "none": "the schema code only adds (CREATE … IF NOT EXISTS, ADD COLUMN; no DROP, RENAME or table rebuild), which an older release ignores"
++ }
++ },
++ "contracts": {
++ "range": ">=0.6.0 <1.0.0"
++ },
++ "leases": {
++ "none": "no claims: one process owns ai.db (better-sqlite3, single writer); a run is taken with a compare-and-set on its status (queued → running)"
++ }
++ },
"notes": "Providers, models, routing profiles, versioned prompt templates and workflows, runs, request logs, citations, scoped cache, quotas and audit. Workflow output is a draft/evidence package attributed to workflow + model + run. ai.run.queued|succeeded|failed|cached have payload contracts (v0.30.0, status planned): AI publishes them once its Events client is wired in; it publishes none today. ai.run.create takes ai.run-request@1 and returns ai.run@1."
}
ddiff --git a/manifests/services/billing.json b/manifests/services/billing.json
iindex 51109d70..74974204 100644
---- a/manifests/services/billing.json
++++ b/manifests/services/billing.json
@@@ -44,5 +44,60 @@
"namespacesOwned": [
"billing.*"
],
++ "lifecycle": {
++ "liveness": {
++ "endpoint": "/api/health",
++ "means": "the process answers and reads the economy freeze flag from its database; it reports the authority, providers and relay but checks no dependency"
++ },
++ "shutdown": {
++ "signal": "SIGTERM",
++ "deadlineSeconds": 5,
++ "drains": [
++ "job timers cleared (provider-event retry, renewal sweep, reconciliation)",
++ "outbox relay and Network key refresh stopped",
++ "HTTP server closed",
++ "database closed"
++ ]
++ },
++ "startupRecovery": {
++ "resumes": [
++ {
++ "kind": "jobs",
++ "what": "stored provider events without processed_at are processed again every minute (processPending); each applies at most once"
++ },
++ {
++ "kind": "schedule",
++ "what": "the hourly subscription renewal sweep and the reconciliation run (first one 60 s after boot) pick up what fell due"
++ },
++ {
++ "kind": "outbox",
++ "what": "unsent events in the outbox table go to OpenVibe.Events once the relay starts (at least once; Events dedupes by event_id)"
++ }
++ ]
++ },
++ "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 (30 s in host.example.json); manual afterwards, as long as no blocker applies",
++ "blockers": {
++ "none": "the schema code only adds (CREATE … IF NOT EXISTS, ADD COLUMN; no DROP, RENAME or table rebuild), which an older release ignores"
++ }
++ },
++ "contracts": {
++ "range": ">=0.5.1 <1.0.0"
++ },
++ "leases": {
++ "claims": [
++ {
++ "what": "a stored provider event (provider_events row) while it is applied",
++ "holder": "the connection that takes the SQLite write lock (IMMEDIATE transaction)",
++ "expires": "at commit or rollback of that transaction",
++ "fencing": "processed_at is re-read inside the transaction, so a second process or connection that waited applies nothing; ledger postings carry idempotency keys"
++ }
++ ]
++ }
++ },
"notes": "The isolated money ledger (ADR-012): balanced append-only journal, provider receipts, subscriptions/entitlements, refunds and payouts. Alpha, shadow mode: Live stays authoritative for money until the Wave 8 cutover; Billing imports and reconciles."
}
ddiff --git a/manifests/services/blog.json b/manifests/services/blog.json
iindex 18ac0083..c361e450 100644
---- a/manifests/services/blog.json
++++ b/manifests/services/blog.json
@@@ -55,5 +55,57 @@
"namespacesOwned": [
"blog.*"
],
++ "lifecycle": {
++ "liveness": {
++ "endpoint": "/api/health",
++ "means": "the process is up and answers HTTP; it checks nothing else (the database and dependencies are what ready checks)"
++ },
++ "shutdown": {
++ "signal": "SIGTERM",
++ "deadlineSeconds": 5,
++ "drains": [
++ "schedule and media-verification worker stopped",
++ "changelog worker stopped",
++ "HTTP server closed",
++ "outbox relay stopped (awaited)",
++ "store closed"
++ ]
++ },
++ "startupRecovery": {
++ "resumes": [
++ {
++ "kind": "schedule",
++ "what": "scheduled publish/unpublish jobs that fell due, and running jobs whose lease expired, are claimed again (openvibe-publishing/schedule, at least once, re-run safe)"
++ },
++ {
++ "kind": "outbox",
++ "what": "unsent events in the outbox table go to OpenVibe.Events once the relay starts (at least once; Events dedupes by event_id)"
++ }
++ ]
++ },
++ "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 (30 s in host.example.json); manual afterwards, as long as no blocker applies",
++ "blockers": {
++ "none": "the schema code only adds (CREATE … IF NOT EXISTS, ADD COLUMN; no DROP, RENAME or table rebuild), which an older release ignores"
++ }
++ },
++ "contracts": {
++ "range": ">=0.13.0 <1.0.0"
++ },
++ "leases": {
++ "claims": [
++ {
++ "what": "a scheduled publication job (blog_schedule_jobs)",
++ "holder": "lease_owner = BLOG_WORKER_ID, default blog-<pid>",
++ "expires": "lease_until = claim time + 60 s (leaseMs); taken again only after it passed",
++ "fencing": "complete, retry and fail only count WHERE lease_owner is the caller, so a worker whose lease was taken over cannot overwrite the new owner's result"
++ }
++ ]
++ }
++ },
"notes": "Port 4810. Owns blogs, blog_memberships, blog_posts, blog_post_revisions, blog_series, blog_taxonomy, blog_post_terms, blog_schedules, blog_redirects, blog_feed_settings in its own SQLite (openvibe-publishing packages, prefix blog). Consumes OpenVibe.Network (SSO, identity.subject.resolve), OpenVibe.Community (community.comment.write, community.comment.moderate), OpenVibe.Media (media.object.read, namespace blog) and publishes through OpenVibe.Events (events.event.publish); OpenVibe.Search reads blog.index_document.* from Events."
}
ddiff --git a/manifests/services/chat.json b/manifests/services/chat.json
iindex cd8413f0..ef186b4e 100644
---- a/manifests/services/chat.json
++++ b/manifests/services/chat.json
@@@ -32,6 +32,67 @@
"chat.dm_settings",
"chat.presence_prefs"
],
++ "lifecycle": {
++ "liveness": {
++ "endpoint": "/health",
++ "means": "the process is up and answers HTTP; it checks nothing else (Chat's database and the Live sync are what /ready checks)"
++ },
++ "shutdown": {
++ "signal": "SIGTERM",
++ "deadlineSeconds": 8,
++ "drains": [
++ "server_restart notice broadcast to every chat socket",
++ "Live sync, outbox relay, Live mirror, Events consumer and subscriptions stopped",
++ "Live mirror flushed (at most 3 s)",
++ "chat and call WebSocket servers closed",
++ "HTTP server closed, then the database"
++ ],
++ "note": "at most 3 s of mirror flush, then a forced exit 5 s later"
++ },
++ "startupRecovery": {
++ "resumes": [
++ {
++ "kind": "state",
++ "what": "a first sync with Live (waited on for at most 15 s) runs before the socket listens"
++ },
++ {
++ "kind": "sessions",
++ "what": "clients reconnect after the restart notice; the audio queue finishes rows that were playing and plays the rest once a room has listeners again; call rooms left open are closed with reason restart"
++ },
++ {
++ "kind": "outbox",
++ "what": "unsent events in the outbox and changes queued in live_mirror_outbox (the read mirror Live keeps for a rollback) are sent again"
++ },
++ {
++ "kind": "consumer",
++ "what": "OpenVibe.Events redelivers the events of Chat's subscriptions (network.block.changed, network.user.token_valid_after, live.release.deployed, …) that were pending during the stop (at least once, deduped by event_id)"
++ }
++ ]
++ },
++ "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 (30 s in host.example.json); manual afterwards, as long as no blocker applies",
++ "blockers": {
++ "none": "the schema code only adds (CREATE … IF NOT EXISTS, ADD COLUMN; no DROP, RENAME or table rebuild), which an older release ignores"
++ }
++ },
++ "contracts": {
++ "range": ">=0.13.0 <1.0.0"
++ },
++ "leases": {
++ "claims": [
++ {
++ "what": "a Live release's deploy card (deploy_releases, keyed by the head commit)",
++ "holder": "the first path that stores it: the Live bridge or the live.release.deployed event",
++ "expires": "never (the row stays); a live.release.deployed older than 6 hours announces nothing",
++ "fencing": "the claim and the card are written in one transaction, so the second path folds into the existing card instead of posting another"
++ }
++ ]
++ }
++ },
"notes": "Wave 6: Live's chat (WebSocket, DMs, history, TTS, sounds, moderation) behind a service boundary on 127.0.0.1:4400, serving Live's chat paths since 2026-09-23. Since 2026-09-24 also its own site, openvibe.chat (global chat, messages, settings, sign-in with the Network), which works while Live is down.",
"health": "/health",
"ready": "/ready",
ddiff --git a/manifests/services/codes.json b/manifests/services/codes.json
iindex 92558664..640406aa 100644
---- a/manifests/services/codes.json
++++ b/manifests/services/codes.json
@@@ -40,5 +40,45 @@
"namespacesOwned": [
"codes.*"
],
++ "lifecycle": {
++ "liveness": {
++ "endpoint": "/api/health",
++ "means": "the process is up and answers HTTP; it checks nothing else (the database and dependencies are what ready checks)"
++ },
++ "shutdown": {
++ "signal": "SIGTERM",
++ "deadlineSeconds": 5,
++ "drains": [
++ "HTTP server closed",
++ "outbox relay stopped (awaited)",
++ "store closed"
++ ],
++ "note": "keepAliveTimeout is 65 s, so an idle keep-alive connection can hold server.close() until the forced exit at 5 s"
++ },
++ "startupRecovery": {
++ "resumes": [
++ {
++ "kind": "outbox",
++ "what": "unsent events in the outbox table go to OpenVibe.Events once the relay starts (at least once; Events dedupes by event_id)"
++ }
++ ]
++ },
++ "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 (30 s in host.example.json); manual afterwards, as long as no blocker applies",
++ "blockers": [
++ "the trust table was rebuilt once to the ADR-013 tier names (server/db.js maps the old values): a release from before it expects the old tier names"
++ ]
++ },
++ "contracts": {
++ "range": ">=0.26.0 <1.0.0"
++ },
++ "leases": {
++ "none": "no claims: one process owns codes.db (better-sqlite3, single writer)"
++ }
++ },
"notes": "Port 4900, unit openvibe-codes.service, state /var/lib/openvibe-codes. The developer portal (ADR-014): projects, members, apps, credentials, grants and quotas are OWNED BY NETWORK and shown through Network's /api/v1/projects API with the signed-in person's own token; Codes stores none of them and never a secret. Codes owns only manifests, releases (+ append-only release_log), trust tiers (metadata) and playground run logs in its own SQLite. Consumes OpenVibe.Network (SSO with PKCE S256 as OAuth client codes, JWKS, the projects API, the registry, client-credentials) and publishes through OpenVibe.Events (events.event.publish). Playgrounds call Events and Media with the APP's own token only. Docs are generated at boot from the pinned openvibe-contracts and openvibe-sdk."
}
ddiff --git a/manifests/services/community.json b/manifests/services/community.json
iindex 8733f618..f1e251c5 100644
---- a/manifests/services/community.json
++++ b/manifests/services/community.json
@@@ -57,5 +57,51 @@
"what": "Share text and code with a link, comment, and follow what people on OpenVibe are making.",
"legalProfile": "ugc",
"position": 2
++ },
++ "lifecycle": {
++ "liveness": {
++ "endpoint": "/api/health",
++ "means": "the process is up and answers HTTP; it checks nothing else (the database and dependencies are what ready checks)"
++ },
++ "shutdown": {
++ "signal": "SIGTERM",
++ "deadlineSeconds": 5,
++ "drains": [
++ "HTTP server closed"
++ ],
++ "note": "only the HTTP server is closed: the events outbox, the Discord relay, the Pulse subscriptions and the profile and search-document scans are not stopped and end with the process"
++ },
++ "startupRecovery": {
++ "resumes": [
++ {
++ "kind": "outbox",
++ "what": "unsent events in the outbox table go to OpenVibe.Events once the relay starts (at least once; Events dedupes by event_id)"
++ },
++ {
++ "kind": "schedule",
++ "what": "the Roadmap space re-syncs from docs/roadmap/public.json; the community.profile and search-document scans start again from what changed"
++ },
++ {
++ "kind": "consumer",
++ "what": "OpenVibe.Events redelivers network.block.changed and network.user.token_valid_after (and the Pulse subscriptions, re-created idempotently at boot) that were pending during the stop (at least once, deduped by event_id)"
++ }
++ ]
++ },
++ "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 (30 s in host.example.json); manual afterwards, as long as no blocker applies",
++ "blockers": {
++ "none": "the schema code only adds (CREATE … IF NOT EXISTS, ADD COLUMN; no DROP, RENAME or table rebuild), which an older release ignores"
++ }
++ },
++ "contracts": {
++ "range": ">=0.5.0 <1.0.0"
++ },
++ "leases": {
++ "none": "no claims: one process owns community.db (better-sqlite3, single writer)"
++ }
}
}
ddiff --git a/manifests/services/contracts.json b/manifests/services/contracts.json
iindex da28f920..b97a8b4f 100644
---- a/manifests/services/contracts.json
++++ b/manifests/services/contracts.json
@@@ -19,5 +19,32 @@
"eventsProduced": [],
"eventsConsumed": [],
"namespacesOwned": [],
++ "lifecycle": {
++ "liveness": {
++ "none": "a library (openvibe-contracts); nothing runs"
++ },
++ "shutdown": {
++ "none": "a library (openvibe-contracts); nothing runs"
++ },
++ "startupRecovery": {
++ "none": "a library (openvibe-contracts); nothing runs"
++ },
++ "rollback": {
++ "conditions": [
++ "manual: consumers pin release tags; going back means pinning the previous tag in package.json and redeploying the consumer"
++ ],
++ "window": "any time; a published tag is never moved or deleted",
++ "blockers": [
++ "a consumer that already uses an API added in the newer tag must drop that use first"
++ ]
++ },
++ "leases": {
++ "none": "a library (openvibe-contracts); nothing runs"
++ },
++ "contracts": {
++ "range": ">=0.2.0 <1.0.0",
++ "note": "its own release line; consumers pin a tag"
++ }
++ },
"notes": "Machine-readable contracts for the whole OpenVibe network. Artifact repository; no runtime."
}
ddiff --git a/manifests/services/coupons.json b/manifests/services/coupons.json
iindex d622c4ad..4888c923 100644
---- a/manifests/services/coupons.json
++++ b/manifests/services/coupons.json
@@@ -50,5 +50,53 @@
"namespacesOwned": [
"coupons.*"
],
++ "lifecycle": {
++ "liveness": {
++ "endpoint": "/api/health",
++ "means": "the process is up and answers HTTP; it checks nothing else (the database and dependencies are what ready checks)"
++ },
++ "shutdown": {
++ "signal": "SIGTERM",
++ "deadlineSeconds": 5,
++ "drains": [
++ "worker stopped (expiry sweep, Sources import)",
++ "HTTP server closed",
++ "outbox relay stopped (awaited)",
++ "store closed"
++ ]
++ },
++ "startupRecovery": {
++ "resumes": [
++ {
++ "kind": "schedule",
++ "what": "the expiry sweep records codes whose expiry passed during the stop (active lists filter on expires_at anyway)"
++ },
++ {
++ "kind": "consumer",
++ "what": "the OpenVibe.Sources change feed is pulled again from the stored cursor, so items published during the stop arrive (idempotent per item revision)"
++ },
++ {
++ "kind": "outbox",
++ "what": "unsent events in the outbox table go to OpenVibe.Events once the relay starts (at least once; Events dedupes by event_id)"
++ }
++ ]
++ },
++ "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 (30 s in host.example.json); manual afterwards, as long as no blocker applies",
++ "blockers": {
++ "none": "the schema code only adds (CREATE … IF NOT EXISTS, ADD COLUMN; no DROP, RENAME or table rebuild), which an older release ignores"
++ }
++ },
++ "contracts": {
++ "range": ">=0.19.0 <1.0.0"
++ },
++ "leases": {
++ "none": "no claims: one process owns coupons.db (better-sqlite3, single writer)"
++ }
++ },
"notes": "Port 4850. Owns coupon_merchants, coupon_merchant_domains, coupons, coupon_sources, coupon_restrictions, coupon_validation_reports, coupon_status_history, coupon_application_hints, coupon_watches in its own SQLite, plus extension_installs (cpx_ browser-helper tokens, scopes coupons.lookup and coupons.report, hashed, revocable). Consumes OpenVibe.Network (SSO, JWKS) and OpenVibe.Sources (sources.item.read, category coupons); publishes through OpenVibe.Events (events.event.publish); OpenVibe.Search reads coupons.index_document.* from Events. The browser helper lives in OpenVibers/OpenVibe.Extensions (ADR-023)."
}
ddiff --git a/manifests/services/deals.json b/manifests/services/deals.json
iindex 626182bc..0e75b311 100644
---- a/manifests/services/deals.json
++++ b/manifests/services/deals.json
@@@ -59,5 +59,53 @@
"namespacesOwned": [
"deals.*"
],
++ "lifecycle": {
++ "liveness": {
++ "endpoint": "/api/health",
++ "means": "the process is up and answers HTTP; it checks nothing else (the database and dependencies are what ready checks)"
++ },
++ "shutdown": {
++ "signal": "SIGTERM",
++ "deadlineSeconds": 5,
++ "drains": [
++ "worker stopped (expiry, hotness, freshness, Sources, housekeeping)",
++ "HTTP server closed",
++ "outbox relay stopped (awaited)",
++ "store closed"
++ ]
++ },
++ "startupRecovery": {
++ "resumes": [
++ {
++ "kind": "schedule",
++ "what": "the next worker tick applies stated expiries, hotness and freshness that fell due; every step is idempotent"
++ },
++ {
++ "kind": "consumer",
++ "what": "the OpenVibe.Sources change feed is pulled again from the stored cursor, so items published during the stop arrive (idempotent per item revision)"
++ },
++ {
++ "kind": "outbox",
++ "what": "unsent events in the outbox table go to OpenVibe.Events once the relay starts (at least once; Events dedupes by event_id)"
++ }
++ ]
++ },
++ "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 (30 s in host.example.json); manual afterwards, as long as no blocker applies",
++ "blockers": {
++ "none": "the schema code only adds (CREATE … IF NOT EXISTS, ADD COLUMN; no DROP, RENAME or table rebuild), which an older release ignores"
++ }
++ },
++ "contracts": {
++ "range": ">=0.19.0 <1.0.0"
++ },
++ "leases": {
++ "none": "no claims: one process owns deals.db (better-sqlite3, single writer)"
++ }
++ },
"notes": "Port 4840. Owns deal_products, deal_product_aliases, deal_offers, deal_offer_sources, deal_votes, deal_hotness_snapshots, deal_watches, deal_price_observations, deal_flags (+ deal_stores) in its own SQLite. Consumes OpenVibe.Network (SSO, identity.subject.resolve), OpenVibe.Sources (sources.item.read; sources.item.* events only wake the pull), OpenVibe.Community (community.comment.write, community.comment.moderate) and publishes through OpenVibe.Events (events.event.publish); OpenVibe.Search reads deals.index_document.* from Events; OpenVibe.Network turns deals.watch.matched into notifications (server/notifications/events-consumer.js)."
}
ddiff --git a/manifests/services/events.json b/manifests/services/events.json
iindex fafcab58..50cd2930 100644
---- a/manifests/services/events.json
++++ b/manifests/services/events.json
@@@ -30,5 +30,51 @@
"eventsProduced": [],
"eventsConsumed": [],
"namespacesOwned": [],
++ "lifecycle": {
++ "liveness": {
++ "endpoint": "/api/health",
++ "means": "the process is up and answers HTTP; it checks nothing else (the database and dependencies are what ready checks)"
++ },
++ "shutdown": {
++ "signal": "SIGTERM",
++ "deadlineSeconds": 10,
++ "drains": [
++ "retention timer stopped",
++ "realtime (SSE) streams stopped",
++ "delivery worker stopped, deliveries in flight awaited",
++ "open HTTP connections closed (closeAllConnections), then the server",
++ "database closed"
++ ],
++ "note": "the forced exit after 10 s exits 1"
++ },
++ "startupRecovery": {
++ "resumes": [
++ {
++ "kind": "jobs",
++ "what": "pending deliveries are retried from their next attempt time; one cut by the stop is sent again (at least once; consumers dedupe by event_id)"
++ },
++ {
++ "kind": "sessions",
++ "what": "SSE clients reconnect and resume from their cursor (browsers get the public events of the last REALTIME_PUBLIC_REPLAY_SECONDS, older cursors a gap)"
++ }
++ ]
++ },
++ "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 (30 s in host.example.json); manual afterwards, as long as no blocker applies",
++ "blockers": {
++ "none": "the schema code only adds (CREATE … IF NOT EXISTS, ADD COLUMN; no DROP, RENAME or table rebuild), which an older release ignores"
++ }
++ },
++ "contracts": {
++ "range": ">=0.5.1 <1.0.0"
++ },
++ "leases": {
++ "none": "no claims: one process owns events.db (better-sqlite3, single writer); one delivery per subscription is in flight at a time, tracked in memory"
++ }
++ },
"notes": "Durable events, subscriptions, signed delivery with retry, dead letters and replay; SSE realtime gateway at /realtime/stream until ADR-005 decides where Realtime lives."
}
ddiff --git a/manifests/services/examples.json b/manifests/services/examples.json
iindex d9623f73..2dc4f88c 100644
---- a/manifests/services/examples.json
++++ b/manifests/services/examples.json
@@@ -17,5 +17,25 @@
"eventsProduced": [],
"eventsConsumed": [],
"namespacesOwned": [],
++ "lifecycle": {
++ "liveness": {
++ "none": "a repository with CI; nothing is released or run"
++ },
++ "shutdown": {
++ "none": "a repository with CI; nothing runs"
++ },
++ "startupRecovery": {
++ "none": "a repository with CI; nothing runs"
++ },
++ "rollback": {
++ "none": "a repository with CI; nothing is deployed"
++ },
++ "contracts": {
++ "range": ">=0.28.0 <1.0.0"
++ },
++ "leases": {
++ "none": "a repository with CI; nothing runs"
++ }
++ },
"notes": "Executable public integration examples (developer apps on openvibe-sdk), each with a smoke test that CI runs against openvibe-sdk/testing's mock platform. A repository with CI, not a runtime and not a published package; nothing to deploy or poll."
}
ddiff --git a/manifests/services/games.json b/manifests/services/games.json
iindex 47eea7f1..69ec2589 100644
---- a/manifests/services/games.json
++++ b/manifests/services/games.json
@@@ -52,5 +52,58 @@
"namespacesOwned": [
"games.*"
],
++ "lifecycle": {
++ "liveness": {
++ "endpoint": "/healthz",
++ "means": "the process is up and answers HTTP; liveness only (world.db and the simulation tick are what /api/ready checks)"
++ },
++ "shutdown": {
++ "signal": "SIGTERM",
++ "deadlineSeconds": 0,
++ "drains": [
++ "metrics timer stopped",
++ "world saved: every session marked dirty and flushed (game.shutdown)",
++ "media mirror stopped; outbox stop requested, not awaited (unsent rows stay in the outbox)",
++ "store closed, HTTP close requested, physics disposed",
++ "process exits at once"
++ ],
++ "note": "exits synchronously: open WebSocket sessions are cut, not drained, and in-flight HTTP requests are dropped"
++ },
++ "startupRecovery": {
++ "resumes": [
++ {
++ "kind": "state",
++ "what": "the world is restored from world.db before the network opens (schema migrated forward first)"
++ },
++ {
++ "kind": "outbox",
++ "what": "unsent events in the outbox table go to OpenVibe.Events once the relay starts (at least once; Events dedupes by event_id)"
++ },
++ {
++ "kind": "jobs",
++ "what": "the media mirror backfills what it has not uploaded yet, then keeps mirroring"
++ },
++ {
++ "kind": "sessions",
++ "what": "players reconnect; characters were saved on shutdown"
++ }
++ ]
++ },
++ "rollback": {
++ "conditions": [
++ "manual only: Games is not managed by ovhost (pnpm workspace with a TypeScript build); check out the previous commit, rebuild and restart openvibe-games.service"
++ ],
++ "window": "as long as world.db has not been migrated past the older build's SCHEMA_VERSION",
++ "blockers": [
++ "a build refuses a world.db whose meta.schema_version is newer than its own (SCHEMA_VERSION 13 today, forward-only migrations): going back past a schema bump fails at boot"
++ ]
++ },
++ "contracts": {
++ "range": ">=0.8.0 <1.0.0"
++ },
++ "leases": {
++ "none": "no claims: one process owns world.db (better-sqlite3, single writer)"
++ }
++ },
"notes": "Scraplandia (browser runtime). Players are keyed by canonical Network subjects (ADR-0006 in the repo). Calls Events (events.event.publish), Media (media.object.upload, namespace games: map-editor assets) and Network identity (identity.subject.resolve, batch legacy adoption) with the games client-credentials principal. Mods: games-content@1 declarative packs only; executable mods wait for OpenVibe.Host Stage C. No achievement system exists, so there are no games.achievement.* events."
}
ddiff --git a/manifests/services/host.json b/manifests/services/host.json
iindex b4477845..2fa4bf27 100644
---- a/manifests/services/host.json
++++ b/manifests/services/host.json
@@@ -42,5 +42,57 @@
"namespacesOwned": [
"host.*"
],
++ "lifecycle": {
++ "liveness": {
++ "endpoint": "/api/health",
++ "means": "the process is up and answers HTTP (the Host API on 4910; the ovhost CLI is not a daemon); it checks nothing else (the database and dependencies are what ready checks)"
++ },
++ "shutdown": {
++ "signal": "SIGTERM",
++ "deadlineSeconds": 5,
++ "drains": [
++ "worker stopped (domain checks, object sweep, outbox prune)",
++ "HTTP server closed",
++ "outbox relay stopped (awaited)",
++ "metrics stopped",
++ "store closed"
++ ]
++ },
++ "startupRecovery": {
++ "resumes": [
++ {
++ "kind": "schedule",
++ "what": "pending custom domains are checked again; objects no deploy references and interrupted temp files are swept after an hour"
++ },
++ {
++ "kind": "outbox",
++ "what": "unsent events in the outbox table go to OpenVibe.Events once the relay starts (at least once; Events dedupes by event_id)"
++ }
++ ]
++ },
++ "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 (30 s in host.example.json); manual afterwards, as long as no blocker applies",
++ "blockers": {
++ "none": "the schema code only adds (CREATE … IF NOT EXISTS, ADD COLUMN; no DROP, RENAME or table rebuild), which an older release ignores"
++ }
++ },
++ "contracts": {
++ "range": ">=0.10.0 <1.0.0"
++ },
++ "leases": {
++ "claims": [
++ {
++ "what": "one ovhost operation at a time per service (deploy and rollback; a drill and backup --all take their own locks)",
++ "holder": "the ovhost process: <stateDir>/locks/<service>.lock holds its pid",
++ "expires": "when the operation releases it",
++ "fencing": "a lock whose pid is no longer alive is removed and taken again (exclusive create); a live holder refuses the second operation"
++ }
++ ]
++ }
++ },
"notes": "Stage A: the ovhost operator CLI (no server). Stage B: the Host API service on port 4910 (unit openvibe-host.service, state /var/lib/openvibe-host-api): tenant projects keyed by project id (prj_, network_project_id once Network has projects, ADR-014), static sites served by Host header at <site>.openvibe.host (a registrable domain separate from every other OpenVibe domain) and on TXT-verified custom domains, immutable content-addressed deploys with rollback, quotas enforced here, upload logs. Consumes OpenVibe.Network (SSO, JWKS, client-credentials) and publishes through OpenVibe.Events (events.event.publish). Stage C (sandboxed user code) is not started."
}
ddiff --git a/manifests/services/live.json b/manifests/services/live.json
iindex b1e5c0cb..f4cfb8c8 100644
---- a/manifests/services/live.json
++++ b/manifests/services/live.json
@@@ -58,5 +58,68 @@
"namespacesOwned": [
"live.*"
],
++ "lifecycle": {
++ "liveness": {
++ "endpoint": "/api/health",
++ "means": "the process is up and answers HTTP (uptime, chat connections); it checks nothing else (/api/ready flips only when every boot step the request paths need is done)"
++ },
++ "shutdown": {
++ "signal": "SIGTERM",
++ "deadlineSeconds": 5.3,
++ "drains": [
++ "server_restart notice to chat sockets",
++ "transcription, timeline-capture and media-analysis children killed; RobotStreamer passthroughs ended",
++ "readiness flips to not ready; job loops stopped; SSE streams closed",
++ "after 300 ms: restreams, recorder, call/chat/control/broadcast WebSockets, JSMPEG, SFU and RTMP stopped; analytics and the database closed",
++ "HTTP server closed"
++ ],
++ "note": "forced exit 5 s after the 300 ms notice delay; the listening socket belongs to openvibe-live.socket, so new HTTP connections queue through the restart while WebSockets, WHIP, RTMP and WebRTC sessions end"
++ },
++ "startupRecovery": {
++ "resumes": [
++ {
++ "kind": "sessions",
++ "what": "streams still marked live get a fresh heartbeat (broadcasters have 5 minutes to reconnect); their restreams, chat relays, RobotStreamer and AI bots start again"
++ },
++ {
++ "kind": "jobs",
++ "what": "VOD/clip transcripts left processing go back to retry (progress kept); earlier failures get a fresh retry ladder"
++ },
++ {
++ "kind": "outbox",
++ "what": "unsent stream events and index documents in the transactional outbox (server/events/stream-events.js) go to OpenVibe.Events; staged chat-table changes queued in chat_staged_outbox go to OpenVibe.Chat when CHAT_AUTHORITY=chat"
++ },
++ {
++ "kind": "consumer",
++ "what": "OpenVibe.Events redelivers media.vod.*, media.clip.*, media.storage.* and network.user.token_valid_after that were pending during the stop (at least once, deduped by event_id)"
++ }
++ ]
++ },
++ "rollback": {
++ "conditions": [
++ "automatic: deploy/scripts/deploy.sh switches back to the previous release when /api/ready does not answer within READY_TIMEOUT (90 s) after the restart (exit 3)",
++ "manual: deploy/scripts/deploy.sh --rollback switches to the previous release"
++ ],
++ "window": "automatic during the 90 s readiness wait; manual while the previous release is still in releases/ and no blocker applies",
++ "blockers": [
++ "ledger migrations (server/db/migrations.js) are forward-only and may transform data (001_vibes_decimal_to_bits multiplied Vibes amounts by 100): a release from before one misreads the data",
++ "with BILLING_AUTHORITY=billing, a release from before the money-authority tripwire would write the legacy money tables",
++ "with a staged chat table at chat (chat_table_authority), a release from before server/chat/chat-tables.js would write it directly"
++ ]
++ },
++ "contracts": {
++ "range": ">=0.2.0 <1.0.0"
++ },
++ "leases": {
++ "claims": [
++ {
++ "what": "write authority over money and over each staged chat table",
++ "holder": "BILLING_AUTHORITY (env) and app_state chat_table_authority, per table",
++ "expires": "only when an operator hands it over (POST /internal/chat-tables/:table; the Billing cutover runbook)",
++ "fencing": "a database-level tripwire throws on any write to the money tables when BILLING_AUTHORITY=billing; chat-tables write() sends a staged table's writes to Chat once it is at chat"
++ }
++ ]
++ }
++ },
"notes": "Still hosts chat, ingest, money and AI authorities until Waves 6-13; paste authority moved to Community (W5)."
}
ddiff --git a/manifests/services/media.json b/manifests/services/media.json
iindex bf47480d..91c110a1 100644
---- a/manifests/services/media.json
++++ b/manifests/services/media.json
@@@ -61,5 +61,65 @@
],
"namespacesOwned": [
"media.*"
-- ]
++ ],
++ "lifecycle": {
++ "liveness": {
++ "endpoint": "/healthz",
++ "means": "the process answers and reads its apps table and active recordings (a database failure makes it fail); storage and the Network key are what /api/ready checks"
++ },
++ "shutdown": {
++ "signal": "SIGTERM",
++ "deadlineSeconds": 70,
++ "drains": [
++ "recordings stopped: ffmpeg gets SIGINT to write its trailer, then SIGTERM after VOD_STOP_GRACE_MS (60 s)",
++ "storage tiering, health scan, copy verification, media_jobs worker and owner-subject jobs stopped",
++ "JWKS refresh stopped, event relay reset, timers cleared",
++ "HTTP server closed, then the database"
++ ],
++ "note": "forced exit after 70 s"
++ },
++ "startupRecovery": {
++ "resumes": [
++ {
++ "kind": "jobs",
++ "what": "media_jobs left running are retried (attempts left) or failed; failed clip cuts are re-cut with auto-retry"
++ },
++ {
++ "kind": "sessions",
++ "what": "recordings left is_recording with no ffmpeg are finalized from what reached the disk (a vod.finalize job, or once at boot without the job worker)"
++ },
++ {
++ "kind": "outbox",
++ "what": "unsent media events in the outbox go to OpenVibe.Events; search documents resume"
++ },
++ {
++ "kind": "consumer",
++ "what": "OpenVibe.Events redelivers network.user.token_valid_after that were pending during the stop (at least once, deduped by event_id)"
++ }
++ ]
++ },
++ "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 (60 s in host.example.json); manual afterwards, as long as no blocker applies",
++ "blockers": [
++ "media_jobs was rebuilt once into the app_id shape (server/db/database.js migrateJobsTable): releases from before it predate that table"
++ ]
++ },
++ "contracts": {
++ "range": ">=0.2.0 <1.0.0"
++ },
++ "leases": {
++ "claims": [
++ {
++ "what": "a media_jobs row while its handler runs",
++ "holder": "the Media process (status running)",
++ "expires": "lease_until = claim + 120 s, renewed while the handler runs; an expired lease makes the job interrupted (retried or failed)",
++ "fencing": "none beyond status: succeed() and fail() match status = running only, with no owner token, so this is safe only because one Media process runs the queue (lease-expiry recovery skips the jobs that process still runs)"
++ }
++ ]
++ }
++ }
}
ddiff --git a/manifests/services/network.json b/manifests/services/network.json
iindex 381e8979..69400abd 100644
---- a/manifests/services/network.json
++++ b/manifests/services/network.json
@@@ -84,5 +84,58 @@
"network.*",
"theme.*",
"notification.*"
-- ]
++ ],
++ "lifecycle": {
++ "liveness": {
++ "endpoint": "/api/health",
++ "means": "the process is up and answers HTTP; it checks nothing else (the database and dependencies are what ready checks)"
++ },
++ "shutdown": {
++ "signal": "SIGTERM",
++ "deadlineSeconds": 10,
++ "drains": [
++ "stops accepting connections and closes idle keep-alive ones",
++ "requests in flight finish",
++ "exit"
++ ],
++ "note": "timers, pollers and relays are not stopped; the forced exit after 10 s ends them, and their work is kept in the database"
++ },
++ "startupRecovery": {
++ "resumes": [
++ {
++ "kind": "outbox",
++ "what": "the developer-app, profile and grant event outboxes (developer/event-relay.js, identity/profile-events.js) send what is unsent"
++ },
++ {
++ "kind": "jobs",
++ "what": "the email queue is processed again every 2 minutes; the retired-owner module sweep runs 5 minutes after boot"
++ },
++ {
++ "kind": "state",
++ "what": "the ecosystem registry polls every service's readiness and release again; deploy drift and library tags refresh"
++ },
++ {
++ "kind": "consumer",
++ "what": "OpenVibe.Events redelivers the moderation, staff and module events it consumes that were pending during the stop (at least once, deduped by event_id)"
++ }
++ ]
++ },
++ "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 (30 s in host.example.json); manual afterwards, as long as no blocker applies",
++ "blockers": [
++ "chrome_cache/chrome_hits were renamed to frame_cache/frame_hits (2026-09-24): a release from before that starts with empty caches",
++ "tool_domains was rebuilt once to allow the mirror role: a release from before it does not know mirror rows"
++ ]
++ },
++ "contracts": {
++ "range": ">=0.2.0 <1.0.0"
++ },
++ "leases": {
++ "none": "no claims: one process owns network.db (better-sqlite3, single writer); a stream-live announcement is claimed per streamer with a cooldown and a daily cap in the same database"
++ }
++ }
}
ddiff --git a/manifests/services/news.json b/manifests/services/news.json
iindex 23ed2621..b09d561f 100644
---- a/manifests/services/news.json
++++ b/manifests/services/news.json
@@@ -63,5 +63,53 @@
"namespacesOwned": [
"news.*"
],
++ "lifecycle": {
++ "liveness": {
++ "endpoint": "/api/health",
++ "means": "the process is up and answers HTTP; it checks nothing else (the database and dependencies are what ready checks)"
++ },
++ "shutdown": {
++ "signal": "SIGTERM",
++ "deadlineSeconds": 5,
++ "drains": [
++ "worker stopped (Sources cursor pull)",
++ "HTTP server closed",
++ "outbox relay stopped (awaited)",
++ "store closed"
++ ]
++ },
++ "startupRecovery": {
++ "resumes": [
++ {
++ "kind": "consumer",
++ "what": "the OpenVibe.Sources change feed is pulled again from the stored cursor, so items published during the stop arrive (idempotent per item revision)"
++ },
++ {
++ "kind": "consumer",
++ "what": "OpenVibe.Events redelivers sources.item.* and sources.fetch.failed that were pending during the stop (at least once, deduped by event_id)"
++ },
++ {
++ "kind": "outbox",
++ "what": "unsent events in the outbox table go to OpenVibe.Events once the relay starts (at least once; Events dedupes by event_id)"
++ }
++ ]
++ },
++ "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 (30 s in host.example.json); manual afterwards, as long as no blocker applies",
++ "blockers": {
++ "none": "the schema code only adds (CREATE … IF NOT EXISTS, ADD COLUMN; no DROP, RENAME or table rebuild), which an older release ignores"
++ }
++ },
++ "contracts": {
++ "range": ">=0.19.0 <1.0.0"
++ },
++ "leases": {
++ "none": "no claims: one process owns news.db (better-sqlite3, single writer)"
++ }
++ },
"notes": "Port 4820. Owns news_topics, news_source_items, news_story_clusters, news_stories, news_story_revisions, news_story_sources, news_perspectives, news_timeline_entries, news_editorial_flags in its own SQLite (openvibe-publishing packages, prefixes news_story and news). Consumes OpenVibe.Sources (sources.item.read, optionally sources.source.read) through an Events subscription (sources.item.*, sources.fetch.failed; events.subscription.manage) and a cursor pull; OpenVibe.Network (SSO, identity.subject.resolve); optionally OpenVibe.AI (ai.run.create, namespace news) for news.summarize_story and news.compare_perspectives drafts; publishes through OpenVibe.Events (events.event.publish); OpenVibe.Search reads news.index_document.* from Events."
}
ddiff --git a/manifests/services/openre.json b/manifests/services/openre.json
iindex d1c6e4f0..362a929b 100644
---- a/manifests/services/openre.json
++++ b/manifests/services/openre.json
@@@ -50,5 +50,72 @@
"namespacesOwned": [
"openre.*"
],
++ "lifecycle": {
++ "liveness": {
++ "endpoint": "/api/health",
++ "means": "the process is up and answers HTTP (openre-api; the coordinator and workers are separate processes); it checks nothing else (the database and dependencies are what ready checks)"
++ },
++ "shutdown": {
++ "signal": "SIGTERM",
++ "deadlineSeconds": 10,
++ "drains": [
++ "openre-api: handles closed; workers are separate processes and keep running",
++ "openre-session-coordinator: coordinator stopped, then the database"
++ ],
++ "workers": {
++ "deadlineSeconds": 86400,
++ "drains": [
++ "SIGTERM starts a drain: the public listener closes; publishers and restream outputs keep running until they end or OPENRE_DRAIN_MAX_MS (default 24 h) passes",
++ "at that deadline ingest sessions end and restream outputs move to the newest generation; the worker exits 0 once idle"
++ ]
++ },
++ "note": "api and coordinator force-exit after 10 s; ovhost never stops or restarts the worker units (a deploy starts a new generation and the coordinator drains the old one)"
++ },
++ "startupRecovery": {
++ "resumes": [
++ {
++ "kind": "sessions",
++ "what": "the coordinator fails the open sessions of workers without a heartbeat (worker_lost) and of expired leases; their outputs go back to pending for the newest restream generation"
++ },
++ {
++ "kind": "state",
++ "what": "a new worker registers as the next generation; older ready generations drain"
++ },
++ {
++ "kind": "outbox",
++ "what": "unsent openre.* events go to OpenVibe.Events from the coordinator's relay"
++ }
++ ]
++ },
++ "rollback": {
++ "conditions": [
++ "automatic: deploy/scripts/deploy.sh switches back to the previous release when openre-api is not ready within 60 s",
++ "manual: a deploy of the previous release (workers: start the previous generation; the coordinator drains the newer one)"
++ ],
++ "window": "automatic during the 60 s readiness wait; manual while the previous release is still in releases/",
++ "blockers": {
++ "none": "the schema code only adds (CREATE … IF NOT EXISTS, ADD COLUMN; no DROP, RENAME or table rebuild), which an older release ignores"
++ }
++ },
++ "contracts": {
++ "range": ">=0.13.0 <1.0.0"
++ },
++ "leases": {
++ "claims": [
++ {
++ "what": "the coordinator role",
++ "holder": "leases row coordinator, holder coordinator:<pid>",
++ "expires": "OPENRE_WORKER_LEASE_MS (15 s), renewed every tick",
++ "fencing": "taken only when the row is the caller's or expired, so a deploy overlap leaves one coordinator acting"
++ },
++ {
++ "what": "a worker and every session it owns",
++ "holder": "the worker process (worker_id, generation) by heartbeat every 2 s",
++ "expires": "lost after 15 s without a heartbeat (OPENRE_WORKER_LEASE_MS); ingest sessions carry lease_expires_at",
++ "fencing": "the coordinator fails a lost worker's sessions (worker_lost) and reassigns its outputs; session changes are compare-and-set on state, so whoever commits first wins; new sessions reach only the newest ready generation"
++ }
++ ]
++ }
++ },
"notes": "Ingest and restream authority (ADR-009). Alpha: RTMP ingest and RTMP/SRT restream run in transport workers with generations and leases, separate from the API; WHIP, WebRTC/SFU and JSMPEG are still ingested by Live. Runs in parallel with Live; slots move one at a time behind Live's per-slot ingest_authority switch."
}
ddiff --git a/manifests/services/publishing.json b/manifests/services/publishing.json
iindex 44542191..4fa011a8 100644
---- a/manifests/services/publishing.json
++++ b/manifests/services/publishing.json
@@@ -16,5 +16,31 @@
"eventsProduced": [],
"eventsConsumed": [],
"namespacesOwned": [],
++ "lifecycle": {
++ "liveness": {
++ "none": "a library (openvibe-publishing); nothing runs"
++ },
++ "shutdown": {
++ "none": "a library (openvibe-publishing); nothing runs"
++ },
++ "startupRecovery": {
++ "none": "a library (openvibe-publishing); nothing runs"
++ },
++ "rollback": {
++ "conditions": [
++ "manual: consumers pin release tags; going back means pinning the previous tag in package.json and redeploying the consumer"
++ ],
++ "window": "any time; a published tag is never moved or deleted",
++ "blockers": [
++ "a consumer that already uses an API added in the newer tag must drop that use first"
++ ]
++ },
++ "leases": {
++ "none": "a library (openvibe-publishing); nothing runs"
++ },
++ "contracts": {
++ "none": "no manifest range: it pins openvibe-contracts v0.49.0 in package.json"
++ }
++ },
"notes": "Shared publishing packages for the publication products (openvibe-publishing, ADR-019): revisions and diffs, scheduling, taxonomy, citations and the fact gate, media attachments, discussion references, the SEO indexability gate, feeds, redirects, authorship, index hooks (the *.index_document.* and publication events) and SSR helpers. A released library, not a runtime or an authority: tagged releases installed from https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/<tag> (v0.2.1 on 2026-09-23) and used by Wiki, Blog, News, Reviews, Deals, Coupons and Trade. Nothing to deploy or poll. The roadmap §30.2 Publishing row (publishing.revision.*, schedule.*, citation.*, seo.evaluate, feed.read, redirect.resolve) is deliberately not registered as capabilities: a package has no boundary where a grant could be checked, and every route that performs those actions belongs to a product that already names it (wiki.page.read|create, wiki.revision.propose|publish|revert, wiki.citation.attach, blog.post.*, blog.feed.read, news.story.read|revise|publish|retract, news.source.attach). The SDK's publishing subpath wraps those product capabilities."
}
ddiff --git a/manifests/services/realtime.json b/manifests/services/realtime.json
iindex ac245583..97459c1d 100644
---- a/manifests/services/realtime.json
++++ b/manifests/services/realtime.json
@@@ -16,5 +16,25 @@
"eventsProduced": [],
"eventsConsumed": [],
"namespacesOwned": [],
++ "lifecycle": {
++ "liveness": {
++ "none": "retired (ADR-005); nothing runs"
++ },
++ "shutdown": {
++ "none": "retired (ADR-005); nothing runs"
++ },
++ "startupRecovery": {
++ "none": "retired (ADR-005); nothing runs"
++ },
++ "rollback": {
++ "none": "retired (ADR-005); nothing is deployed"
++ },
++ "contracts": {
++ "range": "^0.1.0"
++ },
++ "leases": {
++ "none": "retired (ADR-005); nothing runs"
++ }
++ },
"notes": "Closed by ADR-005 (accepted 2026-09-22): browser realtime is a delivery plane inside OpenVibe.Events (SSE /realtime/stream with per-topic authorisation, Last-Event-ID resume and gap markers), so OpenVibe.Realtime is not built. The repository holds its charter and the closure note only: no runtime, domain, capability or event. Reopened only as ADR-005 says (measured connection counts or fan-out latency that need an independently scaled process), by a new ADR."
}
ddiff --git a/manifests/services/reviews.json b/manifests/services/reviews.json
iindex 4faa0ee3..1572d92e 100644
---- a/manifests/services/reviews.json
++++ b/manifests/services/reviews.json
@@@ -57,5 +57,57 @@
"namespacesOwned": [
"reviews.*"
],
++ "lifecycle": {
++ "liveness": {
++ "endpoint": "/api/health",
++ "means": "the process is up and answers HTTP; it checks nothing else (the database and dependencies are what ready checks)"
++ },
++ "shutdown": {
++ "signal": "SIGTERM",
++ "deadlineSeconds": 10,
++ "drains": [
++ "Sources pull and import-queue timers cleared",
++ "outbox relay stopped (awaited)",
++ "HTTP server closed",
++ "database closed"
++ ]
++ },
++ "startupRecovery": {
++ "resumes": [
++ {
++ "kind": "consumer",
++ "what": "the OpenVibe.Sources change feed is pulled again from the stored cursor, so items published during the stop arrive (idempotent per item revision)"
++ },
++ {
++ "kind": "jobs",
++ "what": "the import queue drains what was left in it"
++ },
++ {
++ "kind": "consumer",
++ "what": "OpenVibe.Events redelivers sources.item.* that were pending during the stop (at least once, deduped by event_id)"
++ },
++ {
++ "kind": "outbox",
++ "what": "unsent events in the outbox table go to OpenVibe.Events once the relay starts (at least once; Events dedupes by event_id)"
++ }
++ ]
++ },
++ "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 (30 s in host.example.json); manual afterwards, as long as no blocker applies",
++ "blockers": {
++ "none": "the schema code only adds (CREATE … IF NOT EXISTS, ADD COLUMN; no DROP, RENAME or table rebuild), which an older release ignores"
++ }
++ },
++ "contracts": {
++ "range": ">=0.19.0 <1.0.0"
++ },
++ "leases": {
++ "none": "no claims: one process owns reviews.db (better-sqlite3, single writer)"
++ }
++ },
"notes": "Entities with aliases and typed links; resolution of OpenVibe.Sources review items by deterministic rules (source binding, URL, GTIN/SKU/MPN) with editor confirmation for name-only and conflicting matches; merges that rewrite nothing and splits that restore attribution exactly, both audited; typed signals with provenance (source item, item revision, retrieval time, licence note) and trust metadata; aggregates computed only from signals present (method reviews-aggregate@1, inputs and exclusions exposed, absent without qualifying signals, a new revision on every change); editor-written or AI-drafted-and-approved summaries whose points cite signals and never carry a rating; source removal withdraws signals, flags citing summaries and prepares a pending revision; corrections queue; Community discussion referenced, not copied. Server-rendered pages useful without JavaScript; AggregateRating/Product/Review JSON-LD only when signals back it; sitemaps, feeds and Search index events from the openvibe-publishing v0.2.0 gate. Deployed internally (loopback only), not launched publicly: openvibe.reviews serves the OpenVibe.Sites placeholder until the launch rule holds."
}
ddiff --git a/manifests/services/sdk.json b/manifests/services/sdk.json
iindex f4dffae6..c616b41a 100644
---- a/manifests/services/sdk.json
++++ b/manifests/services/sdk.json
@@@ -15,5 +15,31 @@
"eventsProduced": [],
"eventsConsumed": [],
"namespacesOwned": [],
++ "lifecycle": {
++ "liveness": {
++ "none": "a library (openvibe-sdk); nothing runs"
++ },
++ "shutdown": {
++ "none": "a library (openvibe-sdk); nothing runs"
++ },
++ "startupRecovery": {
++ "none": "a library (openvibe-sdk); nothing runs"
++ },
++ "rollback": {
++ "conditions": [
++ "manual: consumers pin release tags; going back means pinning the previous tag in package.json and redeploying the consumer"
++ ],
++ "window": "any time; a published tag is never moved or deleted",
++ "blockers": [
++ "a consumer that already uses an API added in the newer tag must drop that use first"
++ ]
++ },
++ "leases": {
++ "none": "a library (openvibe-sdk); nothing runs"
++ },
++ "contracts": {
++ "none": "no manifest range: it pins openvibe-contracts v0.49.0 in package.json"
++ }
++ },
"notes": "Supported browser and server clients for the OpenVibe platform (openvibe-sdk). A released library, not a runtime: tagged releases installed from https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/<tag> (v0.4.0 on 2026-09-23). Nothing to deploy or poll."
}
ddiff --git a/manifests/services/search.json b/manifests/services/search.json
iindex 86a2f02b..3a461717 100644
---- a/manifests/services/search.json
++++ b/manifests/services/search.json
@@@ -33,5 +33,55 @@
"*.index_document.deleted"
],
"namespacesOwned": [],
++ "lifecycle": {
++ "liveness": {
++ "endpoint": "/api/health",
++ "means": "the process is up and answers HTTP; it checks nothing else (the database and dependencies are what ready checks)"
++ },
++ "shutdown": {
++ "signal": "SIGTERM",
++ "deadlineSeconds": 10,
++ "drains": [
++ "outbox prune timer and key refresh stopped",
++ "outbox relay stopped (awaited)",
++ "CDN purger stopped (awaited)",
++ "open HTTP connections closed, then the server",
++ "database closed"
++ ],
++ "note": "the forced exit after 10 s exits 1"
++ },
++ "startupRecovery": {
++ "resumes": [
++ {
++ "kind": "outbox",
++ "what": "unsent events in the outbox table go to OpenVibe.Events once the relay starts (at least once; Events dedupes by event_id)"
++ },
++ {
++ "kind": "jobs",
++ "what": "the purger resumes the cdn_purges rows still pending; the removals feed stays readable by products"
++ },
++ {
++ "kind": "consumer",
++ "what": "OpenVibe.Events redelivers *.index_document.upserted and *.index_document.deleted that were pending during the stop (at least once, deduped by event_id)"
++ }
++ ]
++ },
++ "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 (30 s in host.example.json); manual afterwards, as long as no blocker applies",
++ "blockers": {
++ "none": "the schema code only adds (CREATE … IF NOT EXISTS, ADD COLUMN; no DROP, RENAME or table rebuild), which an older release ignores"
++ }
++ },
++ "contracts": {
++ "range": ">=0.7.0 <1.0.0"
++ },
++ "leases": {
++ "none": "no claims: one process owns search.db (better-sqlite3, single writer)"
++ }
++ },
"notes": "Canonical index document (search.index-document@1), event-fed and direct indexing with revision ordering and tombstones, permission-aware query over SQLite FTS5 (docs/adr-engine.md). Anonymous queries need no token."
}
ddiff --git a/manifests/services/shared.json b/manifests/services/shared.json
iindex 5ff25e0b..89d14229 100644
---- a/manifests/services/shared.json
++++ b/manifests/services/shared.json
@@@ -16,5 +16,31 @@
"eventsProduced": [],
"eventsConsumed": [],
"namespacesOwned": [],
++ "lifecycle": {
++ "liveness": {
++ "none": "a library (openvibe-shared); nothing runs"
++ },
++ "shutdown": {
++ "none": "a library (openvibe-shared); nothing runs"
++ },
++ "startupRecovery": {
++ "none": "a library (openvibe-shared); nothing runs"
++ },
++ "rollback": {
++ "conditions": [
++ "manual: consumers pin release tags; going back means pinning the previous tag in package.json and redeploying the consumer"
++ ],
++ "window": "any time; a published tag is never moved or deleted",
++ "blockers": [
++ "a consumer that already uses an API added in the newer tag must drop that use first"
++ ]
++ },
++ "leases": {
++ "none": "a library (openvibe-shared); nothing runs"
++ },
++ "contracts": {
++ "none": "no manifest range: it pins openvibe-contracts v0.52.0 in package.json"
++ }
++ },
"notes": "Versioned UI, chrome, SEO, legal and release-client packages every OpenVibe site renders (openvibe-shared). A released library, not a runtime: tagged releases installed from https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/<tag> (v1.3.0 on 2026-09-23) and served at /shared/* by each site. Nothing to deploy or poll."
}
ddiff --git a/manifests/services/sites.json b/manifests/services/sites.json
iindex bad5875b..bdd58c51 100644
---- a/manifests/services/sites.json
++++ b/manifests/services/sites.json
@@@ -17,5 +17,31 @@
"eventsProduced": [],
"eventsConsumed": [],
"namespacesOwned": [],
++ "lifecycle": {
++ "liveness": {
++ "none": "static files served by nginx; there is no process of its own"
++ },
++ "shutdown": {
++ "none": "static files served by nginx; there is no process of its own to stop"
++ },
++ "startupRecovery": {
++ "none": "static files; nothing is in flight"
++ },
++ "rollback": {
++ "conditions": [
++ "manual: ovhost rollback sites --to <sha> rebuilds that commit and installs its vhosts (nginx -t before the reload); there is no ready URL, so nothing is automatic"
++ ],
++ "window": "any time; each deploy builds from a commit",
++ "blockers": {
++ "none": "a static build with no database"
++ }
++ },
++ "contracts": {
++ "range": ">=0.2.0 <1.0.0"
++ },
++ "leases": {
++ "none": "static files; nothing runs"
++ }
++ },
"notes": "Static placeholder generator; not a runtime."
}
ddiff --git a/manifests/services/sources.json b/manifests/services/sources.json
iindex 7b914d1c..51b12d13 100644
---- a/manifests/services/sources.json
++++ b/manifests/services/sources.json
@@@ -34,5 +34,50 @@
],
"eventsConsumed": [],
"namespacesOwned": [],
++ "lifecycle": {
++ "liveness": {
++ "endpoint": "/api/health",
++ "means": "the process is up and answers HTTP; it checks nothing else (the database and dependencies are what ready checks)"
++ },
++ "shutdown": {
++ "signal": "SIGTERM",
++ "deadlineSeconds": 20,
++ "drains": [
++ "scheduler stopped; runs in flight get up to 15 s to finish",
++ "outbox relay stopped (awaited)",
++ "open HTTP connections closed, then the server",
++ "database closed"
++ ],
++ "note": "the forced exit after 20 s exits 1"
++ },
++ "startupRecovery": {
++ "resumes": [
++ {
++ "kind": "schedule",
++ "what": "sources whose next_due_at passed during the stop are fetched on the first ticks, oldest first"
++ },
++ {
++ "kind": "outbox",
++ "what": "unsent events in the outbox table go to OpenVibe.Events once the relay starts (at least once; Events dedupes by event_id)"
++ }
++ ]
++ },
++ "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 (30 s in host.example.json); manual afterwards, as long as no blocker applies",
++ "blockers": {
++ "none": "the schema code only adds (CREATE … IF NOT EXISTS, ADD COLUMN; no DROP, RENAME or table rebuild), which an older release ignores"
++ }
++ },
++ "contracts": {
++ "range": ">=0.7.0 <1.0.0"
++ },
++ "leases": {
++ "none": "no claims: one process owns sources.db (better-sqlite3, single writer); a second run of a source already in flight is refused in memory"
++ }
++ },
"notes": "Source registry and ingestion workers (RSS/Atom, sitemaps, JSON-LD, mapped official APIs, manual). Every item carries provenance; every fetch is a recorded run with an explicit state; failures never create or modify items."
}
ddiff --git a/manifests/services/tips.json b/manifests/services/tips.json
iindex e5018219..a4a52f73 100644
---- a/manifests/services/tips.json
++++ b/manifests/services/tips.json
@@@ -65,5 +65,57 @@
"namespacesOwned": [
"tips.*"
],
++ "lifecycle": {
++ "liveness": {
++ "endpoint": "/api/health",
++ "means": "the process answers and reports the outbox status from its database; it checks no dependency (Billing and the Network key are what ready checks)"
++ },
++ "shutdown": {
++ "signal": "SIGTERM",
++ "deadlineSeconds": 5,
++ "drains": [
++ "job timers cleared",
++ "overlay streams closed",
++ "outbox relay and Network key refresh stopped",
++ "HTTP server closed, then the database"
++ ]
++ },
++ "startupRecovery": {
++ "resumes": [
++ {
++ "kind": "jobs",
++ "what": "pending chat effects are delivered again with retries; due Billing transfers of funded checkouts are sent again"
++ },
++ {
++ "kind": "schedule",
++ "what": "overlay deliveries past their window are marked failed"
++ },
++ {
++ "kind": "consumer",
++ "what": "OpenVibe.Events redelivers billing.transaction.settled, billing.transaction.reversed and billing.receipt.external that were pending during the stop (at least once, deduped by event_id)"
++ },
++ {
++ "kind": "outbox",
++ "what": "unsent events in the outbox table go to OpenVibe.Events once the relay starts (at least once; Events dedupes by event_id)"
++ }
++ ]
++ },
++ "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 (30 s in host.example.json); manual afterwards, as long as no blocker applies",
++ "blockers": {
++ "none": "the schema code only adds (CREATE … IF NOT EXISTS, ADD COLUMN; no DROP, RENAME or table rebuild), which an older release ignores"
++ }
++ },
++ "contracts": {
++ "range": ">=0.11.0 <1.0.0"
++ },
++ "leases": {
++ "none": "no claims: one process owns tips.db (better-sqlite3, single writer); an effect moves with a compare-and-set on its state"
++ }
++ },
"notes": "Creator support (roadmap Wave 9): tip interactions whose payment state mirrors OpenVibe.Billing (keyed by Billing transaction id) and whose delivery state is Tips' own; goals derived from settled interactions; paid messages, TTS and media requests created on settlement; scoped revocable overlay tokens with SSE overlays; simulation mode. Tips never moves money (ADR-012). Alpha: deployed internally (loopback only), not launched publicly: openvibe.tips serves the OpenVibe.Sites placeholder until the launch rule in README.md holds."
}
ddiff --git a/manifests/services/tools.json b/manifests/services/tools.json
iindex adffc5ec..82ac0975 100644
---- a/manifests/services/tools.json
++++ b/manifests/services/tools.json
@@@ -47,5 +47,52 @@
"namespacesOwned": [
"tools.*"
],
++ "lifecycle": {
++ "liveness": {
++ "endpoint": "/api/health",
++ "means": "the gateway answers HTTP and reports the tool registry, run API and jobs facade counters; it checks nothing else"
++ },
++ "shutdown": {
++ "signal": "SIGTERM",
++ "deadlineSeconds": 5,
++ "drains": [
++ "tool apps (maps, food, img, yt, audio, text, docs): analytics flushed and closed, jobs store and worker pool closed (img, audio, docs), guard closed, HTTP server closed",
++ "running jobs stay running in jobs.db for the next boot"
++ ],
++ "note": "the gateway (openvibe-tools.service) has no SIGTERM handler: Node's default action ends it at once, cutting requests in flight; the apps force-exit 1 after 5 s"
++ },
++ "startupRecovery": {
++ "resumes": [
++ {
++ "kind": "jobs",
++ "what": "jobs left running in img, audio and docs jobs.db are re-queued while attempts remain (onRestart requeue), else failed as retryable"
++ },
++ {
++ "kind": "outbox",
++ "what": "unsent tools.job.* events go to OpenVibe.Events"
++ },
++ {
++ "kind": "consumer",
++ "what": "OpenVibe.Events redelivers network.user.token_valid_after that were pending during the stop (at least once, deduped by event_id)"
++ }
++ ]
++ },
++ "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 (60 s in host.example.json); manual afterwards, as long as no blocker applies",
++ "blockers": {
++ "none": "the schema code only adds (CREATE … IF NOT EXISTS, ADD COLUMN; no DROP, RENAME or table rebuild), which an older release ignores"
++ }
++ },
++ "contracts": {
++ "range": ">=0.7.0 <1.0.0"
++ },
++ "leases": {
++ "none": "no claims: one process owns each app's jobs.db (better-sqlite3, single writer); a job is taken with a compare-and-set (queued → running)"
++ }
++ },
"notes": "Public at openvibe.tools: 169 tools in 9 families on the gateway (port 4001) and its satellites, catalogue at GET /api/catalog.json (re-served by Network). Asynchronous jobs (Wave 11) run on the img, audio and docs satellites only (POST /api/v1/jobs, GET/DELETE /api/v1/jobs/:id, POST /api/v1/jobs/:id/retry, PUT|DELETE /api/v1/jobs/:id/references/:ref, SSE events, result files); the gateway has no /api/v1/jobs yet. Job results are stored as private Media objects (TOOLS_JOB_RESULTS=media: Tools holds media.object.upload and media.object.read, namespace tools) and job lifecycle events tools.job.created|started|succeeded|failed are published to OpenVibe.Events through an outbox in each satellite's jobs.db (EVENTS_URL set; events.event.publish). The tool registry (ADR-027, tools.tool.read) is served: GET /api/v1/tools[/:id[/schema]] on the gateway, for all 169 tools, and on the img, audio, docs, text, yt, maps and food satellites for their own. Planned (ADR-027): the uniform run API POST /api/v1/tools/:id/run (tools.tool.run; network probes need tools.net.probe), a jobs facade on the gateway, tiered quotas and a pseudonymous abuse log; the legacy /api/process, /api/net/* and /api/dev/* routes keep working with Deprecation and Sunset (2026-12-31) headers. GET /api/ready is truthful on the gateway (it checks every satellite) and on each satellite."
}
ddiff --git a/manifests/services/trade.json b/manifests/services/trade.json
iindex 317acc0c..278eaabb 100644
---- a/manifests/services/trade.json
++++ b/manifests/services/trade.json
@@@ -59,5 +59,57 @@
"namespacesOwned": [
"trade.*"
],
++ "lifecycle": {
++ "liveness": {
++ "endpoint": "/api/health",
++ "means": "the process is up and answers HTTP; it checks nothing else (the database and dependencies are what ready checks)"
++ },
++ "shutdown": {
++ "signal": "SIGTERM",
++ "deadlineSeconds": 5,
++ "drains": [
++ "worker stopped (Sources sync, freshness)",
++ "HTTP server closed",
++ "outbox relay stopped (awaited)",
++ "store closed"
++ ]
++ },
++ "startupRecovery": {
++ "resumes": [
++ {
++ "kind": "consumer",
++ "what": "the OpenVibe.Sources change feed is pulled again from the stored cursor, so items published during the stop arrive (idempotent per item revision)"
++ },
++ {
++ "kind": "schedule",
++ "what": "freshness is re-evaluated on Trade's clock; documents whose gate changed are sent again"
++ },
++ {
++ "kind": "consumer",
++ "what": "OpenVibe.Events redelivers sources.item.* and sources.fetch.failed that were pending during the stop (at least once, deduped by event_id)"
++ },
++ {
++ "kind": "outbox",
++ "what": "unsent events in the outbox table go to OpenVibe.Events once the relay starts (at least once; Events dedupes by event_id)"
++ }
++ ]
++ },
++ "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 (30 s in host.example.json); manual afterwards, as long as no blocker applies",
++ "blockers": {
++ "none": "the schema code only adds (CREATE … IF NOT EXISTS, ADD COLUMN; no DROP, RENAME or table rebuild), which an older release ignores"
++ }
++ },
++ "contracts": {
++ "range": ">=0.19.0 <1.0.0"
++ },
++ "leases": {
++ "none": "no claims: one process owns trade.db (better-sqlite3, single writer)"
++ }
++ },
"notes": "Port 4860. Informational only (ADR-025): no custody, order execution, escrow, listings between users or personalised advice. Owns trade_instruments, trade_instrument_aliases, trade_watchlists, trade_watchlist_items, trade_market_observations, trade_source_documents, trade_alert_rules, trade_alert_deliveries, trade_context_revisions in its own SQLite. Reads OpenVibe.Sources (sources.item.read, sources.source.read, category trade) by cursor and wakes on signed sources.* webhook deliveries; consumes OpenVibe.AI trade.summarize_market_context output as reviewed drafts; publishes through OpenVibe.Events (events.event.publish); OpenVibe.Search reads trade.index_document.* from Events."
}
ddiff --git a/manifests/services/vip.json b/manifests/services/vip.json
iindex f0e61325..e93c01e2 100644
---- a/manifests/services/vip.json
++++ b/manifests/services/vip.json
@@@ -55,5 +55,52 @@
"namespacesOwned": [
"vip.*"
],
++ "lifecycle": {
++ "liveness": {
++ "endpoint": "/api/health",
++ "means": "the process answers and reports its release and the outbox status from its database; it checks no dependency"
++ },
++ "shutdown": {
++ "signal": "SIGTERM",
++ "deadlineSeconds": 5,
++ "drains": [
++ "job timers cleared",
++ "outbox relay, Network key refresh and metrics stopped",
++ "HTTP server closed, then the database"
++ ]
++ },
++ "startupRecovery": {
++ "resumes": [
++ {
++ "kind": "schedule",
++ "what": "entitlement projections due for re-confirmation with Billing are refreshed"
++ },
++ {
++ "kind": "consumer",
++ "what": "OpenVibe.Events redelivers billing.entitlement.changed, billing.subscription.canceled and billing.transaction.reversed that were pending during the stop (at least once, deduped by event_id)"
++ },
++ {
++ "kind": "outbox",
++ "what": "unsent events in the outbox table go to OpenVibe.Events once the relay starts (at least once; Events dedupes by event_id)"
++ }
++ ]
++ },
++ "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 (30 s in host.example.json); manual afterwards, as long as no blocker applies",
++ "blockers": [
++ "idx_vip_rules_resource (one active rule per resource) was replaced by a per-creator unique index: a release from before it recreates the narrower index, which fails at boot once two creators gate the same resource"
++ ]
++ },
++ "contracts": {
++ "range": ">=0.14.0 <1.0.0"
++ },
++ "leases": {
++ "none": "no claims: one process owns vip.db (better-sqlite3, single writer)"
++ }
++ },
"notes": "Memberships (ADR-012): plans, immutable versioned plan terms, perks, product bindings, member preferences and gated-resource policy. OpenVibe.Billing holds subscriptions, charges, renewals, refunds and the canonical entitlements; VIP keeps a short-lived projection of them (valid_until, authoritative fallback, 'unknown' never authorizes). Alpha: deployed internally (loopback only), not launched publicly: openvibe.vip serves the OpenVibe.Sites placeholder until the launch rule holds."
}
ddiff --git a/manifests/services/wiki.json b/manifests/services/wiki.json
iindex 9271938a..4117d240 100644
---- a/manifests/services/wiki.json
++++ b/manifests/services/wiki.json
@@@ -52,5 +52,56 @@
"namespacesOwned": [
"wiki.*"
],
++ "lifecycle": {
++ "liveness": {
++ "endpoint": "/api/health",
++ "means": "the process is up and answers HTTP; it checks nothing else (the database and dependencies are what ready checks)"
++ },
++ "shutdown": {
++ "signal": "SIGTERM",
++ "deadlineSeconds": 10,
++ "drains": [
++ "schedule, media-check and projects timers cleared",
++ "outbox relay stopped (awaited)",
++ "HTTP server closed",
++ "database closed"
++ ]
++ },
++ "startupRecovery": {
++ "resumes": [
++ {
++ "kind": "schedule",
++ "what": "scheduled publish/unpublish jobs that fell due, and running jobs whose lease expired, are claimed again (openvibe-publishing/schedule, at least once, re-run safe)"
++ },
++ {
++ "kind": "outbox",
++ "what": "unsent events in the outbox table go to OpenVibe.Events once the relay starts (at least once; Events dedupes by event_id)"
++ }
++ ]
++ },
++ "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 (30 s in host.example.json); manual afterwards, as long as no blocker applies",
++ "blockers": {
++ "none": "the schema code only adds (CREATE … IF NOT EXISTS, ADD COLUMN; no DROP, RENAME or table rebuild), which an older release ignores"
++ }
++ },
++ "contracts": {
++ "range": ">=0.13.0 <1.0.0"
++ },
++ "leases": {
++ "claims": [
++ {
++ "what": "a scheduled publication job (wiki_schedule_jobs)",
++ "holder": "lease_owner = WIKI_WORKER_ID, default wiki-<pid>",
++ "expires": "lease_until = claim time + 60 s (scheduleLeaseMs default); taken again only after it passed",
++ "fencing": "complete, retry and fail only count WHERE lease_owner is the caller, so a worker whose lease was taken over cannot overwrite the new owner's result"
++ }
++ ]
++ }
++ },
"notes": "Wiki spaces (official and user), page trees with canonical slugs and history-aware redirects, immutable revisions with diff and revert, typed infoboxes, [[internal links]] with red links, citations per revision (Sources items or URLs with retrieval time), Media attachments with an explicit broken state, Community discussion references, watchers notified through wiki.watch.triggered, AI proposals held for a person's approval. Server-rendered public routes useful without JavaScript; sitemaps, Atom/JSON feeds and Search index events only for public, published, indexable pages (openvibe-publishing v0.2.0). Deployed and public at https://openvibe.wiki."
}
ddiff --git a/package-lock.json b/package-lock.json
iindex 062396ad..b0302796 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -1,12 +1,12 @@
{
"name": "openvibe-contracts",
-- "version": "0.54.0",
++ "version": "0.55.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvibe-contracts",
-- "version": "0.54.0",
++ "version": "0.55.0",
"license": "AGPL-3.0-only",
"dependencies": {
"ajv": "^8.17.1",
ddiff --git a/package.json b/package.json
iindex 122588f6..3451d9e0 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-contracts",
-- "version": "0.54.0",
++ "version": "0.55.0",
"description": "Machine-readable contracts for the OpenVibe network: schemas, manifests, ids, error envelope, capability checks.",
"main": "index.js",
"types": "generated/typescript/index.d.ts",
ddiff --git a/test/run.js b/test/run.js
iindex 6fd68a5c..bf78eca3 100644
---- a/test/run.js
++++ b/test/run.js
@@@ -568,6 +568,53 @@ await assert.rejects(failing.getToken(), /401: invalid_client/);
ok(ms.filter(m => m.site).length >= 10, 'the network\'s sites are in the manifests');
}
++// ── Lifecycle declarations (WS-P task 1): liveness, shutdown, recovery, rollback, contracts, leases ──
++// Every manifest declares all six; a part that does not apply is { none: reason }. A service that runs
++// (it has an internalOrigin) declares a real liveness endpoint (its `health`) and a real shutdown; one
++// that runs nothing (library, repository, retired) declares none for both. contracts.range is the
++// contractRanges entry, so the two cannot drift.
++{
++ const PARTS = ['liveness', 'shutdown', 'startupRecovery', 'rollback', 'contracts', 'leases'];
++ const isNone = (v) => !!v && typeof v.none === 'string' && Object.keys(v).length === 1;
++ for (const m of services.manifests) {
++ const lc = m.lifecycle;
++ ok(lc && PARTS.every(k => lc[k]), `${m.id} declares its lifecycle (${PARTS.join(', ')})`);
++ const runs = typeof m.internalOrigin === 'string';
++ if (runs) {
++ ok(!isNone(lc.liveness) && lc.liveness.endpoint === m.health, `${m.id} liveness endpoint is its health path (${m.health})`);
++ ok(!isNone(lc.shutdown) && typeof lc.shutdown.deadlineSeconds === 'number' && lc.shutdown.drains.length > 0, `${m.id} runs: it declares its shutdown signal, deadline and drains`);
++ ok(!isNone(lc.startupRecovery) && !isNone(lc.rollback), `${m.id} runs: it declares what it resumes at boot and how it is rolled back`);
++ }
++ if (['library', 'repository', 'retired'].includes(m.exposure.state)) ok(isNone(lc.liveness) && isNone(lc.shutdown) && isNone(lc.startupRecovery), `${m.id} runs nothing: liveness, shutdown and recovery are none`);
++ const range = m.contractRanges && m.contractRanges['openvibe-contracts'];
++ if (range) ok(lc.contracts.range === range, `${m.id} lifecycle.contracts.range is its contractRanges entry (${range})`);
++ else ok(isNone(lc.contracts), `${m.id} has no contractRanges entry, so its lifecycle.contracts is none`);
++ if (!isNone(lc.shutdown) && lc.shutdown.workers) ok(lc.shutdown.workers.deadlineSeconds >= lc.shutdown.deadlineSeconds, `${m.id} workers drain at least as long as the main process`);
++ if (!isNone(lc.leases)) for (const c of lc.leases.claims) ok(c.fencing.length >= 10, `${m.id} says how a stale holder of "${c.what}" is fenced`);
++ }
++ // Each lifecycle field carries a short description (walked through oneOf branches).
++ const described = [];
++ (function walk(node, at) {
++ if (!node || typeof node !== 'object') return;
++ for (const [k, v] of Object.entries(node.properties || {})) {
++ described.push([`${at}.${k}`, typeof v.description === 'string' && v.description.length > 0]);
++ walk(v, `${at}.${k}`);
++ for (const b of v.oneOf || []) walk(b, `${at}.${k}`);
++ if (v.items) walk(v.items, `${at}.${k}[]`);
++ }
++ })(contracts.schema('registry.service-manifest').properties.lifecycle, 'lifecycle');
++ ok(described.length >= 25, `lifecycle schema fields walked (${described.length})`);
++ for (const [at, has] of described) ok(has, `${at} has a description`);
++ const V = (lifecycle) => contracts.validate('registry.service-manifest@1', { ...services.get('media'), lifecycle }).valid;
++ const media = services.get('media').lifecycle;
++ ok(V(media), 'the media lifecycle validates on its own manifest');
++ ok(!V({ ...media, shutdown: { ...media.shutdown, deadlineSeconds: -1 } }), 'a negative deadline fails');
++ ok(!V({ ...media, shutdown: { ...media.shutdown, signal: 'SIGKILL' } }), 'SIGKILL is not a stop signal a process can handle');
++ ok(!V({ ...media, rollback: { ...media.rollback, blockers: [] } }), 'blockers is a non-empty list or { none }');
++ ok(!V({ ...media, contracts: { range: 'latest' } }), 'contracts.range is a version range');
++ ok(!V({ ...media, liveness: { none: 'x', endpoint: '/healthz' } }), 'none stands alone');
++}
++
// ── User-module namespaces ────────────────────────────────────────────────
{
const { modules } = contracts;