diff --git a/CHANGELOG.md b/CHANGELOG.md index ee5159f..3e5a502 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to the `littlebigbrain` Python SDK are documented here. +## 0.9.0 + +Durable, asynchronous NDJSON imports. + +- Sync and async clients add `submit_import_ndjson`, `get_import_job`, + `cancel_import_job`, and `wait_for_import_job`. +- Submissions consume iterable/async-iterable input as a streaming HTTP body, + require an explicit idempotency key, and never fall back to the synchronous + import route. +- Durable methods fail clearly unless the server advertises + `durable_import_jobs_v1`. + ## 0.8.1 Adjacency-backed Explorer reads now report the coherent adjacency coverage diff --git a/README.md b/README.md index 262fd11..2139d16 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,21 @@ lbb.graph("main").facts.import_ndjson( ) ``` +For large or long-running loads, submit a streamed durable job: + +```python +accepted = lbb.submit_import_ndjson( + records(), + idempotency_key="hubspot:portal-42:run-2026-07-29", +) +completed = lbb.wait_for_import_job(accepted.job_id) +print(completed.state, completed.committed_commit_seq) +``` + +The async client accepts an async iterable as well. Success means all grouped +commits are durable and final publication was enqueued; it does not mean +published indexes have already reached `committed_commit_seq`. + **Time-travel read.** Pin a SPARQL query to a past instant — results reflect the graph as it was then: ```python diff --git a/contracts/openapi.json b/contracts/openapi.json index 00ff525..0241150 100644 --- a/contracts/openapi.json +++ b/contracts/openapi.json @@ -4364,6 +4364,214 @@ "GraphId": { "type": "string" }, + "GraphImportJobAccepted": { + "description": "Stable response returned when a durable import is accepted or replayed.", + "properties": { + "idempotent_replay": { + "description": "True when this request resolved to an existing job with the same\nidempotency key and exact content hash.", + "type": "boolean" + }, + "job_id": { + "type": "string" + }, + "state": { + "$ref": "#/components/schemas/GraphImportJobState" + }, + "upload_bytes": { + "format": "int64", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "job_id", + "state", + "idempotent_replay", + "upload_bytes" + ], + "type": "object" + }, + "GraphImportJobCancelResponse": { + "description": "Response to `DELETE /v1/graph/import-jobs`.", + "properties": { + "job_id": { + "type": "string" + }, + "state": { + "$ref": "#/components/schemas/GraphImportJobState" + } + }, + "required": [ + "job_id", + "state" + ], + "type": "object" + }, + "GraphImportJobFailure": { + "description": "Terminal failure details for a durable import. The message is safe to expose\nto the submitting client; internal object keys and worker identities stay in\nstructured server logs.", + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + } + }, + "required": [ + "code", + "message", + "retryable" + ], + "type": "object" + }, + "GraphImportJobProgress": { + "description": "Bounded progress persisted by an import worker after every grouped commit.", + "properties": { + "bytes_processed": { + "format": "int64", + "minimum": 0, + "type": "integer" + }, + "committed_commit_seq": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/CommitSeq" + } + ] + }, + "error_count": { + "format": "int64", + "minimum": 0, + "type": "integer" + }, + "errors": { + "items": { + "$ref": "#/components/schemas/GraphImportLineError" + }, + "type": "array" + }, + "groups_committed": { + "format": "int64", + "minimum": 0, + "type": "integer" + }, + "lines_read": { + "format": "int64", + "minimum": 0, + "type": "integer" + }, + "observations": { + "format": "int64", + "minimum": 0, + "type": "integer" + }, + "properties": { + "format": "int64", + "minimum": 0, + "type": "integer" + }, + "triplets": { + "format": "int64", + "minimum": 0, + "type": "integer" + }, + "upload_bytes": { + "format": "int64", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "upload_bytes", + "bytes_processed", + "lines_read", + "triplets", + "properties", + "observations", + "groups_committed", + "error_count" + ], + "type": "object" + }, + "GraphImportJobState": { + "description": "Lifecycle of a durable NDJSON import submitted through\n`POST /v1/graph/import-jobs`.\n\nA succeeded import has durably committed its records and enqueued the final\npublication job. It does **not** mean the published read generation has\nalready caught up to [`GraphImportJobStatus::committed_commit_seq`].", + "enum": [ + "queued", + "running", + "cancellation_requested", + "cancelled", + "succeeded", + "failed" + ], + "type": "string" + }, + "GraphImportJobStatus": { + "description": "Queryable status for a durable import job.", + "properties": { + "committed_commit_seq": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/CommitSeq" + } + ] + }, + "enqueued_at_micros": { + "format": "int64", + "type": "integer" + }, + "failure": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/GraphImportJobFailure" + } + ] + }, + "job_id": { + "type": "string" + }, + "progress": { + "$ref": "#/components/schemas/GraphImportJobProgress" + }, + "publication_job": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/GraphImportPublishedGenerationOutcome", + "description": "Final publication enqueue receipt. Presence means publication was\naccepted, not that published reads have caught up." + } + ] + }, + "state": { + "$ref": "#/components/schemas/GraphImportJobState" + }, + "updated_at_micros": { + "format": "int64", + "type": "integer" + } + }, + "required": [ + "job_id", + "state", + "progress", + "enqueued_at_micros", + "updated_at_micros" + ], + "type": "object" + }, "GraphImportLine": { "description": "One line of an NDJSON bulk-import stream (`POST /v1/graph/import`). Each line\nis a single JSON object that is either a triplet (carries `relation`) or an\nentity-properties record (carries `properties`); the two shapes are disjoint,\nso the importer routes each line by shape. This is the streamable counterpart\nof `TripletCommitFile`: a client streams a large dataset line-by-line instead\nof buffering one giant commit, and the server batches lines into bounded\ninternal commits.", "oneOf": [ @@ -15966,6 +16174,42 @@ ], "type": "string" }, + "VersionResponse": { + "description": "Unauthenticated build, wire-capability, and persisted-format identity.", + "properties": { + "built_at": { + "description": "UTC build timestamp baked into the running binary.", + "type": "string" + }, + "capabilities": { + "description": "Fine-grained additive features safe for SDK capability gates.", + "items": { + "type": "string" + }, + "type": "array" + }, + "git_commit": { + "description": "Source revision baked into the running binary.", + "type": "string" + }, + "persisted_format": { + "description": "Deterministic fingerprint of all persisted format versions.", + "type": "string" + }, + "version": { + "description": "Backward-compatibility date sent in `Lbb-Version`.", + "type": "string" + } + }, + "required": [ + "version", + "capabilities", + "git_commit", + "built_at", + "persisted_format" + ], + "type": "object" + }, "VocabExportResponse": { "description": "`GET /v1/search/vocab` — the graph's grounding vocabulary as sorted,\ndeduped string sections: the canonical input for a decoder-side automaton\n(an FST/trie builder consumes byte-sorted streams directly), and the\nvocabulary half of an \"intelligence to go\" export bundle. Derived per\nsnapshot from the same sources the suggest surface reads — no doc/postings\nblock reads.", "properties": { @@ -16917,15 +17161,843 @@ "bearer": [] } ], - "summary": "List built-in demo datasets", + "summary": "List built-in demo datasets", + "tags": [ + "demo" + ] + } + }, + "/v1/demo/download-public-datasets": { + "post": { + "operationId": "post_v1_demo_download_public_datasets", + "parameters": [ + { + "description": "Graph name (default `main`)", + "in": "query", + "name": "graph", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Branch name (default `main`)", + "in": "query", + "name": "branch", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "API contract version to pin. Use `2026-07-23` for this beta-breaking shape.", + "in": "header", + "name": "Lbb-Version", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Stable client-generated key for safely retrying mutations and supervision writes.", + "in": "header", + "name": "Idempotency-Key", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Public dataset id", + "in": "query", + "name": "dataset", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + }, + "description": "OK", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LbbErrorEnvelope" + } + } + }, + "description": "Bad request", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LbbErrorEnvelope" + } + } + }, + "description": "Unauthorized", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LbbErrorEnvelope" + } + } + }, + "description": "Forbidden", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LbbErrorEnvelope" + } + } + }, + "description": "Not found", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LbbErrorEnvelope" + } + } + }, + "description": "Conflict", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LbbErrorEnvelope" + } + } + }, + "description": "Rate limit exceeded", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LbbErrorEnvelope" + } + } + }, + "description": "Internal server error", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LbbErrorEnvelope" + } + } + }, + "description": "Service unavailable", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + } + }, + "security": [ + { + "bearer": [] + } + ], + "summary": "Download one public benchmark dataset sample into the local dataset cache", + "tags": [ + "demo" + ] + } + }, + "/v1/demo/load": { + "post": { + "operationId": "post_v1_demo_load", + "parameters": [ + { + "description": "Graph name (default `main`)", + "in": "query", + "name": "graph", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Branch name (default `main`)", + "in": "query", + "name": "branch", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "API contract version to pin. Use `2026-07-23` for this beta-breaking shape.", + "in": "header", + "name": "Lbb-Version", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Stable client-generated key for safely retrying mutations and supervision writes.", + "in": "header", + "name": "Idempotency-Key", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Dataset id", + "in": "query", + "name": "dataset", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Build indexes after loading", + "in": "query", + "name": "build", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + }, + "description": "OK", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LbbErrorEnvelope" + } + } + }, + "description": "Bad request", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LbbErrorEnvelope" + } + } + }, + "description": "Unauthorized", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LbbErrorEnvelope" + } + } + }, + "description": "Forbidden", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LbbErrorEnvelope" + } + } + }, + "description": "Not found", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LbbErrorEnvelope" + } + } + }, + "description": "Conflict", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LbbErrorEnvelope" + } + } + }, + "description": "Rate limit exceeded", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LbbErrorEnvelope" + } + } + }, + "description": "Internal server error", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LbbErrorEnvelope" + } + } + }, + "description": "Service unavailable", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + } + }, + "security": [ + { + "bearer": [] + } + ], + "summary": "Load a demo dataset, optionally building indexes", + "tags": [ + "demo" + ] + } + }, + "/v1/demo/public-datasets": { + "get": { + "operationId": "get_v1_demo_public_datasets", + "parameters": [ + { + "description": "Graph name (default `main`)", + "in": "query", + "name": "graph", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Branch name (default `main`)", + "in": "query", + "name": "branch", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "API contract version to pin. Use `2026-07-23` for this beta-breaking shape.", + "in": "header", + "name": "Lbb-Version", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + }, + "description": "OK", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LbbErrorEnvelope" + } + } + }, + "description": "Bad request", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LbbErrorEnvelope" + } + } + }, + "description": "Unauthorized", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LbbErrorEnvelope" + } + } + }, + "description": "Forbidden", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LbbErrorEnvelope" + } + } + }, + "description": "Not found", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LbbErrorEnvelope" + } + } + }, + "description": "Conflict", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LbbErrorEnvelope" + } + } + }, + "description": "Rate limit exceeded", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LbbErrorEnvelope" + } + } + }, + "description": "Internal server error", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LbbErrorEnvelope" + } + } + }, + "description": "Service unavailable", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + } + }, + "security": [ + { + "bearer": [] + } + ], + "summary": "List public benchmark dataset availability", "tags": [ "demo" ] } }, - "/v1/demo/download-public-datasets": { + "/v1/demo/run": { "post": { - "operationId": "post_v1_demo_download_public_datasets", + "operationId": "post_v1_demo_run", "parameters": [ { "description": "Graph name (default `main`)", @@ -16964,7 +18036,7 @@ } }, { - "description": "Public dataset id", + "description": "Dataset id", "in": "query", "name": "dataset", "required": false, @@ -17196,15 +18268,15 @@ "bearer": [] } ], - "summary": "Download one public benchmark dataset sample into the local dataset cache", + "summary": "Run canned checks against a demo dataset", "tags": [ "demo" ] } }, - "/v1/demo/load": { - "post": { - "operationId": "post_v1_demo_load", + "/v1/graph/branch": { + "delete": { + "operationId": "delete_v1_graph_branch", "parameters": [ { "description": "Graph name (default `main`)", @@ -17243,279 +18315,9 @@ } }, { - "description": "Dataset id", - "in": "query", - "name": "dataset", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "Build indexes after loading", - "in": "query", - "name": "build", - "required": false, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - }, - "description": "OK", - "headers": { - "Lbb-Version": { - "description": "API contract version used for the response", - "schema": { - "type": "string" - } - }, - "X-Request-Id": { - "description": "Request correlation id", - "schema": { - "type": "string" - } - } - } - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LbbErrorEnvelope" - } - } - }, - "description": "Bad request", - "headers": { - "Lbb-Version": { - "description": "API contract version used for the response", - "schema": { - "type": "string" - } - }, - "X-Request-Id": { - "description": "Request correlation id", - "schema": { - "type": "string" - } - } - } - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LbbErrorEnvelope" - } - } - }, - "description": "Unauthorized", - "headers": { - "Lbb-Version": { - "description": "API contract version used for the response", - "schema": { - "type": "string" - } - }, - "X-Request-Id": { - "description": "Request correlation id", - "schema": { - "type": "string" - } - } - } - }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LbbErrorEnvelope" - } - } - }, - "description": "Forbidden", - "headers": { - "Lbb-Version": { - "description": "API contract version used for the response", - "schema": { - "type": "string" - } - }, - "X-Request-Id": { - "description": "Request correlation id", - "schema": { - "type": "string" - } - } - } - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LbbErrorEnvelope" - } - } - }, - "description": "Not found", - "headers": { - "Lbb-Version": { - "description": "API contract version used for the response", - "schema": { - "type": "string" - } - }, - "X-Request-Id": { - "description": "Request correlation id", - "schema": { - "type": "string" - } - } - } - }, - "409": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LbbErrorEnvelope" - } - } - }, - "description": "Conflict", - "headers": { - "Lbb-Version": { - "description": "API contract version used for the response", - "schema": { - "type": "string" - } - }, - "X-Request-Id": { - "description": "Request correlation id", - "schema": { - "type": "string" - } - } - } - }, - "429": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LbbErrorEnvelope" - } - } - }, - "description": "Rate limit exceeded", - "headers": { - "Lbb-Version": { - "description": "API contract version used for the response", - "schema": { - "type": "string" - } - }, - "X-Request-Id": { - "description": "Request correlation id", - "schema": { - "type": "string" - } - } - } - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LbbErrorEnvelope" - } - } - }, - "description": "Internal server error", - "headers": { - "Lbb-Version": { - "description": "API contract version used for the response", - "schema": { - "type": "string" - } - }, - "X-Request-Id": { - "description": "Request correlation id", - "schema": { - "type": "string" - } - } - } - }, - "503": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LbbErrorEnvelope" - } - } - }, - "description": "Service unavailable", - "headers": { - "Lbb-Version": { - "description": "API contract version used for the response", - "schema": { - "type": "string" - } - }, - "X-Request-Id": { - "description": "Request correlation id", - "schema": { - "type": "string" - } - } - } - } - }, - "security": [ - { - "bearer": [] - } - ], - "summary": "Load a demo dataset, optionally building indexes", - "tags": [ - "demo" - ] - } - }, - "/v1/demo/public-datasets": { - "get": { - "operationId": "get_v1_demo_public_datasets", - "parameters": [ - { - "description": "Graph name (default `main`)", - "in": "query", - "name": "graph", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "Branch name (default `main`)", + "description": "Must equal the target branch id", "in": "query", - "name": "branch", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "API contract version to pin. Use `2026-07-23` for this beta-breaking shape.", - "in": "header", - "name": "Lbb-Version", + "name": "confirm", "required": false, "schema": { "type": "string" @@ -17527,7 +18329,7 @@ "content": { "application/json": { "schema": { - "type": "object" + "$ref": "#/components/schemas/GraphBranchDeleteResponse" } } }, @@ -17745,15 +18547,13 @@ "bearer": [] } ], - "summary": "List public benchmark dataset availability", + "summary": "Delete the scoped branch; refuses to delete a graph's final live branch", "tags": [ - "demo" + "graph" ] - } - }, - "/v1/demo/run": { + }, "post": { - "operationId": "post_v1_demo_run", + "operationId": "post_v1_graph_branch", "parameters": [ { "description": "Graph name (default `main`)", @@ -17790,23 +18590,24 @@ "schema": { "type": "string" } - }, - { - "description": "Dataset id", - "in": "query", - "name": "dataset", - "required": false, - "schema": { - "type": "string" - } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GraphBranchCreateRequest" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json": { "schema": { - "type": "object" + "$ref": "#/components/schemas/GraphBranchCreateResponse" } } }, @@ -18024,15 +18825,15 @@ "bearer": [] } ], - "summary": "Run canned checks against a demo dataset", + "summary": "Fork the scoped branch from an existing branch in the same graph", "tags": [ - "demo" + "graph" ] } }, - "/v1/graph/branch": { - "delete": { - "operationId": "delete_v1_graph_branch", + "/v1/graph/branch/merge": { + "post": { + "operationId": "post_v1_graph_branch_merge", "parameters": [ { "description": "Graph name (default `main`)", @@ -18069,11 +18870,273 @@ "schema": { "type": "string" } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GraphBranchMergeRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GraphBranchMergeResponse" + } + } + }, + "description": "OK", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LbbErrorEnvelope" + } + } + }, + "description": "Bad request", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LbbErrorEnvelope" + } + } + }, + "description": "Unauthorized", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LbbErrorEnvelope" + } + } + }, + "description": "Forbidden", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LbbErrorEnvelope" + } + } + }, + "description": "Not found", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LbbErrorEnvelope" + } + } + }, + "description": "Conflict", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LbbErrorEnvelope" + } + } + }, + "description": "Rate limit exceeded", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LbbErrorEnvelope" + } + } + }, + "description": "Internal server error", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LbbErrorEnvelope" + } + } + }, + "description": "Service unavailable", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + } + }, + "security": [ { - "description": "Must equal the target branch id", + "bearer": [] + } + ], + "summary": "Replay a child branch's post-fork commits onto the scoped branch (its fork parent) as one commit — validate-then-merge; requires an Idempotency-Key", + "tags": [ + "graph" + ] + } + }, + "/v1/graph/card": { + "get": { + "operationId": "get_v1_graph_card", + "parameters": [ + { + "description": "Graph name (default `main`)", "in": "query", - "name": "confirm", + "name": "graph", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Branch name (default `main`)", + "in": "query", + "name": "branch", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "API contract version to pin. Use `2026-07-23` for this beta-breaking shape.", + "in": "header", + "name": "Lbb-Version", "required": false, "schema": { "type": "string" @@ -18085,7 +19148,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GraphBranchDeleteResponse" + "$ref": "#/components/schemas/GraphCard" } } }, @@ -18303,13 +19366,15 @@ "bearer": [] } ], - "summary": "Delete the scoped branch; refuses to delete a graph's final live branch", + "summary": "Per-snapshot self-description (types, relations, properties, top values)", "tags": [ "graph" ] - }, - "post": { - "operationId": "post_v1_graph_branch", + } + }, + "/v1/graph/changes": { + "get": { + "operationId": "get_v1_graph_changes", "parameters": [ { "description": "Graph name (default `main`)", @@ -18339,31 +19404,30 @@ } }, { - "description": "Stable client-generated key for safely retrying mutations and supervision writes.", - "in": "header", - "name": "Idempotency-Key", + "description": "Exclusive lower bound commit_seq (default 0)", + "in": "query", + "name": "since", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Max records per page (default 500, max 5000)", + "in": "query", + "name": "limit", "required": false, "schema": { "type": "string" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GraphBranchCreateRequest" - } - } - }, - "required": true - }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GraphBranchCreateResponse" + "$ref": "#/components/schemas/GraphChangesResponse" } } }, @@ -18581,15 +19645,15 @@ "bearer": [] } ], - "summary": "Fork the scoped branch from an existing branch in the same graph", + "summary": "Changes since a commit_seq (delta read); 409 with reset when since predates compaction", "tags": [ "graph" ] } }, - "/v1/graph/branch/merge": { + "/v1/graph/commit": { "post": { - "operationId": "post_v1_graph_branch_merge", + "operationId": "post_v1_graph_commit", "parameters": [ { "description": "Graph name (default `main`)", @@ -18622,6 +19686,15 @@ "description": "Stable client-generated key for safely retrying mutations and supervision writes.", "in": "header", "name": "Idempotency-Key", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Validate only and return a GraphCommitDryRunResponse without writing (no Idempotency-Key required)", + "in": "query", + "name": "dry_run", "required": false, "schema": { "type": "string" @@ -18632,7 +19705,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GraphBranchMergeRequest" + "$ref": "#/components/schemas/TripletCommitFile" } } }, @@ -18643,7 +19716,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GraphBranchMergeResponse" + "$ref": "#/components/schemas/GraphCommitResponse" } } }, @@ -18861,15 +19934,15 @@ "bearer": [] } ], - "summary": "Replay a child branch's post-fork commits onto the scoped branch (its fork parent) as one commit — validate-then-merge; requires an Idempotency-Key", + "summary": "Commit triplets and optional entity embeddings", "tags": [ "graph" ] } }, - "/v1/graph/card": { - "get": { - "operationId": "get_v1_graph_card", + "/v1/graph/compact": { + "post": { + "operationId": "post_v1_graph_compact", "parameters": [ { "description": "Graph name (default `main`)", @@ -18897,6 +19970,33 @@ "schema": { "type": "string" } + }, + { + "description": "Stable client-generated key for safely retrying mutations and supervision writes.", + "in": "header", + "name": "Idempotency-Key", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Minimum tail commits before compaction", + "in": "query", + "name": "min_tail_commits", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Merge into at most this many segments", + "in": "query", + "name": "max_segments", + "required": false, + "schema": { + "type": "string" + } } ], "responses": { @@ -18904,7 +20004,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GraphCard" + "$ref": "#/components/schemas/WalCompactResponse" } } }, @@ -19122,15 +20222,15 @@ "bearer": [] } ], - "summary": "Per-snapshot self-description (types, relations, properties, top values)", + "summary": "Fold the WAL tail into snapshot segments", "tags": [ "graph" ] } }, - "/v1/graph/changes": { - "get": { - "operationId": "get_v1_graph_changes", + "/v1/graph/create": { + "post": { + "operationId": "post_v1_graph_create", "parameters": [ { "description": "Graph name (default `main`)", @@ -19160,18 +20260,9 @@ } }, { - "description": "Exclusive lower bound commit_seq (default 0)", - "in": "query", - "name": "since", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "Max records per page (default 500, max 5000)", - "in": "query", - "name": "limit", + "description": "Stable client-generated key for safely retrying mutations and supervision writes.", + "in": "header", + "name": "Idempotency-Key", "required": false, "schema": { "type": "string" @@ -19183,7 +20274,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GraphChangesResponse" + "$ref": "#/components/schemas/CreateGraphResponse" } } }, @@ -19401,15 +20492,15 @@ "bearer": [] } ], - "summary": "Changes since a commit_seq (delta read); 409 with reset when since predates compaction", + "summary": "Create the scoped graph and branch", "tags": [ "graph" ] } }, - "/v1/graph/commit": { + "/v1/graph/delete": { "post": { - "operationId": "post_v1_graph_commit", + "operationId": "post_v1_graph_delete", "parameters": [ { "description": "Graph name (default `main`)", @@ -19442,37 +20533,27 @@ "description": "Stable client-generated key for safely retrying mutations and supervision writes.", "in": "header", "name": "Idempotency-Key", - "required": true, + "required": false, "schema": { "type": "string" } }, { - "description": "Validate only and return a GraphCommitDryRunResponse without writing (no Idempotency-Key required)", + "description": "Must equal the target graph id to authorize deletion", "in": "query", - "name": "dry_run", + "name": "confirm", "required": false, "schema": { "type": "string" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TripletCommitFile" - } - } - }, - "required": true - }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GraphCommitResponse" + "$ref": "#/components/schemas/GraphDeleteResponse" } } }, @@ -19690,15 +20771,15 @@ "bearer": [] } ], - "summary": "Commit triplets and optional entity embeddings", + "summary": "Delete every branch and deregister the scoped graph; destructive and idempotent", "tags": [ "graph" ] } }, - "/v1/graph/compact": { - "post": { - "operationId": "post_v1_graph_compact", + "/v1/graph/embedding": { + "get": { + "operationId": "get_v1_graph_embedding", "parameters": [ { "description": "Graph name (default `main`)", @@ -19726,33 +20807,6 @@ "schema": { "type": "string" } - }, - { - "description": "Stable client-generated key for safely retrying mutations and supervision writes.", - "in": "header", - "name": "Idempotency-Key", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "Minimum tail commits before compaction", - "in": "query", - "name": "min_tail_commits", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "Merge into at most this many segments", - "in": "query", - "name": "max_segments", - "required": false, - "schema": { - "type": "string" - } } ], "responses": { @@ -19760,7 +20814,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WalCompactResponse" + "$ref": "#/components/schemas/ManagedEmbeddingConfigResponse" } } }, @@ -19978,15 +21032,13 @@ "bearer": [] } ], - "summary": "Fold the WAL tail into snapshot segments", + "summary": "Read the graph branch's default managed embedding configuration", "tags": [ "graph" ] - } - }, - "/v1/graph/create": { + }, "post": { - "operationId": "post_v1_graph_create", + "operationId": "post_v1_graph_embedding", "parameters": [ { "description": "Graph name (default `main`)", @@ -20025,12 +21077,22 @@ } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagedEmbeddingConfigRequest" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateGraphResponse" + "$ref": "#/components/schemas/ManagedEmbeddingConfigResponse" } } }, @@ -20248,15 +21310,15 @@ "bearer": [] } ], - "summary": "Create the scoped graph and branch", + "summary": "Set the graph branch's default managed embedding configuration", "tags": [ "graph" ] } }, - "/v1/graph/delete": { + "/v1/graph/embedding/backfill": { "post": { - "operationId": "post_v1_graph_delete", + "operationId": "post_v1_graph_embedding_backfill", "parameters": [ { "description": "Graph name (default `main`)", @@ -20295,21 +21357,49 @@ } }, { - "description": "Must equal the target graph id to authorize deletion", + "description": "Embedding batch size (default 128, max 512)", "in": "query", - "name": "confirm", + "name": "batch_size", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Maximum entities to consider (default 100000)", + "in": "query", + "name": "limit", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Re-embed all entities instead of only new entities", + "in": "query", + "name": "full", "required": false, "schema": { "type": "string" } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagedEmbeddingBackfillJobRequest" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GraphDeleteResponse" + "$ref": "#/components/schemas/ManagedEmbeddingBackfillJobStatusResponse" } } }, @@ -20527,15 +21617,15 @@ "bearer": [] } ], - "summary": "Delete every branch and deregister the scoped graph; destructive and idempotent", + "summary": "Submit a durable managed-embedding backfill job (legacy path alias)", "tags": [ "graph" ] } }, - "/v1/graph/embedding": { - "get": { - "operationId": "get_v1_graph_embedding", + "/v1/graph/embedding/backfill-jobs": { + "delete": { + "operationId": "delete_v1_graph_embedding_backfill_jobs", "parameters": [ { "description": "Graph name (default `main`)", @@ -20563,6 +21653,24 @@ "schema": { "type": "string" } + }, + { + "description": "Stable client-generated key for safely retrying mutations and supervision writes.", + "in": "header", + "name": "Idempotency-Key", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Stable backfill job id", + "in": "query", + "name": "job_id", + "required": false, + "schema": { + "type": "string" + } } ], "responses": { @@ -20570,7 +21678,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ManagedEmbeddingConfigResponse" + "$ref": "#/components/schemas/ManagedEmbeddingBackfillJobStatusResponse" } } }, @@ -20788,13 +21896,13 @@ "bearer": [] } ], - "summary": "Read the graph branch's default managed embedding configuration", + "summary": "Request cancellation of a managed-embedding backfill job", "tags": [ "graph" ] }, - "post": { - "operationId": "post_v1_graph_embedding", + "get": { + "operationId": "get_v1_graph_embedding_backfill_jobs", "parameters": [ { "description": "Graph name (default `main`)", @@ -20824,31 +21932,21 @@ } }, { - "description": "Stable client-generated key for safely retrying mutations and supervision writes.", - "in": "header", - "name": "Idempotency-Key", + "description": "Stable backfill job id", + "in": "query", + "name": "job_id", "required": false, "schema": { "type": "string" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ManagedEmbeddingConfigRequest" - } - } - }, - "required": true - }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ManagedEmbeddingConfigResponse" + "$ref": "#/components/schemas/ManagedEmbeddingBackfillJobStatusResponse" } } }, @@ -21066,15 +22164,13 @@ "bearer": [] } ], - "summary": "Set the graph branch's default managed embedding configuration", + "summary": "Read one managed-embedding backfill job", "tags": [ "graph" ] - } - }, - "/v1/graph/embedding/backfill": { + }, "post": { - "operationId": "post_v1_graph_embedding_backfill", + "operationId": "post_v1_graph_embedding_backfill_jobs", "parameters": [ { "description": "Graph name (default `main`)", @@ -21111,33 +22207,6 @@ "schema": { "type": "string" } - }, - { - "description": "Embedding batch size (default 128, max 512)", - "in": "query", - "name": "batch_size", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "Maximum entities to consider (default 100000)", - "in": "query", - "name": "limit", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "Re-embed all entities instead of only new entities", - "in": "query", - "name": "full", - "required": false, - "schema": { - "type": "string" - } } ], "requestBody": { @@ -21373,15 +22442,15 @@ "bearer": [] } ], - "summary": "Submit a durable managed-embedding backfill job (legacy path alias)", + "summary": "Submit a durable managed-embedding backfill job", "tags": [ "graph" ] } }, - "/v1/graph/embedding/backfill-jobs": { - "delete": { - "operationId": "delete_v1_graph_embedding_backfill_jobs", + "/v1/graph/embedding/models": { + "get": { + "operationId": "get_v1_graph_embedding_models", "parameters": [ { "description": "Graph name (default `main`)", @@ -21409,24 +22478,6 @@ "schema": { "type": "string" } - }, - { - "description": "Stable client-generated key for safely retrying mutations and supervision writes.", - "in": "header", - "name": "Idempotency-Key", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "Stable backfill job id", - "in": "query", - "name": "job_id", - "required": false, - "schema": { - "type": "string" - } } ], "responses": { @@ -21434,7 +22485,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ManagedEmbeddingBackfillJobStatusResponse" + "$ref": "#/components/schemas/ManagedEmbeddingModelsResponse" } } }, @@ -21652,13 +22703,15 @@ "bearer": [] } ], - "summary": "Request cancellation of a managed-embedding backfill job", + "summary": "List the embedding models available on this deployment", "tags": [ "graph" ] - }, - "get": { - "operationId": "get_v1_graph_embedding_backfill_jobs", + } + }, + "/v1/graph/embedding/promote": { + "post": { + "operationId": "post_v1_graph_embedding_promote", "parameters": [ { "description": "Graph name (default `main`)", @@ -21688,9 +22741,27 @@ } }, { - "description": "Stable backfill job id", + "description": "Stable client-generated key for safely retrying mutations and supervision writes.", + "in": "header", + "name": "Idempotency-Key", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Finished training run identifier", "in": "query", - "name": "job_id", + "name": "run_id", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Allow promotion when evaluation quality regressed", + "in": "query", + "name": "allow_regression", "required": false, "schema": { "type": "string" @@ -21702,7 +22773,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ManagedEmbeddingBackfillJobStatusResponse" + "$ref": "#/components/schemas/ManagedEmbeddingPromoteResponse" } } }, @@ -21920,13 +22991,15 @@ "bearer": [] } ], - "summary": "Read one managed-embedding backfill job", + "summary": "Promote a successful fine-tuned embedding run to the graph default", "tags": [ "graph" ] - }, - "post": { - "operationId": "post_v1_graph_embedding_backfill_jobs", + } + }, + "/v1/graph/entities/sample": { + "get": { + "operationId": "get_v1_graph_entities_sample", "parameters": [ { "description": "Graph name (default `main`)", @@ -21956,31 +23029,39 @@ } }, { - "description": "Stable client-generated key for safely retrying mutations and supervision writes.", - "in": "header", - "name": "Idempotency-Key", + "description": "Exact entity type", + "in": "query", + "name": "type", "required": false, "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ManagedEmbeddingBackfillJobRequest" - } + }, + { + "description": "Max sampled rows (up to 128)", + "in": "query", + "name": "limit", + "required": false, + "schema": { + "type": "string" } }, - "required": true - }, + { + "description": "Accepted as strong or eventual; adjacency serves the exact immutable base pinned by the published generation without inspecting the live WAL in either mode", + "in": "query", + "name": "consistency", + "required": false, + "schema": { + "type": "string" + } + } + ], "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ManagedEmbeddingBackfillJobStatusResponse" + "$ref": "#/components/schemas/EntityTypeSampleResponse" } } }, @@ -22198,15 +23279,15 @@ "bearer": [] } ], - "summary": "Submit a durable managed-embedding backfill job", + "summary": "Bounded class sample from the ranged adjacency index", "tags": [ "graph" ] } }, - "/v1/graph/embedding/models": { + "/v1/graph/entity": { "get": { - "operationId": "get_v1_graph_embedding_models", + "operationId": "get_v1_graph_entity", "parameters": [ { "description": "Graph name (default `main`)", @@ -22234,6 +23315,33 @@ "schema": { "type": "string" } + }, + { + "description": "Entity id", + "in": "query", + "name": "id", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Entity type for name-addressed lookup", + "in": "query", + "name": "type", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Entity canonical name for name-addressed lookup", + "in": "query", + "name": "name", + "required": false, + "schema": { + "type": "string" + } } ], "responses": { @@ -22241,7 +23349,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ManagedEmbeddingModelsResponse" + "$ref": "#/components/schemas/EntityDetailResponse" } } }, @@ -22459,15 +23567,15 @@ "bearer": [] } ], - "summary": "List the embedding models available on this deployment", + "summary": "Entity detail", "tags": [ "graph" ] } }, - "/v1/graph/embedding/promote": { - "post": { - "operationId": "post_v1_graph_embedding_promote", + "/v1/graph/entity/metadata": { + "get": { + "operationId": "get_v1_graph_entity_metadata", "parameters": [ { "description": "Graph name (default `main`)", @@ -22497,27 +23605,36 @@ } }, { - "description": "Stable client-generated key for safely retrying mutations and supervision writes.", - "in": "header", - "name": "Idempotency-Key", + "description": "Entity id", + "in": "query", + "name": "id", "required": false, "schema": { "type": "string" } }, { - "description": "Finished training run identifier", + "description": "Entity type for name-addressed lookup", "in": "query", - "name": "run_id", + "name": "type", "required": false, "schema": { "type": "string" } }, { - "description": "Allow promotion when evaluation quality regressed", + "description": "Entity canonical name for name-addressed lookup", "in": "query", - "name": "allow_regression", + "name": "name", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "RFC3339 as-of timestamp", + "in": "query", + "name": "as_of", "required": false, "schema": { "type": "string" @@ -22529,7 +23646,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ManagedEmbeddingPromoteResponse" + "$ref": "#/components/schemas/EntityMetadataResponse" } } }, @@ -22747,15 +23864,15 @@ "bearer": [] } ], - "summary": "Promote a successful fine-tuned embedding run to the graph default", + "summary": "Entity metadata, stored attributes, and immutable object reference status", "tags": [ "graph" ] } }, - "/v1/graph/entities/sample": { + "/v1/graph/entity/neighborhood": { "get": { - "operationId": "get_v1_graph_entities_sample", + "operationId": "get_v1_graph_entity_neighborhood", "parameters": [ { "description": "Graph name (default `main`)", @@ -22785,7 +23902,16 @@ } }, { - "description": "Exact entity type", + "description": "Entity id", + "in": "query", + "name": "id", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Entity type for name-addressed lookup", "in": "query", "name": "type", "required": false, @@ -22794,18 +23920,27 @@ } }, { - "description": "Max sampled rows (up to 128)", + "description": "Entity canonical name for name-addressed lookup", "in": "query", - "name": "limit", + "name": "name", "required": false, "schema": { "type": "string" } }, { - "description": "Accepted as strong or eventual; adjacency serves the exact immutable base pinned by the published generation without inspecting the live WAL in either mode", + "description": "Comma-separated relation filter", "in": "query", - "name": "consistency", + "name": "relations", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "RFC3339 as-of timestamp", + "in": "query", + "name": "as_of", "required": false, "schema": { "type": "string" @@ -22817,7 +23952,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EntityTypeSampleResponse" + "$ref": "#/components/schemas/EntityNeighborhoodResponse" } } }, @@ -23035,15 +24170,15 @@ "bearer": [] } ], - "summary": "Bounded class sample from the ranged adjacency index", + "summary": "Entity point lookup with its out/in neighborhood", "tags": [ "graph" ] } }, - "/v1/graph/entity": { + "/v1/graph/export/job": { "get": { - "operationId": "get_v1_graph_entity", + "operationId": "get_v1_graph_export_job", "parameters": [ { "description": "Graph name (default `main`)", @@ -23073,27 +24208,9 @@ } }, { - "description": "Entity id", - "in": "query", - "name": "id", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "Entity type for name-addressed lookup", - "in": "query", - "name": "type", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "Entity canonical name for name-addressed lookup", + "description": "The export job id returned by the enqueue call", "in": "query", - "name": "name", + "name": "job_id", "required": false, "schema": { "type": "string" @@ -23105,7 +24222,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EntityDetailResponse" + "type": "object" } } }, @@ -23323,15 +24440,13 @@ "bearer": [] } ], - "summary": "Entity detail", + "summary": "Poll an enqueued background export job by ?job_id= (scoped to the requesting tenant)", "tags": [ "graph" ] - } - }, - "/v1/graph/entity/metadata": { - "get": { - "operationId": "get_v1_graph_entity_metadata", + }, + "post": { + "operationId": "post_v1_graph_export_job", "parameters": [ { "description": "Graph name (default `main`)", @@ -23361,36 +24476,18 @@ } }, { - "description": "Entity id", - "in": "query", - "name": "id", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "Entity type for name-addressed lookup", - "in": "query", - "name": "type", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "Entity canonical name for name-addressed lookup", - "in": "query", - "name": "name", + "description": "Stable client-generated key for safely retrying mutations and supervision writes.", + "in": "header", + "name": "Idempotency-Key", "required": false, "schema": { "type": "string" } }, { - "description": "RFC3339 as-of timestamp", + "description": "Pin the export to a past snapshot commit_seq", "in": "query", - "name": "as_of", + "name": "as_of_commit_seq", "required": false, "schema": { "type": "string" @@ -23402,7 +24499,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EntityMetadataResponse" + "type": "object" } } }, @@ -23620,15 +24717,15 @@ "bearer": [] } ], - "summary": "Entity metadata, stored attributes, and immutable object reference status", + "summary": "Enqueue a background full-fidelity export for a large tenant; writes zstd NDJSON parts + a manifest under tenants//exports//. Returns the job id; poll with GET /v1/graph/export/job?job_id=", "tags": [ "graph" ] } }, - "/v1/graph/entity/neighborhood": { - "get": { - "operationId": "get_v1_graph_entity_neighborhood", + "/v1/graph/fork": { + "post": { + "operationId": "post_v1_graph_fork", "parameters": [ { "description": "Graph name (default `main`)", @@ -23658,45 +24755,36 @@ } }, { - "description": "Entity id", - "in": "query", - "name": "id", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "Entity type for name-addressed lookup", - "in": "query", - "name": "type", + "description": "Stable client-generated key for safely retrying mutations and supervision writes.", + "in": "header", + "name": "Idempotency-Key", "required": false, "schema": { "type": "string" } }, { - "description": "Entity canonical name for name-addressed lookup", + "description": "Source graph id to fork from", "in": "query", - "name": "name", + "name": "src", "required": false, "schema": { "type": "string" } }, { - "description": "Comma-separated relation filter", + "description": "Destination graph id to create (must not exist)", "in": "query", - "name": "relations", + "name": "dst", "required": false, "schema": { "type": "string" } }, { - "description": "RFC3339 as-of timestamp", + "description": "Must equal the destination graph id to authorize the fork", "in": "query", - "name": "as_of", + "name": "confirm", "required": false, "schema": { "type": "string" @@ -23708,7 +24796,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EntityNeighborhoodResponse" + "$ref": "#/components/schemas/GraphForkResponse" } } }, @@ -23926,15 +25014,15 @@ "bearer": [] } ], - "summary": "Entity point lookup with its out/in neighborhood", + "summary": "Fork a graph: copy-based clone of the source graph's current head into a new graph in the same tenant — no data replay, no re-embedding; runs as a durable job (poll the destination's metadata)", "tags": [ "graph" ] } }, - "/v1/graph/export/job": { + "/v1/graph/groundability": { "get": { - "operationId": "get_v1_graph_export_job", + "operationId": "get_v1_graph_groundability", "parameters": [ { "description": "Graph name (default `main`)", @@ -23964,9 +25052,9 @@ } }, { - "description": "The export job id returned by the enqueue call", + "description": "Requested published sample window (default 50, max 200)", "in": "query", - "name": "job_id", + "name": "sample", "required": false, "schema": { "type": "string" @@ -23978,7 +25066,7 @@ "content": { "application/json": { "schema": { - "type": "object" + "$ref": "#/components/schemas/GroundabilityReport" } } }, @@ -24196,13 +25284,15 @@ "bearer": [] } ], - "summary": "Poll an enqueued background export job by ?job_id= (scoped to the requesting tenant)", + "summary": "Read the bounded published groundability artifact; returns typed unavailable until maintenance has published one", "tags": [ "graph" ] - }, + } + }, + "/v1/graph/import": { "post": { - "operationId": "post_v1_graph_export_job", + "operationId": "post_v1_graph_import", "parameters": [ { "description": "Graph name (default `main`)", @@ -24241,9 +25331,36 @@ } }, { - "description": "Pin the export to a past snapshot commit_seq", + "description": "Records per internal commit (default 1000, clamped 1-10000)", "in": "query", - "name": "as_of_commit_seq", + "name": "batch", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Abort on the first malformed line instead of skipping it", + "in": "query", + "name": "strict", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "RFC3339 backfill instant stamped on every batch", + "in": "query", + "name": "observed_at", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Durably enqueue one atomic published-generation build after the final batch", + "in": "query", + "name": "publish", "required": false, "schema": { "type": "string" @@ -24255,7 +25372,7 @@ "content": { "application/json": { "schema": { - "type": "object" + "$ref": "#/components/schemas/GraphImportResponse" } } }, @@ -24473,15 +25590,15 @@ "bearer": [] } ], - "summary": "Enqueue a background full-fidelity export for a large tenant; writes zstd NDJSON parts + a manifest under tenants//exports//. Returns the job id; poll with GET /v1/graph/export/job?job_id=", + "summary": "Bulk-ingest a dataset as NDJSON (one triplet or entity-properties object per line); lines are batched into bounded internal commits", "tags": [ "graph" ] } }, - "/v1/graph/fork": { - "post": { - "operationId": "post_v1_graph_fork", + "/v1/graph/import-jobs": { + "delete": { + "operationId": "delete_v1_graph_import_jobs", "parameters": [ { "description": "Graph name (default `main`)", @@ -24520,28 +25637,10 @@ } }, { - "description": "Source graph id to fork from", - "in": "query", - "name": "src", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "Destination graph id to create (must not exist)", - "in": "query", - "name": "dst", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "Must equal the destination graph id to authorize the fork", + "description": "Stable job id returned by the submit call", "in": "query", - "name": "confirm", - "required": false, + "name": "job_id", + "required": true, "schema": { "type": "string" } @@ -24552,7 +25651,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GraphForkResponse" + "$ref": "#/components/schemas/GraphImportJobCancelResponse" } } }, @@ -24770,15 +25869,13 @@ "bearer": [] } ], - "summary": "Fork a graph: copy-based clone of the source graph's current head into a new graph in the same tenant — no data replay, no re-embedding; runs as a durable job (poll the destination's metadata)", + "summary": "Request cooperative cancellation of a durable import", "tags": [ "graph" ] - } - }, - "/v1/graph/groundability": { + }, "get": { - "operationId": "get_v1_graph_groundability", + "operationId": "get_v1_graph_import_jobs", "parameters": [ { "description": "Graph name (default `main`)", @@ -24808,10 +25905,10 @@ } }, { - "description": "Requested published sample window (default 50, max 200)", + "description": "Stable job id returned by the submit call", "in": "query", - "name": "sample", - "required": false, + "name": "job_id", + "required": true, "schema": { "type": "string" } @@ -24822,7 +25919,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GroundabilityReport" + "$ref": "#/components/schemas/GraphImportJobStatus" } } }, @@ -25040,15 +26137,13 @@ "bearer": [] } ], - "summary": "Read the bounded published groundability artifact; returns typed unavailable until maintenance has published one", + "summary": "Read durable import state, grouped-commit progress, terminal failure, and publication receipt", "tags": [ "graph" ] - } - }, - "/v1/graph/import": { + }, "post": { - "operationId": "post_v1_graph_import", + "operationId": "post_v1_graph_import_jobs", "parameters": [ { "description": "Graph name (default `main`)", @@ -25081,13 +26176,13 @@ "description": "Stable client-generated key for safely retrying mutations and supervision writes.", "in": "header", "name": "Idempotency-Key", - "required": false, + "required": true, "schema": { "type": "string" } }, { - "description": "Records per internal commit (default 1000, clamped 1-10000)", + "description": "Records per grouped commit (default and maximum 10000; minimum 1)", "in": "query", "name": "batch", "required": false, @@ -25096,7 +26191,7 @@ } }, { - "description": "Abort on the first malformed line instead of skipping it", + "description": "Fail the durable job on the first malformed line instead of recording and skipping it", "in": "query", "name": "strict", "required": false, @@ -25105,30 +26200,57 @@ } }, { - "description": "RFC3339 backfill instant stamped on every batch", + "description": "RFC3339 backfill instant stamped on every grouped commit", "in": "query", "name": "observed_at", "required": false, "schema": { "type": "string" } - }, - { - "description": "Durably enqueue one atomic published-generation build after the final batch", - "in": "query", - "name": "publish", - "required": false, - "schema": { - "type": "string" - } } ], + "requestBody": { + "content": { + "application/x-ndjson": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "description": "Newline-delimited JSON records streamed without assembling the complete import in client or server memory.", + "required": true + }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GraphImportResponse" + "$ref": "#/components/schemas/GraphImportJobAccepted" + } + } + }, + "description": "OK", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + }, + "202": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GraphImportJobAccepted" } } }, @@ -25346,7 +26468,7 @@ "bearer": [] } ], - "summary": "Bulk-ingest a dataset as NDJSON (one triplet or entity-properties object per line); lines are batched into bounded internal commits", + "summary": "Stream an NDJSON dataset into immutable storage and enqueue a durable grouped import; requires an Idempotency-Key", "tags": [ "graph" ] @@ -29040,15 +30162,285 @@ "bearer": [] } ], - "summary": "Observe a conversation episode: store it verbatim as EPISODE evidence, anchor and gate extracted facts on an observe branch, optionally auto-merge when validation is clean; requires an Idempotency-Key (flag-gated: --enable-observe)", + "summary": "Observe a conversation episode: store it verbatim as EPISODE evidence, anchor and gate extracted facts on an observe branch, optionally auto-merge when validation is clean; requires an Idempotency-Key (flag-gated: --enable-observe)", + "tags": [ + "system" + ] + } + }, + "/v1/models/cadence": { + "get": { + "operationId": "get_v1_models_cadence", + "parameters": [ + { + "description": "Graph name (default `main`)", + "in": "query", + "name": "graph", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Branch name (default `main`)", + "in": "query", + "name": "branch", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "API contract version to pin. Use `2026-07-23` for this beta-breaking shape.", + "in": "header", + "name": "Lbb-Version", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Model kind", + "in": "query", + "name": "kind", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModelCadenceResponse" + } + } + }, + "description": "OK", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LbbErrorEnvelope" + } + } + }, + "description": "Bad request", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LbbErrorEnvelope" + } + } + }, + "description": "Unauthorized", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LbbErrorEnvelope" + } + } + }, + "description": "Forbidden", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LbbErrorEnvelope" + } + } + }, + "description": "Not found", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LbbErrorEnvelope" + } + } + }, + "description": "Conflict", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LbbErrorEnvelope" + } + } + }, + "description": "Rate limit exceeded", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LbbErrorEnvelope" + } + } + }, + "description": "Internal server error", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LbbErrorEnvelope" + } + } + }, + "description": "Service unavailable", + "headers": { + "Lbb-Version": { + "description": "API contract version used for the response", + "schema": { + "type": "string" + } + }, + "X-Request-Id": { + "description": "Request correlation id", + "schema": { + "type": "string" + } + } + } + } + }, + "security": [ + { + "bearer": [] + } + ], + "summary": "Doubling retrain policy for a model kind: retrain_due when the graph doubled since the promoted run trained", "tags": [ "system" ] } }, - "/v1/models/cadence": { + "/v1/models/extractor-dataset": { "get": { - "operationId": "get_v1_models_cadence", + "operationId": "get_v1_models_extractor_dataset", "parameters": [ { "description": "Graph name (default `main`)", @@ -29078,9 +30470,18 @@ } }, { - "description": "Model kind", + "description": "Max episodes (default 200, cap 500)", "in": "query", - "name": "kind", + "name": "limit", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Maximum fact sequence included", + "in": "query", + "name": "split_seq", "required": false, "schema": { "type": "string" @@ -29092,7 +30493,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ModelCadenceResponse" + "$ref": "#/components/schemas/ExtractorDatasetResponse" } } }, @@ -29310,15 +30711,15 @@ "bearer": [] } ], - "summary": "Doubling retrain policy for a model kind: retrain_due when the graph doubled since the promoted run trained", + "summary": "Fetch the bounded published extractor training artifact", "tags": [ "system" ] } }, - "/v1/models/extractor-dataset": { + "/v1/models/planner-dataset": { "get": { - "operationId": "get_v1_models_extractor_dataset", + "operationId": "get_v1_models_planner_dataset", "parameters": [ { "description": "Graph name (default `main`)", @@ -29348,7 +30749,7 @@ } }, { - "description": "Max episodes (default 200, cap 500)", + "description": "Max examples (default 500, cap 2000)", "in": "query", "name": "limit", "required": false, @@ -29357,7 +30758,7 @@ } }, { - "description": "Maximum fact sequence included", + "description": "Maximum signal sequence included", "in": "query", "name": "split_seq", "required": false, @@ -29371,7 +30772,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ExtractorDatasetResponse" + "$ref": "#/components/schemas/PlannerDatasetResponse" } } }, @@ -29589,15 +30990,15 @@ "bearer": [] } ], - "summary": "Fetch the bounded published extractor training artifact", + "summary": "Fetch the bounded published planner training artifact", "tags": [ "system" ] } }, - "/v1/models/planner-dataset": { + "/v1/models/planner-preference-dataset": { "get": { - "operationId": "get_v1_models_planner_dataset", + "operationId": "get_v1_models_planner_preference_dataset", "parameters": [ { "description": "Graph name (default `main`)", @@ -29627,7 +31028,7 @@ } }, { - "description": "Max examples (default 500, cap 2000)", + "description": "Max pairs (default 500, cap 2000)", "in": "query", "name": "limit", "required": false, @@ -29650,7 +31051,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PlannerDatasetResponse" + "$ref": "#/components/schemas/PlannerPreferenceDatasetResponse" } } }, @@ -29868,15 +31269,15 @@ "bearer": [] } ], - "summary": "Fetch the bounded published planner training artifact", + "summary": "Fetch the bounded published planner preference artifact", "tags": [ "system" ] } }, - "/v1/models/planner-preference-dataset": { - "get": { - "operationId": "get_v1_models_planner_preference_dataset", + "/v1/models/promote": { + "post": { + "operationId": "post_v1_models_promote", "parameters": [ { "description": "Graph name (default `main`)", @@ -29906,18 +31307,27 @@ } }, { - "description": "Max pairs (default 500, cap 2000)", + "description": "Stable client-generated key for safely retrying mutations and supervision writes.", + "in": "header", + "name": "Idempotency-Key", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Model kind (embedding|calibration|fusion|suggest_ranker|planner|extractor)", "in": "query", - "name": "limit", + "name": "kind", "required": false, "schema": { "type": "string" } }, { - "description": "Maximum signal sequence included", + "description": "Run number to promote", "in": "query", - "name": "split_seq", + "name": "run", "required": false, "schema": { "type": "string" @@ -29929,7 +31339,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PlannerPreferenceDatasetResponse" + "type": "object" } } }, @@ -30147,15 +31557,15 @@ "bearer": [] } ], - "summary": "Fetch the bounded published planner preference artifact", + "summary": "CAS-promote a recorded run to CURRENT for its kind (manifests stay immutable; replay is a no-op)", "tags": [ "system" ] } }, - "/v1/models/promote": { + "/v1/models/promote-extractor": { "post": { - "operationId": "post_v1_models_promote", + "operationId": "post_v1_models_promote_extractor", "parameters": [ { "description": "Graph name (default `main`)", @@ -30194,18 +31604,18 @@ } }, { - "description": "Model kind (embedding|calibration|fusion|suggest_ranker|planner|extractor)", + "description": "The extractor_lora training run to promote", "in": "query", - "name": "kind", + "name": "run_id", "required": false, "schema": { "type": "string" } }, { - "description": "Run number to promote", + "description": "Promote even when tuned fact F1 regressed", "in": "query", - "name": "run", + "name": "allow_regression", "required": false, "schema": { "type": "string" @@ -30435,15 +31845,15 @@ "bearer": [] } ], - "summary": "CAS-promote a recorded run to CURRENT for its kind (manifests stay immutable; replay is a no-op)", + "summary": "Promote a finished extractor_lora run: gate on held-out fact F1 (?allow_regression=true to override), record a WS9 kind=extractor manifest with the adapter artifact, CAS-promote — resident extraction then serves the adapter", "tags": [ "system" ] } }, - "/v1/models/promote-extractor": { + "/v1/models/promote-planner": { "post": { - "operationId": "post_v1_models_promote_extractor", + "operationId": "post_v1_models_promote_planner", "parameters": [ { "description": "Graph name (default `main`)", @@ -30482,7 +31892,7 @@ } }, { - "description": "The extractor_lora training run to promote", + "description": "The planner_lora training run to promote", "in": "query", "name": "run_id", "required": false, @@ -30491,7 +31901,7 @@ } }, { - "description": "Promote even when tuned fact F1 regressed", + "description": "Promote even when tuned slot exactness regressed", "in": "query", "name": "allow_regression", "required": false, @@ -30723,15 +32133,15 @@ "bearer": [] } ], - "summary": "Promote a finished extractor_lora run: gate on held-out fact F1 (?allow_regression=true to override), record a WS9 kind=extractor manifest with the adapter artifact, CAS-promote — resident extraction then serves the adapter", + "summary": "Promote a finished planner_lora run: gate on held-out slot exactness (?allow_regression=true to override), record a WS9 kind=planner manifest with the adapter artifact, then CAS-promote", "tags": [ "system" ] } }, - "/v1/models/promote-planner": { + "/v1/models/record": { "post": { - "operationId": "post_v1_models_promote_planner", + "operationId": "post_v1_models_record", "parameters": [ { "description": "Graph name (default `main`)", @@ -30768,26 +32178,18 @@ "schema": { "type": "string" } - }, - { - "description": "The planner_lora training run to promote", - "in": "query", - "name": "run_id", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "Promote even when tuned slot exactness regressed", - "in": "query", - "name": "allow_regression", - "required": false, - "schema": { - "type": "string" - } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModelRunManifest" + } + } + }, + "required": true + }, "responses": { "200": { "content": { @@ -31011,15 +32413,15 @@ "bearer": [] } ], - "summary": "Promote a finished planner_lora run: gate on held-out slot exactness (?allow_regression=true to override), record a WS9 kind=planner manifest with the adapter artifact, then CAS-promote", + "summary": "Record one immutable model-as-run manifest in the registry (WS9); runs number sequentially per kind", "tags": [ "system" ] } }, - "/v1/models/record": { - "post": { - "operationId": "post_v1_models_record", + "/v1/models/registry": { + "get": { + "operationId": "get_v1_models_registry", "parameters": [ { "description": "Graph name (default `main`)", @@ -31049,31 +32451,21 @@ } }, { - "description": "Stable client-generated key for safely retrying mutations and supervision writes.", - "in": "header", - "name": "Idempotency-Key", + "description": "Model kind", + "in": "query", + "name": "kind", "required": false, "schema": { "type": "string" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ModelRunManifest" - } - } - }, - "required": true - }, "responses": { "200": { "content": { "application/json": { "schema": { - "type": "object" + "$ref": "#/components/schemas/ModelRegistryResponse" } } }, @@ -31291,15 +32683,15 @@ "bearer": [] } ], - "summary": "Record one immutable model-as-run manifest in the registry (WS9); runs number sequentially per kind", + "summary": "List a kind's model runs newest-first with effective promotion state", "tags": [ "system" ] } }, - "/v1/models/registry": { - "get": { - "operationId": "get_v1_models_registry", + "/v1/models/registry/gc": { + "post": { + "operationId": "post_v1_models_registry_gc", "parameters": [ { "description": "Graph name (default `main`)", @@ -31328,6 +32720,15 @@ "type": "string" } }, + { + "description": "Stable client-generated key for safely retrying mutations and supervision writes.", + "in": "header", + "name": "Idempotency-Key", + "required": false, + "schema": { + "type": "string" + } + }, { "description": "Model kind", "in": "query", @@ -31336,6 +32737,15 @@ "schema": { "type": "string" } + }, + { + "description": "Most-recent runs to keep besides the promoted one (default 3)", + "in": "query", + "name": "keep", + "required": false, + "schema": { + "type": "string" + } } ], "responses": { @@ -31343,7 +32753,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ModelRegistryResponse" + "type": "object" } } }, @@ -31561,15 +32971,15 @@ "bearer": [] } ], - "summary": "List a kind's model runs newest-first with effective promotion state", + "summary": "Delete run prefixes beyond the promoted run + the last N; reports deleted runs", "tags": [ "system" ] } }, - "/v1/models/registry/gc": { + "/v1/models/shadow-eval": { "post": { - "operationId": "post_v1_models_registry_gc", + "operationId": "post_v1_models_shadow_eval", "parameters": [ { "description": "Graph name (default `main`)", @@ -31606,32 +33016,24 @@ "schema": { "type": "string" } - }, - { - "description": "Model kind", - "in": "query", - "name": "kind", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "Most-recent runs to keep besides the promoted one (default 3)", - "in": "query", - "name": "keep", - "required": false, - "schema": { - "type": "string" - } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ShadowEvalRequest" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json": { "schema": { - "type": "object" + "$ref": "#/components/schemas/ShadowEvalResponse" } } }, @@ -31849,15 +33251,15 @@ "bearer": [] } ], - "summary": "Delete run prefixes beyond the promoted run + the last N; reports deleted runs", + "summary": "Run a bounded champion/challenger evaluation on one published generation; returns typed unavailable until the generation-pinned evaluator artifact is enabled", "tags": [ "system" ] } }, - "/v1/models/shadow-eval": { - "post": { - "operationId": "post_v1_models_shadow_eval", + "/v1/models/split-audit": { + "get": { + "operationId": "get_v1_models_split_audit", "parameters": [ { "description": "Graph name (default `main`)", @@ -31887,31 +33289,30 @@ } }, { - "description": "Stable client-generated key for safely retrying mutations and supervision writes.", - "in": "header", - "name": "Idempotency-Key", + "description": "Model kind", + "in": "query", + "name": "kind", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Run number", + "in": "query", + "name": "run", "required": false, "schema": { "type": "string" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ShadowEvalRequest" - } - } - }, - "required": true - }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ShadowEvalResponse" + "$ref": "#/components/schemas/ModelSplitAudit" } } }, @@ -32129,15 +33530,15 @@ "bearer": [] } ], - "summary": "Run a bounded champion/challenger evaluation on one published generation; returns typed unavailable until the generation-pinned evaluator artifact is enabled", + "summary": "Verify a run's temporal-split obligation from its recorded lineage (train ≤ pin, eval > pin)", "tags": [ "system" ] } }, - "/v1/models/split-audit": { + "/v1/models/suggest-dataset": { "get": { - "operationId": "get_v1_models_split_audit", + "operationId": "get_v1_models_suggest_dataset", "parameters": [ { "description": "Graph name (default `main`)", @@ -32167,18 +33568,18 @@ } }, { - "description": "Model kind", + "description": "Max probes (default 100, cap 200)", "in": "query", - "name": "kind", + "name": "limit", "required": false, "schema": { "type": "string" } }, { - "description": "Run number", + "description": "Maximum signal sequence included", "in": "query", - "name": "run", + "name": "split_seq", "required": false, "schema": { "type": "string" @@ -32190,7 +33591,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ModelSplitAudit" + "$ref": "#/components/schemas/SuggestDatasetResponse" } } }, @@ -32408,15 +33809,15 @@ "bearer": [] } ], - "summary": "Verify a run's temporal-split obligation from its recorded lineage (train ≤ pin, eval > pin)", + "summary": "Fetch the bounded published suggestion-ranker training artifact", "tags": [ "system" ] } }, - "/v1/models/suggest-dataset": { + "/v1/models/synthetic-eval": { "get": { - "operationId": "get_v1_models_suggest_dataset", + "operationId": "get_v1_models_synthetic_eval", "parameters": [ { "description": "Graph name (default `main`)", @@ -32446,22 +33847,13 @@ } }, { - "description": "Max probes (default 100, cap 200)", + "description": "Max probes (default 100, max 500)", "in": "query", "name": "limit", "required": false, "schema": { "type": "string" } - }, - { - "description": "Maximum signal sequence included", - "in": "query", - "name": "split_seq", - "required": false, - "schema": { - "type": "string" - } } ], "responses": { @@ -32469,7 +33861,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SuggestDatasetResponse" + "$ref": "#/components/schemas/SyntheticEvalResponse" } } }, @@ -32687,15 +34079,15 @@ "bearer": [] } ], - "summary": "Fetch the bounded published suggestion-ranker training artifact", + "summary": "Execution-verified QA probes generated from current edges (labels are the executed projections; deterministic per snapshot)", "tags": [ "system" ] } }, - "/v1/models/synthetic-eval": { + "/v1/models/train-jobs": { "get": { - "operationId": "get_v1_models_synthetic_eval", + "operationId": "get_v1_models_train_jobs", "parameters": [ { "description": "Graph name (default `main`)", @@ -32725,9 +34117,9 @@ } }, { - "description": "Max probes (default 100, max 500)", + "description": "Stable job id returned by the submit call", "in": "query", - "name": "limit", + "name": "job_id", "required": false, "schema": { "type": "string" @@ -32739,7 +34131,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SyntheticEvalResponse" + "$ref": "#/components/schemas/TrainModelJobStatusResponse" } } }, @@ -32957,15 +34349,13 @@ "bearer": [] } ], - "summary": "Execution-verified QA probes generated from current edges (labels are the executed projections; deterministic per snapshot)", + "summary": "Read durable trainer job state, progress, terminal error, and the full gated training result", "tags": [ "system" ] - } - }, - "/v1/models/train-jobs": { - "get": { - "operationId": "get_v1_models_train_jobs", + }, + "post": { + "operationId": "post_v1_models_train_jobs", "parameters": [ { "description": "Graph name (default `main`)", @@ -32995,15 +34385,25 @@ } }, { - "description": "Stable job id returned by the submit call", - "in": "query", - "name": "job_id", + "description": "Stable client-generated key for safely retrying mutations and supervision writes.", + "in": "header", + "name": "Idempotency-Key", "required": false, "schema": { "type": "string" } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TrainModelRequest" + } + } + }, + "required": true + }, "responses": { "200": { "content": { @@ -33227,13 +34627,15 @@ "bearer": [] } ], - "summary": "Read durable trainer job state, progress, terminal error, and the full gated training result", + "summary": "Idempotently enqueue a durable background trainer job; reconnect through the returned stable job id", "tags": [ "system" ] - }, + } + }, + "/v1/models/train-tick": { "post": { - "operationId": "post_v1_models_train_jobs", + "operationId": "post_v1_models_train_tick", "parameters": [ { "description": "Graph name (default `main`)", @@ -33287,7 +34689,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TrainModelJobStatusResponse" + "$ref": "#/components/schemas/TrainModelResponse" } } }, @@ -33505,15 +34907,15 @@ "bearer": [] } ], - "summary": "Idempotently enqueue a durable background trainer job; reconnect through the returned stable job id", + "summary": "One deterministic trainer tick: probe set (synthetic or BYO) → bounded candidate search on the train slice → held-out eval gate → record run (held or promoted) → CAS promote only when the gate passes", "tags": [ "system" ] } }, - "/v1/models/train-tick": { - "post": { - "operationId": "post_v1_models_train_tick", + "/v1/models/training-config": { + "get": { + "operationId": "get_v1_models_training_config", "parameters": [ { "description": "Graph name (default `main`)", @@ -33541,33 +34943,14 @@ "schema": { "type": "string" } - }, - { - "description": "Stable client-generated key for safely retrying mutations and supervision writes.", - "in": "header", - "name": "Idempotency-Key", - "required": false, - "schema": { - "type": "string" - } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TrainModelRequest" - } - } - }, - "required": true - }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TrainModelResponse" + "$ref": "#/components/schemas/ModelTrainingConfig" } } }, @@ -33785,15 +35168,13 @@ "bearer": [] } ], - "summary": "One deterministic trainer tick: probe set (synthetic or BYO) → bounded candidate search on the train slice → held-out eval gate → record run (held or promoted) → CAS promote only when the gate passes", + "summary": "The graph's automatic-training configuration (default: off)", "tags": [ "system" ] - } - }, - "/v1/models/training-config": { - "get": { - "operationId": "get_v1_models_training_config", + }, + "post": { + "operationId": "post_v1_models_training_config", "parameters": [ { "description": "Graph name (default `main`)", @@ -33821,8 +35202,27 @@ "schema": { "type": "string" } + }, + { + "description": "Stable client-generated key for safely retrying mutations and supervision writes.", + "in": "header", + "name": "Idempotency-Key", + "required": false, + "schema": { + "type": "string" + } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModelTrainingConfig" + } + } + }, + "required": true + }, "responses": { "200": { "content": { @@ -34046,13 +35446,15 @@ "bearer": [] } ], - "summary": "The graph's automatic-training configuration (default: off)", + "summary": "Set the automatic-training configuration (auto_train toggle + trainable kinds)", "tags": [ "system" ] - }, - "post": { - "operationId": "post_v1_models_training_config", + } + }, + "/v1/ontology": { + "get": { + "operationId": "get_v1_ontology", "parameters": [ { "description": "Graph name (default `main`)", @@ -34080,33 +35482,14 @@ "schema": { "type": "string" } - }, - { - "description": "Stable client-generated key for safely retrying mutations and supervision writes.", - "in": "header", - "name": "Idempotency-Key", - "required": false, - "schema": { - "type": "string" - } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ModelTrainingConfig" - } - } - }, - "required": true - }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ModelTrainingConfig" + "$ref": "#/components/schemas/OntologyView" } } }, @@ -34324,15 +35707,15 @@ "bearer": [] } ], - "summary": "Set the automatic-training configuration (auto_train toggle + trainable kinds)", + "summary": "Active ontology vocabulary (entity types and relations) for the scoped graph", "tags": [ - "system" + "ontology" ] } }, - "/v1/ontology": { + "/v1/ontology/conformance": { "get": { - "operationId": "get_v1_ontology", + "operationId": "get_v1_ontology_conformance", "parameters": [ { "description": "Graph name (default `main`)", @@ -34360,6 +35743,24 @@ "schema": { "type": "string" } + }, + { + "description": "eventual (default) serves the referenced durable report; strong requires validation at current head with current ontology/shapes", + "in": "query", + "name": "consistency", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Maximum returned result rows; result_count remains exact", + "in": "query", + "name": "limit", + "required": false, + "schema": { + "type": "string" + } } ], "responses": { @@ -34367,7 +35768,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OntologyView" + "$ref": "#/components/schemas/SchemaAuditReport" } } }, @@ -34585,15 +35986,15 @@ "bearer": [] } ], - "summary": "Active ontology vocabulary (entity types and relations) for the scoped graph", + "summary": "Read the durable ontology-conformance report referenced by the published snapshot, including its validation watermark and ontology/shapes provenance", "tags": [ "ontology" ] } }, - "/v1/ontology/conformance": { - "get": { - "operationId": "get_v1_ontology_conformance", + "/v1/ontology/define": { + "post": { + "operationId": "post_v1_ontology_define", "parameters": [ { "description": "Graph name (default `main`)", @@ -34623,30 +36024,31 @@ } }, { - "description": "eventual (default) serves the referenced durable report; strong requires validation at current head with current ontology/shapes", - "in": "query", - "name": "consistency", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "Maximum returned result rows; result_count remains exact", - "in": "query", - "name": "limit", + "description": "Stable client-generated key for safely retrying mutations and supervision writes.", + "in": "header", + "name": "Idempotency-Key", "required": false, "schema": { "type": "string" } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OntologyDefineRequest" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchemaAuditReport" + "$ref": "#/components/schemas/OntologyDefineResponse" } } }, @@ -34864,15 +36266,15 @@ "bearer": [] } ], - "summary": "Read the durable ontology-conformance report referenced by the published snapshot, including its validation watermark and ontology/shapes provenance", + "summary": "Import a custom ontology and create the scoped graph head with it; fails if the graph already exists", "tags": [ "ontology" ] } }, - "/v1/ontology/define": { - "post": { - "operationId": "post_v1_ontology_define", + "/v1/ontology/drafts": { + "get": { + "operationId": "get_v1_ontology_drafts", "parameters": [ { "description": "Graph name (default `main`)", @@ -34902,31 +36304,21 @@ } }, { - "description": "Stable client-generated key for safely retrying mutations and supervision writes.", - "in": "header", - "name": "Idempotency-Key", + "description": "Stable ontology draft identifier", + "in": "query", + "name": "draft_id", "required": false, "schema": { "type": "string" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OntologyDefineRequest" - } - } - }, - "required": true - }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OntologyDefineResponse" + "$ref": "#/components/schemas/OntologyDraft" } } }, @@ -35144,15 +36536,13 @@ "bearer": [] } ], - "summary": "Import a custom ontology and create the scoped graph head with it; fails if the graph already exists", + "summary": "Read a durable ontology review artifact", "tags": [ "ontology" ] - } - }, - "/v1/ontology/drafts": { - "get": { - "operationId": "get_v1_ontology_drafts", + }, + "post": { + "operationId": "post_v1_ontology_drafts", "parameters": [ { "description": "Graph name (default `main`)", @@ -35182,15 +36572,25 @@ } }, { - "description": "Stable ontology draft identifier", - "in": "query", - "name": "draft_id", + "description": "Stable client-generated key for safely retrying mutations and supervision writes.", + "in": "header", + "name": "Idempotency-Key", "required": false, "schema": { "type": "string" } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OntologyDraftCreateRequest" + } + } + }, + "required": true + }, "responses": { "200": { "content": { @@ -35414,13 +36814,15 @@ "bearer": [] } ], - "summary": "Read a durable ontology review artifact", + "summary": "Create a deterministic, snapshot-pinned ontology proposal from connector samples without inserting those samples", "tags": [ "ontology" ] - }, + } + }, + "/v1/ontology/drafts/promote": { "post": { - "operationId": "post_v1_ontology_drafts", + "operationId": "post_v1_ontology_drafts_promote", "parameters": [ { "description": "Graph name (default `main`)", @@ -35453,22 +36855,21 @@ "description": "Stable client-generated key for safely retrying mutations and supervision writes.", "in": "header", "name": "Idempotency-Key", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Stable ontology draft identifier", + "in": "query", + "name": "draft_id", "required": false, "schema": { "type": "string" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OntologyDraftCreateRequest" - } - } - }, - "required": true - }, "responses": { "200": { "content": { @@ -35692,15 +37093,15 @@ "bearer": [] } ], - "summary": "Create a deterministic, snapshot-pinned ontology proposal from connector samples without inserting those samples", + "summary": "Atomically publish a validated ontology draft; requires Idempotency-Key", "tags": [ "ontology" ] } }, - "/v1/ontology/drafts/promote": { + "/v1/ontology/drafts/reject": { "post": { - "operationId": "post_v1_ontology_drafts_promote", + "operationId": "post_v1_ontology_drafts_reject", "parameters": [ { "description": "Graph name (default `main`)", @@ -35733,7 +37134,7 @@ "description": "Stable client-generated key for safely retrying mutations and supervision writes.", "in": "header", "name": "Idempotency-Key", - "required": true, + "required": false, "schema": { "type": "string" } @@ -35746,6 +37147,15 @@ "schema": { "type": "string" } + }, + { + "description": "Auditable rejection reason", + "in": "query", + "name": "reason", + "required": false, + "schema": { + "type": "string" + } } ], "responses": { @@ -35971,15 +37381,15 @@ "bearer": [] } ], - "summary": "Atomically publish a validated ontology draft; requires Idempotency-Key", + "summary": "Reject a reviewable ontology draft without changing the graph ontology", "tags": [ "ontology" ] } }, - "/v1/ontology/drafts/reject": { + "/v1/ontology/drafts/validate": { "post": { - "operationId": "post_v1_ontology_drafts_reject", + "operationId": "post_v1_ontology_drafts_validate", "parameters": [ { "description": "Graph name (default `main`)", @@ -36025,15 +37435,6 @@ "schema": { "type": "string" } - }, - { - "description": "Auditable rejection reason", - "in": "query", - "name": "reason", - "required": false, - "schema": { - "type": "string" - } } ], "responses": { @@ -36259,15 +37660,15 @@ "bearer": [] } ], - "summary": "Reject a reviewable ontology draft without changing the graph ontology", + "summary": "Revalidate the exact proposed operations at the draft's pinned graph snapshot", "tags": [ "ontology" ] } }, - "/v1/ontology/drafts/validate": { + "/v1/ontology/evolve": { "post": { - "operationId": "post_v1_ontology_drafts_validate", + "operationId": "post_v1_ontology_evolve", "parameters": [ { "description": "Graph name (default `main`)", @@ -36306,21 +37707,31 @@ } }, { - "description": "Stable ontology draft identifier", + "description": "Validate and return the exact predicted evolution without writing an ontology object or graph head", "in": "query", - "name": "draft_id", + "name": "dry_run", "required": false, "schema": { "type": "string" } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OntologyEvolveRequest" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OntologyDraft" + "$ref": "#/components/schemas/OntologyEvolveResponse" } } }, @@ -36538,15 +37949,15 @@ "bearer": [] } ], - "summary": "Revalidate the exact proposed operations at the draft's pinned graph snapshot", + "summary": "Evolve the scoped graph's ontology with ordered operations, or preview the exact diff, conflicts, and resulting version without mutation", "tags": [ "ontology" ] } }, - "/v1/ontology/evolve": { + "/v1/ontology/induce": { "post": { - "operationId": "post_v1_ontology_evolve", + "operationId": "post_v1_ontology_induce", "parameters": [ { "description": "Graph name (default `main`)", @@ -36583,22 +37994,13 @@ "schema": { "type": "string" } - }, - { - "description": "Validate and return the exact predicted evolution without writing an ontology object or graph head", - "in": "query", - "name": "dry_run", - "required": false, - "schema": { - "type": "string" - } } ], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OntologyEvolveRequest" + "$ref": "#/components/schemas/OntologyInduceRequest" } } }, @@ -36609,7 +38011,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OntologyEvolveResponse" + "$ref": "#/components/schemas/OntologyInduceResponse" } } }, @@ -36827,15 +38229,15 @@ "bearer": [] } ], - "summary": "Evolve the scoped graph's ontology with ordered operations, or preview the exact diff, conflicts, and resulting version without mutation", + "summary": "Embedding-cluster ontology induction (MVP): mine tight embedding clusters among currently under-typed entities/edges and return suggested ontology patches. Suggestions only -- never applies anything; accept one via /v1/ontology/evolve with its suggested_ops", "tags": [ "ontology" ] } }, - "/v1/ontology/induce": { + "/v1/ontology/resolve": { "post": { - "operationId": "post_v1_ontology_induce", + "operationId": "post_v1_ontology_resolve", "parameters": [ { "description": "Graph name (default `main`)", @@ -36878,7 +38280,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OntologyInduceRequest" + "$ref": "#/components/schemas/OntologyResolveRequest" } } }, @@ -36889,7 +38291,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OntologyInduceResponse" + "$ref": "#/components/schemas/OntologyResolveResponse" } } }, @@ -37107,15 +38509,15 @@ "bearer": [] } ], - "summary": "Embedding-cluster ontology induction (MVP): mine tight embedding clusters among currently under-typed entities/edges and return suggested ontology patches. Suggestions only -- never applies anything; accept one via /v1/ontology/evolve with its suggested_ops", + "summary": "Resolve mentions to concepts/entities", "tags": [ "ontology" ] } }, - "/v1/ontology/resolve": { + "/v1/ontology/search": { "post": { - "operationId": "post_v1_ontology_resolve", + "operationId": "post_v1_ontology_search", "parameters": [ { "description": "Graph name (default `main`)", @@ -37158,7 +38560,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OntologyResolveRequest" + "$ref": "#/components/schemas/OntologySearchRequest" } } }, @@ -37169,7 +38571,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OntologyResolveResponse" + "$ref": "#/components/schemas/OntologySearchResponse" } } }, @@ -37387,15 +38789,15 @@ "bearer": [] } ], - "summary": "Resolve mentions to concepts/entities", + "summary": "Discover ontology concepts, terms, and relations", "tags": [ "ontology" ] } }, - "/v1/ontology/search": { + "/v1/query/analytics": { "post": { - "operationId": "post_v1_ontology_search", + "operationId": "post_v1_query_analytics", "parameters": [ { "description": "Graph name (default `main`)", @@ -37438,7 +38840,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OntologySearchRequest" + "$ref": "#/components/schemas/AnalyticQueryRequest" } } }, @@ -37449,7 +38851,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OntologySearchResponse" + "$ref": "#/components/schemas/AnalyticQueryResponse" } } }, @@ -37667,15 +39069,15 @@ "bearer": [] } ], - "summary": "Discover ontology concepts, terms, and relations", + "summary": "Basic-graph-pattern (BGP) analytic query over the permutation view", "tags": [ - "ontology" + "query" ] } }, - "/v1/query/analytics": { + "/v1/query/conflicts": { "post": { - "operationId": "post_v1_query_analytics", + "operationId": "post_v1_query_conflicts", "parameters": [ { "description": "Graph name (default `main`)", @@ -37718,7 +39120,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AnalyticQueryRequest" + "$ref": "#/components/schemas/GovernedConflictAggregationRequest" } } }, @@ -37729,7 +39131,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AnalyticQueryResponse" + "$ref": "#/components/schemas/GovernedConflictAggregationResponse" } } }, @@ -37947,15 +39349,15 @@ "bearer": [] } ], - "summary": "Basic-graph-pattern (BGP) analytic query over the permutation view", + "summary": "ACL-first snapshot aggregation returning only keys with multiple distinct values and bounded evidence entity ids", "tags": [ "query" ] } }, - "/v1/query/conflicts": { + "/v1/query/history": { "post": { - "operationId": "post_v1_query_conflicts", + "operationId": "post_v1_query_history", "parameters": [ { "description": "Graph name (default `main`)", @@ -37998,7 +39400,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GovernedConflictAggregationRequest" + "$ref": "#/components/schemas/RelationshipHistoryRequest" } } }, @@ -38009,7 +39411,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GovernedConflictAggregationResponse" + "$ref": "#/components/schemas/RelationshipHistoryResponse" } } }, @@ -38227,15 +39629,15 @@ "bearer": [] } ], - "summary": "ACL-first snapshot aggregation returning only keys with multiple distinct values and bounded evidence entity ids", + "summary": "Relationship history", "tags": [ "query" ] } }, - "/v1/query/history": { + "/v1/query/sparql": { "post": { - "operationId": "post_v1_query_history", + "operationId": "post_v1_query_sparql", "parameters": [ { "description": "Graph name (default `main`)", @@ -38278,7 +39680,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RelationshipHistoryRequest" + "$ref": "#/components/schemas/SparqlSelectRequest" } } }, @@ -38289,7 +39691,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RelationshipHistoryResponse" + "$ref": "#/components/schemas/SparqlSelectResponse" } } }, @@ -38507,15 +39909,15 @@ "bearer": [] } ], - "summary": "Relationship history", + "summary": "SPARQL-subset SELECT/ASK query (BGP WHERE, projection, DISTINCT, LIMIT/OFFSET)", "tags": [ "query" ] } }, - "/v1/query/sparql": { + "/v1/query/sparql-text": { "post": { - "operationId": "post_v1_query_sparql", + "operationId": "post_v1_query_sparql_text", "parameters": [ { "description": "Graph name (default `main`)", @@ -38558,7 +39960,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SparqlSelectRequest" + "$ref": "#/components/schemas/SparqlTextRequest" } } }, @@ -38569,7 +39971,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SparqlSelectResponse" + "$ref": "#/components/schemas/SparqlTextResponse" } } }, @@ -38787,15 +40189,15 @@ "bearer": [] } ], - "summary": "SPARQL-subset SELECT/ASK query (BGP WHERE, projection, DISTINCT, LIMIT/OFFSET)", + "summary": "SPARQL 1.1 query from text (SELECT/ASK) over the live graph -> SPARQL Results JSON", "tags": [ "query" ] } }, - "/v1/query/sparql-text": { + "/v1/query/state": { "post": { - "operationId": "post_v1_query_sparql_text", + "operationId": "post_v1_query_state", "parameters": [ { "description": "Graph name (default `main`)", @@ -38838,7 +40240,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SparqlTextRequest" + "$ref": "#/components/schemas/CurrentStateRequest" } } }, @@ -38849,7 +40251,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SparqlTextResponse" + "$ref": "#/components/schemas/CurrentStateResponse" } } }, @@ -39067,15 +40469,15 @@ "bearer": [] } ], - "summary": "SPARQL 1.1 query from text (SELECT/ASK) over the live graph -> SPARQL Results JSON", + "summary": "Current state of an entity's relations, optionally as-of", "tags": [ "query" ] } }, - "/v1/query/state": { + "/v1/query/transitions": { "post": { - "operationId": "post_v1_query_state", + "operationId": "post_v1_query_transitions", "parameters": [ { "description": "Graph name (default `main`)", @@ -39118,7 +40520,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CurrentStateRequest" + "$ref": "#/components/schemas/EntityTransitionsRequest" } } }, @@ -39129,7 +40531,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CurrentStateResponse" + "$ref": "#/components/schemas/EntityTransitionsResponse" } } }, @@ -39347,15 +40749,15 @@ "bearer": [] } ], - "summary": "Current state of an entity's relations, optionally as-of", + "summary": "State-transition log for an entity's relation, with dwell time", "tags": [ "query" ] } }, - "/v1/query/transitions": { + "/v1/query/why": { "post": { - "operationId": "post_v1_query_transitions", + "operationId": "post_v1_query_why", "parameters": [ { "description": "Graph name (default `main`)", @@ -39398,7 +40800,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EntityTransitionsRequest" + "$ref": "#/components/schemas/WhyRequest" } } }, @@ -39409,7 +40811,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EntityTransitionsResponse" + "$ref": "#/components/schemas/WhyResponse" } } }, @@ -39627,15 +41029,15 @@ "bearer": [] } ], - "summary": "State-transition log for an entity's relation, with dwell time", + "summary": "Lineage and evidence for a single edge", "tags": [ "query" ] } }, - "/v1/query/why": { - "post": { - "operationId": "post_v1_query_why", + "/v1/schema": { + "get": { + "operationId": "get_v1_schema", "parameters": [ { "description": "Graph name (default `main`)", @@ -39663,33 +41065,14 @@ "schema": { "type": "string" } - }, - { - "description": "Stable client-generated key for safely retrying mutations and supervision writes.", - "in": "header", - "name": "Idempotency-Key", - "required": false, - "schema": { - "type": "string" - } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WhyRequest" - } - } - }, - "required": true - }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WhyResponse" + "$ref": "#/components/schemas/SchemaBundleView" } } }, @@ -39907,15 +41290,15 @@ "bearer": [] } ], - "summary": "Lineage and evidence for a single edge", + "summary": "Read active ontology and SHACL shapes metadata without running validation", "tags": [ - "query" + "schema" ] } }, - "/v1/schema": { - "get": { - "operationId": "get_v1_schema", + "/v1/schema/publish": { + "post": { + "operationId": "post_v1_schema_publish", "parameters": [ { "description": "Graph name (default `main`)", @@ -39943,14 +41326,33 @@ "schema": { "type": "string" } + }, + { + "description": "Stable client-generated key for safely retrying mutations and supervision writes.", + "in": "header", + "name": "Idempotency-Key", + "required": false, + "schema": { + "type": "string" + } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SchemaPublishRequest" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchemaBundleView" + "$ref": "#/components/schemas/SchemaPublishResponse" } } }, @@ -40168,15 +41570,15 @@ "bearer": [] } ], - "summary": "Read active ontology and SHACL shapes metadata without running validation", + "summary": "Atomically activate a SHACL shapes bundle and enqueue durable conformance validation", "tags": [ "schema" ] } }, - "/v1/schema/publish": { - "post": { - "operationId": "post_v1_schema_publish", + "/v1/search": { + "get": { + "operationId": "get_v1_search", "parameters": [ { "description": "Graph name (default `main`)", @@ -40206,31 +41608,39 @@ } }, { - "description": "Stable client-generated key for safely retrying mutations and supervision writes.", - "in": "header", - "name": "Idempotency-Key", + "description": "Search text", + "in": "query", + "name": "query", "required": false, "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SchemaPublishRequest" - } + }, + { + "description": "Number of results", + "in": "query", + "name": "top_k", + "required": false, + "schema": { + "type": "string" } }, - "required": true - }, + { + "description": "strong or eventual", + "in": "query", + "name": "consistency", + "required": false, + "schema": { + "type": "string" + } + } + ], "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SchemaPublishResponse" + "$ref": "#/components/schemas/SemanticGraphSearchResponse" } } }, @@ -40448,15 +41858,15 @@ "bearer": [] } ], - "summary": "Atomically activate a SHACL shapes bundle and enqueue durable conformance validation", + "summary": "Quick semantic hybrid search by text", "tags": [ - "schema" + "search" ] } }, - "/v1/search": { - "get": { - "operationId": "get_v1_search", + "/v1/search/embedding": { + "post": { + "operationId": "post_v1_search_embedding", "parameters": [ { "description": "Graph name (default `main`)", @@ -40486,39 +41896,31 @@ } }, { - "description": "Search text", - "in": "query", - "name": "query", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "Number of results", - "in": "query", - "name": "top_k", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "strong or eventual", - "in": "query", - "name": "consistency", + "description": "Stable client-generated key for safely retrying mutations and supervision writes.", + "in": "header", + "name": "Idempotency-Key", "required": false, "schema": { "type": "string" } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmbeddingSearchRequest" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SemanticGraphSearchResponse" + "$ref": "#/components/schemas/EmbeddingSearchResponse" } } }, @@ -40736,15 +42138,15 @@ "bearer": [] } ], - "summary": "Quick semantic hybrid search by text", + "summary": "ANN/vector search", "tags": [ "search" ] } }, - "/v1/search/embedding": { + "/v1/search/embedding/inspect": { "post": { - "operationId": "post_v1_search_embedding", + "operationId": "post_v1_search_embedding_inspect", "parameters": [ { "description": "Graph name (default `main`)", @@ -40787,7 +42189,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EmbeddingSearchRequest" + "$ref": "#/components/schemas/EmbeddingIndexInspectRequest" } } }, @@ -40798,7 +42200,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EmbeddingSearchResponse" + "$ref": "#/components/schemas/EmbeddingIndexInspectResponse" } } }, @@ -41016,15 +42418,15 @@ "bearer": [] } ], - "summary": "ANN/vector search", + "summary": "Inspect the ANN index layout", "tags": [ "search" ] } }, - "/v1/search/embedding/inspect": { + "/v1/search/feedback": { "post": { - "operationId": "post_v1_search_embedding_inspect", + "operationId": "post_v1_search_feedback", "parameters": [ { "description": "Graph name (default `main`)", @@ -41067,7 +42469,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EmbeddingIndexInspectRequest" + "$ref": "#/components/schemas/SearchFeedbackRequest" } } }, @@ -41078,7 +42480,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/EmbeddingIndexInspectResponse" + "$ref": "#/components/schemas/SearchFeedbackResponse" } } }, @@ -41296,15 +42698,15 @@ "bearer": [] } ], - "summary": "Inspect the ANN index layout", + "summary": "Append relevance labels for search results", "tags": [ "search" ] } }, - "/v1/search/feedback": { - "post": { - "operationId": "post_v1_search_feedback", + "/v1/search/feedback/export": { + "get": { + "operationId": "get_v1_search_feedback_export", "parameters": [ { "description": "Graph name (default `main`)", @@ -41332,33 +42734,14 @@ "schema": { "type": "string" } - }, - { - "description": "Stable client-generated key for safely retrying mutations and supervision writes.", - "in": "header", - "name": "Idempotency-Key", - "required": false, - "schema": { - "type": "string" - } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SearchFeedbackRequest" - } - } - }, - "required": true - }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SearchFeedbackResponse" + "$ref": "#/components/schemas/SearchFeedbackExportResponse" } } }, @@ -41576,15 +42959,15 @@ "bearer": [] } ], - "summary": "Append relevance labels for search results", + "summary": "Export relevance labels as qrels-style rows for training", "tags": [ "search" ] } }, - "/v1/search/feedback/export": { + "/v1/search/feedback/summary": { "get": { - "operationId": "get_v1_search_feedback_export", + "operationId": "get_v1_search_feedback_summary", "parameters": [ { "description": "Graph name (default `main`)", @@ -41619,7 +43002,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SearchFeedbackExportResponse" + "$ref": "#/components/schemas/SearchFeedbackSummaryResponse" } } }, @@ -41837,15 +43220,15 @@ "bearer": [] } ], - "summary": "Export relevance labels as qrels-style rows for training", + "summary": "Read constant-size feedback counts and promoted-model status", "tags": [ "search" ] } }, - "/v1/search/feedback/summary": { - "get": { - "operationId": "get_v1_search_feedback_summary", + "/v1/search/full-text": { + "post": { + "operationId": "post_v1_search_full_text", "parameters": [ { "description": "Graph name (default `main`)", @@ -41873,14 +43256,33 @@ "schema": { "type": "string" } + }, + { + "description": "Stable client-generated key for safely retrying mutations and supervision writes.", + "in": "header", + "name": "Idempotency-Key", + "required": false, + "schema": { + "type": "string" + } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FullTextSearchRequest" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SearchFeedbackSummaryResponse" + "$ref": "#/components/schemas/FullTextSearchResponse" } } }, @@ -42098,15 +43500,15 @@ "bearer": [] } ], - "summary": "Read constant-size feedback counts and promoted-model status", + "summary": "BM25 search", "tags": [ "search" ] } }, - "/v1/search/full-text": { + "/v1/search/full-text/inspect": { "post": { - "operationId": "post_v1_search_full_text", + "operationId": "post_v1_search_full_text_inspect", "parameters": [ { "description": "Graph name (default `main`)", @@ -42149,7 +43551,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/FullTextSearchRequest" + "$ref": "#/components/schemas/FullTextIndexInspectRequest" } } }, @@ -42160,7 +43562,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/FullTextSearchResponse" + "$ref": "#/components/schemas/FullTextIndexInspectResponse" } } }, @@ -42378,15 +43780,15 @@ "bearer": [] } ], - "summary": "BM25 search", + "summary": "Inspect the BM25 index layout", "tags": [ "search" ] } }, - "/v1/search/full-text/inspect": { + "/v1/search/multi": { "post": { - "operationId": "post_v1_search_full_text_inspect", + "operationId": "post_v1_search_multi", "parameters": [ { "description": "Graph name (default `main`)", @@ -42429,7 +43831,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/FullTextIndexInspectRequest" + "$ref": "#/components/schemas/HybridMultiSearchRequest" } } }, @@ -42440,7 +43842,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/FullTextIndexInspectResponse" + "$ref": "#/components/schemas/HybridMultiSearchResponse" } } }, @@ -42658,15 +44060,15 @@ "bearer": [] } ], - "summary": "Inspect the BM25 index layout", + "summary": "Reciprocal-rank-fusion across sub-queries", "tags": [ "search" ] } }, - "/v1/search/multi": { + "/v1/search/resolve-term": { "post": { - "operationId": "post_v1_search_multi", + "operationId": "post_v1_search_resolve_term", "parameters": [ { "description": "Graph name (default `main`)", @@ -42709,7 +44111,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HybridMultiSearchRequest" + "$ref": "#/components/schemas/ResolveTermRequest" } } }, @@ -42720,7 +44122,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HybridMultiSearchResponse" + "$ref": "#/components/schemas/ResolveTermResponse" } } }, @@ -42938,15 +44340,15 @@ "bearer": [] } ], - "summary": "Reciprocal-rank-fusion across sub-queries", + "summary": "Snap free text to vocabulary from the pinned published read root", "tags": [ "search" ] } }, - "/v1/search/resolve-term": { + "/v1/search/session": { "post": { - "operationId": "post_v1_search_resolve_term", + "operationId": "post_v1_search_session", "parameters": [ { "description": "Graph name (default `main`)", @@ -42985,22 +44387,12 @@ } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResolveTermRequest" - } - } - }, - "required": true - }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ResolveTermResponse" + "$ref": "#/components/schemas/SearchSessionOpenResponse" } } }, @@ -43218,15 +44610,15 @@ "bearer": [] } ], - "summary": "Snap free text to vocabulary from the pinned published read root", + "summary": "Open a snapshot-pinned search session (WS6 L2 channel)", "tags": [ "search" ] } }, - "/v1/search/session": { + "/v1/search/session/commit": { "post": { - "operationId": "post_v1_search_session", + "operationId": "post_v1_search_session_commit", "parameters": [ { "description": "Graph name (default `main`)", @@ -43265,12 +44657,22 @@ } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SearchSessionCommitRequest" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SearchSessionOpenResponse" + "$ref": "#/components/schemas/SearchSessionCommitResponse" } } }, @@ -43488,15 +44890,15 @@ "bearer": [] } ], - "summary": "Open a snapshot-pinned search session (WS6 L2 channel)", + "summary": "Run the search against the pinned session (equivalence: byte-identical to a plain search)", "tags": [ "search" ] } }, - "/v1/search/session/commit": { + "/v1/search/session/prefix": { "post": { - "operationId": "post_v1_search_session_commit", + "operationId": "post_v1_search_session_prefix", "parameters": [ { "description": "Graph name (default `main`)", @@ -43539,7 +44941,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SearchSessionCommitRequest" + "$ref": "#/components/schemas/SearchSessionPrefixRequest" } } }, @@ -43550,7 +44952,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SearchSessionCommitResponse" + "$ref": "#/components/schemas/SearchSessionPrefixResponse" } } }, @@ -43768,15 +45170,15 @@ "bearer": [] } ], - "summary": "Run the search against the pinned session (equivalence: byte-identical to a plain search)", + "summary": "Stream the query prefix: warms the block cache + answers grounded suggestions", "tags": [ "search" ] } }, - "/v1/search/session/prefix": { + "/v1/search/suggest": { "post": { - "operationId": "post_v1_search_session_prefix", + "operationId": "post_v1_search_suggest", "parameters": [ { "description": "Graph name (default `main`)", @@ -43819,7 +45221,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SearchSessionPrefixRequest" + "$ref": "#/components/schemas/SearchSuggestRequest" } } }, @@ -43830,7 +45232,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SearchSessionPrefixResponse" + "$ref": "#/components/schemas/SearchSuggestResponse" } } }, @@ -44048,15 +45450,15 @@ "bearer": [] } ], - "summary": "Stream the query prefix: warms the block cache + answers grounded suggestions", + "summary": "Grounded prefix completion from index vocabulary + ontology", "tags": [ "search" ] } }, - "/v1/search/suggest": { - "post": { - "operationId": "post_v1_search_suggest", + "/v1/search/vocab": { + "get": { + "operationId": "get_v1_search_vocab", "parameters": [ { "description": "Graph name (default `main`)", @@ -44086,31 +45488,30 @@ } }, { - "description": "Stable client-generated key for safely retrying mutations and supervision writes.", - "in": "header", - "name": "Idempotency-Key", + "description": "Comma-separated subset of terms,attribute_values,schema,entities (default all)", + "in": "query", + "name": "sections", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Per-section cap (default 50000, max 200000; hitting it sets truncated)", + "in": "query", + "name": "limit", "required": false, "schema": { "type": "string" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SearchSuggestRequest" - } - } - }, - "required": true - }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SearchSuggestResponse" + "$ref": "#/components/schemas/VocabExportResponse" } } }, @@ -44328,15 +45729,15 @@ "bearer": [] } ], - "summary": "Grounded prefix completion from index vocabulary + ontology", + "summary": "Grounding-vocabulary export: byte-sorted, deduped string sections (terms, attribute values, schema, entities) — decoder-side automaton input / export-bundle half", "tags": [ "search" ] } }, - "/v1/search/vocab": { + "/v1/signals": { "get": { - "operationId": "get_v1_search_vocab", + "operationId": "get_v1_signals", "parameters": [ { "description": "Graph name (default `main`)", @@ -44366,16 +45767,25 @@ } }, { - "description": "Comma-separated subset of terms,attribute_values,schema,entities (default all)", + "description": "Inclusive lower flush seq (default 0)", "in": "query", - "name": "sections", + "name": "from", "required": false, "schema": { "type": "string" } }, { - "description": "Per-section cap (default 50000, max 200000; hitting it sets truncated)", + "description": "Inclusive upper flush seq (default unbounded)", + "in": "query", + "name": "to", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Max signals returned (default 500, max 5000)", "in": "query", "name": "limit", "required": false, @@ -44389,7 +45799,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/VocabExportResponse" + "$ref": "#/components/schemas/SignalReadResponse" } } }, @@ -44607,15 +46017,13 @@ "bearer": [] } ], - "summary": "Grounding-vocabulary export: byte-sorted, deduped string sections (terms, attribute values, schema, entities) — decoder-side automaton input / export-bundle half", + "summary": "Read captured signals by flush-seq range, oldest first — the flywheel training feed; the seq is the temporal-split coordinate", "tags": [ - "search" + "system" ] - } - }, - "/v1/signals": { - "get": { - "operationId": "get_v1_signals", + }, + "post": { + "operationId": "post_v1_signals", "parameters": [ { "description": "Graph name (default `main`)", @@ -44645,39 +46053,31 @@ } }, { - "description": "Inclusive lower flush seq (default 0)", - "in": "query", - "name": "from", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "Inclusive upper flush seq (default unbounded)", - "in": "query", - "name": "to", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "Max signals returned (default 500, max 5000)", - "in": "query", - "name": "limit", - "required": false, + "description": "Stable client-generated key for safely retrying mutations and supervision writes.", + "in": "header", + "name": "Idempotency-Key", + "required": true, "schema": { "type": "string" } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SignalIngestRequest" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignalReadResponse" + "$ref": "#/components/schemas/SignalIngestResponse" } } }, @@ -44895,13 +46295,15 @@ "bearer": [] } ], - "summary": "Read captured signals by flush-seq range, oldest first — the flywheel training feed; the seq is the temporal-split coordinate", + "summary": "Ingest flywheel signals (append-only; requires --enable-signals)", "tags": [ "system" ] - }, - "post": { - "operationId": "post_v1_signals", + } + }, + "/v1/status": { + "get": { + "operationId": "get_v1_status", "parameters": [ { "description": "Graph name (default `main`)", @@ -44929,33 +46331,14 @@ "schema": { "type": "string" } - }, - { - "description": "Stable client-generated key for safely retrying mutations and supervision writes.", - "in": "header", - "name": "Idempotency-Key", - "required": true, - "schema": { - "type": "string" - } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SignalIngestRequest" - } - } - }, - "required": true - }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SignalIngestResponse" + "type": "object" } } }, @@ -45173,15 +46556,15 @@ "bearer": [] } ], - "summary": "Ingest flywheel signals (append-only; requires --enable-signals)", + "summary": "Server, graph, and persisted-index status", "tags": [ "system" ] } }, - "/v1/status": { + "/v1/storage/objects": { "get": { - "operationId": "get_v1_status", + "operationId": "get_v1_storage_objects", "parameters": [ { "description": "Graph name (default `main`)", @@ -45209,6 +46592,33 @@ "schema": { "type": "string" } + }, + { + "description": "Key prefix", + "in": "query", + "name": "prefix", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Max keys", + "in": "query", + "name": "limit", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Opaque cursor from the previous page", + "in": "query", + "name": "cursor", + "required": false, + "schema": { + "type": "string" + } } ], "responses": { @@ -45434,77 +46844,22 @@ "bearer": [] } ], - "summary": "Server, graph, and persisted-index status", + "summary": "List graph-scoped object keys for inspection", "tags": [ "system" ] } }, - "/v1/storage/objects": { + "/version": { "get": { - "operationId": "get_v1_storage_objects", - "parameters": [ - { - "description": "Graph name (default `main`)", - "in": "query", - "name": "graph", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "Branch name (default `main`)", - "in": "query", - "name": "branch", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "API contract version to pin. Use `2026-07-23` for this beta-breaking shape.", - "in": "header", - "name": "Lbb-Version", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "Key prefix", - "in": "query", - "name": "prefix", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "Max keys", - "in": "query", - "name": "limit", - "required": false, - "schema": { - "type": "string" - } - }, - { - "description": "Opaque cursor from the previous page", - "in": "query", - "name": "cursor", - "required": false, - "schema": { - "type": "string" - } - } - ], + "operationId": "get_version", + "parameters": [], "responses": { "200": { "content": { "application/json": { "schema": { - "type": "object" + "$ref": "#/components/schemas/VersionResponse" } } }, @@ -45717,12 +47072,8 @@ } } }, - "security": [ - { - "bearer": [] - } - ], - "summary": "List graph-scoped object keys for inspection", + "security": [], + "summary": "Read build identity, wire capabilities, and persisted-format fingerprint", "tags": [ "system" ] diff --git a/lbb/__init__.py b/lbb/__init__.py index 16bfd59..6633823 100644 --- a/lbb/__init__.py +++ b/lbb/__init__.py @@ -12,6 +12,7 @@ from .client import ( AsyncLbbClient, IndexLineageObservation, + LbbCapabilityError, LbbClient, LbbError, ListPage, @@ -26,6 +27,7 @@ "LbbClient", "AsyncLbbClient", "LbbError", + "LbbCapabilityError", "IndexLineageObservation", "ListPage", "RawLbbResponse", diff --git a/lbb/_async_client.py b/lbb/_async_client.py index 8fa5885..d217d6f 100644 --- a/lbb/_async_client.py +++ b/lbb/_async_client.py @@ -4,7 +4,15 @@ import asyncio import inspect -from collections.abc import Callable, Mapping, Sequence +import json +from collections.abc import ( + AsyncIterable, + AsyncIterator, + Callable, + Iterable, + Mapping, + Sequence, +) from typing import Any, cast import httpx @@ -17,6 +25,7 @@ DEFAULT_TIMEOUT, Body, IndexLineageObservation, + LbbCapabilityError, ListPage, ModelT, RawLbbResponse, @@ -42,6 +51,32 @@ _SchemaNamespace, ) +AsyncImportItem = Mapping[str, Any] | str | bytes +AsyncImportSource = ( + AsyncIterable[AsyncImportItem] | Iterable[AsyncImportItem] | str | bytes +) + + +def _import_bytes(line: AsyncImportItem) -> bytes: + if isinstance(line, bytes): + encoded = line + elif isinstance(line, str): + encoded = line.encode() + else: + encoded = json.dumps(line, separators=(",", ":")).encode() + return encoded if encoded.endswith(b"\n") else encoded + b"\n" + + +async def _aiter_import_ndjson(lines: AsyncImportSource) -> AsyncIterator[bytes]: + if isinstance(lines, (str, bytes)): + yield _import_bytes(lines) + elif isinstance(lines, AsyncIterable): + async for line in lines: + yield _import_bytes(line) + else: + for line in lines: + yield _import_bytes(line) + class _AsyncContextNamespace(_ContextNamespace): async def suggest( @@ -71,6 +106,7 @@ async def groundability( await super().groundability(sample=sample, options=options), ) + class _AsyncOntologyNamespace(_OntologyNamespace): async def view( self, *, counts: bool = False, options: RequestOptions | None = None @@ -218,6 +254,7 @@ async def create_model( await super().create_model(body, idempotency_key=idempotency_key), ) + class _AsyncSchemaNamespace(_SchemaNamespace): async def view_model(self) -> models.SchemaBundleView: return cast(models.SchemaBundleView, await super().view_model()) @@ -353,6 +390,7 @@ async def retract_model( await super().retract_model(body, idempotency_key=idempotency_key), ) + class _AsyncEntityNamespace(_EntityNamespace): async def sample( self, @@ -374,6 +412,7 @@ async def filter_by_attributes_model( await super().filter_by_attributes_model(**kwargs), ) + class AsyncLbbClient(_BaseLbbClient): """Asynchronous client. Usable as an async context manager.""" @@ -420,6 +459,91 @@ def __init__( self._http = httpx.AsyncClient( timeout=timeout, transport=transport, event_hooks=event_hooks ) + self._capabilities: set[str] | None = None + + async def _require_capability(self, capability: str) -> None: + if self._capabilities is None: + response = (await self.raw_request("GET", "/version")).data + advertised = ( + response.get("capabilities", []) + if isinstance(response, Mapping) + else [] + ) + self._capabilities = {str(item) for item in advertised} + if capability not in self._capabilities: + raise LbbCapabilityError(capability) + + async def submit_import_ndjson( + self, + lines: AsyncImportSource, + *, + idempotency_key: str, + batch: int | None = None, + strict: bool | None = None, + observed_at: str | None = None, + ) -> models.GraphImportJobAccepted: + """Stream NDJSON once and enqueue a durable import job.""" + if not idempotency_key.strip(): + raise ValueError( + "submit_import_ndjson requires a non-empty idempotency_key" + ) + await self._require_capability("durable_import_jobs_v1") + return await self._model_request( + models.GraphImportJobAccepted, + "POST", + "/v1/graph/import-jobs", + params={"batch": batch, "strict": strict, "observed_at": observed_at}, + content=_aiter_import_ndjson(lines), + content_type="application/x-ndjson", + idempotency_key=idempotency_key, + options={"max_retries": 0, "retry": False}, + ) + + async def get_import_job(self, job_id: str) -> models.GraphImportJobStatus: + await self._require_capability("durable_import_jobs_v1") + return await self._model_request( + models.GraphImportJobStatus, + "GET", + "/v1/graph/import-jobs", + params={"job_id": job_id}, + ) + + async def cancel_import_job( + self, job_id: str + ) -> models.GraphImportJobCancelResponse: + await self._require_capability("durable_import_jobs_v1") + return await self._model_request( + models.GraphImportJobCancelResponse, + "DELETE", + "/v1/graph/import-jobs", + params={"job_id": job_id}, + ) + + async def wait_for_import_job( + self, + job_id: str, + *, + timeout: float | None = None, + poll_interval: float = 1.0, + ) -> models.GraphImportJobStatus: + if poll_interval < 0: + raise ValueError("poll_interval must be non-negative") + if timeout is not None and timeout < 0: + raise ValueError("timeout must be non-negative") + loop = asyncio.get_running_loop() + deadline = loop.time() + timeout if timeout is not None else None + terminal = { + models.GraphImportJobState.succeeded, + models.GraphImportJobState.failed, + models.GraphImportJobState.cancelled, + } + while True: + status = await self.get_import_job(job_id) + if status.state in terminal: + return status + if deadline is not None and loop.time() >= deadline: + raise TimeoutError(f"timed out waiting for durable import job {job_id}") + await asyncio.sleep(poll_interval) def graph(self, name: str, *, branch: str | None = None) -> _AsyncGraphNamespace: return _AsyncGraphNamespace(self, name, branch) @@ -439,9 +563,7 @@ async def delete_branch(self, *, confirm: str) -> models.GraphBranchDeleteRespon ) async def fork_graph(self, src: str, dst: str) -> models.GraphForkResponse: - return cast( - models.GraphForkResponse, await super().fork_graph(src, dst) - ) + return cast(models.GraphForkResponse, await super().fork_graph(src, dst)) async def reload( self, @@ -700,7 +822,7 @@ async def raw_request( *, params: Mapping[str, Any] | None = None, body: Body | None = None, - content: str | None = None, + content: Any | None = None, content_type: str | None = None, idempotency_key: str | None = None, options: RequestOptions | None = None, @@ -788,7 +910,7 @@ async def _request( *, params: Mapping[str, Any] | None = None, body: Body | None = None, - content: str | None = None, + content: Any | None = None, content_type: str | None = None, idempotency_key: str | None = None, options: RequestOptions | None = None, @@ -813,7 +935,7 @@ async def _model_request( *, params: Mapping[str, Any] | None = None, body: Body | None = None, - content: str | None = None, + content: Any | None = None, content_type: str | None = None, idempotency_key: str | None = None, options: RequestOptions | None = None, diff --git a/lbb/_client_base.py b/lbb/_client_base.py index 384d19c..cc91511 100644 --- a/lbb/_client_base.py +++ b/lbb/_client_base.py @@ -93,6 +93,17 @@ def __init__( ) +class LbbCapabilityError(RuntimeError): + """Raised before upload when the server lacks a required additive capability.""" + + def __init__(self, capability: str) -> None: + self.capability = capability + super().__init__( + f"Little Big Brain server does not advertise {capability}; " + "upgrade the server before using this SDK method" + ) + + def _endpoint_migration_hint(code: str | None) -> str | None: if code == "stack_endpoint_required": return "Copy endpoint_url from the stack's Connect page and use it as base_url." @@ -567,7 +578,7 @@ def _request_kwargs( *, params: Mapping[str, Any] | None, body: Body | None, - content: str | None, + content: Any | None, content_type: str | None, idempotency_key: str | None, headers: Mapping[str, str] | None = None, @@ -626,7 +637,7 @@ def raw_request( *, params: Mapping[str, Any] | None = None, body: Body | None = None, - content: str | None = None, + content: Any | None = None, content_type: str | None = None, idempotency_key: str | None = None, options: RequestOptions | None = None, @@ -640,7 +651,7 @@ def _request( *, params: Mapping[str, Any] | None = None, body: Body | None = None, - content: str | None = None, + content: Any | None = None, content_type: str | None = None, idempotency_key: str | None = None, options: RequestOptions | None = None, @@ -1492,9 +1503,7 @@ def ontology_conformance(self, *, consistency: str | None = None) -> Any: provenance to match current head; it never runs validation inline. """ params = self._consistency_params(consistency, None) - return self._request( - "GET", "/v1/ontology/conformance", params=params or None - ) + return self._request("GET", "/v1/ontology/conformance", params=params or None) def ontology_conformance_model( self, *, consistency: str | None = None @@ -1620,7 +1629,7 @@ def _model_request( *, params: Mapping[str, Any] | None = None, body: Body | None = None, - content: str | None = None, + content: Any | None = None, content_type: str | None = None, idempotency_key: str | None = None, options: RequestOptions | None = None, @@ -1815,6 +1824,7 @@ def retract_model( idempotency_key=idempotency_key or self._client.idempotency_key("retract"), ) + class _FactsNamespace: def __init__(self, client: _BaseLbbClient, graph: str, branch: str | None) -> None: self._client = client @@ -2034,6 +2044,7 @@ def groundability( options=_read_options(options), ) + class _OntologyNamespace: """Typed ontology discovery and lifecycle operations.""" @@ -2227,6 +2238,7 @@ def conflicts( options=_read_options(options), ) + class _SchemaNamespace: """Active ontology/SHACL bundle metadata and atomic publication.""" @@ -2238,13 +2250,9 @@ def view(self) -> Any: return self._client._request("GET", "/v1/schema") def view_model(self) -> models.SchemaBundleView: - return self._client._model_request( - models.SchemaBundleView, "GET", "/v1/schema" - ) + return self._client._model_request(models.SchemaBundleView, "GET", "/v1/schema") - def publish( - self, body: Body, *, idempotency_key: str | None = None - ) -> Any: + def publish(self, body: Body, *, idempotency_key: str | None = None) -> Any: """Atomically publish a bundle; conformance is produced asynchronously.""" return self._client._request( "POST", diff --git a/lbb/_sync_client.py b/lbb/_sync_client.py index 71f2e2b..32dc782 100644 --- a/lbb/_sync_client.py +++ b/lbb/_sync_client.py @@ -2,8 +2,9 @@ from __future__ import annotations +import json import time -from collections.abc import Callable, Mapping +from collections.abc import Callable, Iterable, Iterator, Mapping from typing import Any, cast import httpx @@ -16,6 +17,7 @@ DEFAULT_TIMEOUT, Body, IndexLineageObservation, + LbbCapabilityError, ListPage, ModelT, RawLbbResponse, @@ -39,12 +41,29 @@ _SchemaNamespace, ) +ImportItem = Mapping[str, Any] | str | bytes +ImportSource = Iterable[ImportItem] | str | bytes + + +def _iter_import_ndjson(lines: ImportSource) -> Iterator[bytes]: + source: Iterable[ImportItem] = [lines] if isinstance(lines, (str, bytes)) else lines + for line in source: + if isinstance(line, bytes): + encoded = line + elif isinstance(line, str): + encoded = line.encode() + else: + encoded = json.dumps(line, separators=(",", ":")).encode() + yield encoded if encoded.endswith(b"\n") else encoded + b"\n" + class _SyncContextNamespace(_ContextNamespace): def suggest( self, body: Body, *, options: RequestOptions | None = None ) -> models.SearchSuggestResponse: - return cast(models.SearchSuggestResponse, super().suggest(body, options=options)) + return cast( + models.SearchSuggestResponse, super().suggest(body, options=options) + ) def resolve( self, body: Body, *, options: RequestOptions | None = None @@ -64,6 +83,7 @@ def groundability( super().groundability(sample=sample, options=options), ) + class _SyncOntologyNamespace(_OntologyNamespace): def view( self, *, counts: bool = False, options: RequestOptions | None = None @@ -84,12 +104,16 @@ def conformance( def search( self, body: Body, *, options: RequestOptions | None = None ) -> models.OntologySearchResponse: - return cast(models.OntologySearchResponse, super().search(body, options=options)) + return cast( + models.OntologySearchResponse, super().search(body, options=options) + ) def resolve( self, body: Body, *, options: RequestOptions | None = None ) -> models.OntologyResolveResponse: - return cast(models.OntologyResolveResponse, super().resolve(body, options=options)) + return cast( + models.OntologyResolveResponse, super().resolve(body, options=options) + ) def define(self, body: Body) -> models.OntologyDefineResponse: return cast(models.OntologyDefineResponse, super().define(body)) @@ -149,7 +173,10 @@ def sparql( def analytics( self, body: Body, *, options: RequestOptions | None = None ) -> models.AnalyticQueryResponse: - return cast(models.AnalyticQueryResponse, super().analytics(body, options=options)) + return cast( + models.AnalyticQueryResponse, super().analytics(body, options=options) + ) + class LbbClient(_BaseLbbClient): """Synchronous client. Usable as a context manager.""" @@ -194,7 +221,96 @@ def __init__( self.ontology = _SyncOntologyNamespace(self) self.query = _SyncQueryNamespace(self) self.schema = _SchemaNamespace(self) - self._http = httpx.Client(timeout=timeout, transport=transport, event_hooks=event_hooks) + self._http = httpx.Client( + timeout=timeout, transport=transport, event_hooks=event_hooks + ) + self._capabilities: set[str] | None = None + + def _require_capability(self, capability: str) -> None: + if self._capabilities is None: + response = self.raw_request("GET", "/version").data + advertised = ( + response.get("capabilities", []) + if isinstance(response, Mapping) + else [] + ) + self._capabilities = {str(item) for item in advertised} + if capability not in self._capabilities: + raise LbbCapabilityError(capability) + + def submit_import_ndjson( + self, + lines: ImportSource, + *, + idempotency_key: str, + batch: int | None = None, + strict: bool | None = None, + observed_at: str | None = None, + ) -> models.GraphImportJobAccepted: + """Stream NDJSON once and enqueue a durable import job. + + Automatic transport retries are disabled because an arbitrary iterator + may be one-shot. Reinvoke with a fresh iterable and the same explicit + key for an idempotent replay. + """ + if not idempotency_key.strip(): + raise ValueError( + "submit_import_ndjson requires a non-empty idempotency_key" + ) + self._require_capability("durable_import_jobs_v1") + return self._model_request( + models.GraphImportJobAccepted, + "POST", + "/v1/graph/import-jobs", + params={"batch": batch, "strict": strict, "observed_at": observed_at}, + content=_iter_import_ndjson(lines), + content_type="application/x-ndjson", + idempotency_key=idempotency_key, + options={"max_retries": 0, "retry": False}, + ) + + def get_import_job(self, job_id: str) -> models.GraphImportJobStatus: + self._require_capability("durable_import_jobs_v1") + return self._model_request( + models.GraphImportJobStatus, + "GET", + "/v1/graph/import-jobs", + params={"job_id": job_id}, + ) + + def cancel_import_job(self, job_id: str) -> models.GraphImportJobCancelResponse: + self._require_capability("durable_import_jobs_v1") + return self._model_request( + models.GraphImportJobCancelResponse, + "DELETE", + "/v1/graph/import-jobs", + params={"job_id": job_id}, + ) + + def wait_for_import_job( + self, + job_id: str, + *, + timeout: float | None = None, + poll_interval: float = 1.0, + ) -> models.GraphImportJobStatus: + if poll_interval < 0: + raise ValueError("poll_interval must be non-negative") + if timeout is not None and timeout < 0: + raise ValueError("timeout must be non-negative") + deadline = time.monotonic() + timeout if timeout is not None else None + terminal = { + models.GraphImportJobState.succeeded, + models.GraphImportJobState.failed, + models.GraphImportJobState.cancelled, + } + while True: + status = self.get_import_job(job_id) + if status.state in terminal: + return status + if deadline is not None and time.monotonic() >= deadline: + raise TimeoutError(f"timed out waiting for durable import job {job_id}") + time.sleep(poll_interval) def raw_request( self, @@ -203,7 +319,7 @@ def raw_request( *, params: Mapping[str, Any] | None = None, body: Body | None = None, - content: str | None = None, + content: Any | None = None, content_type: str | None = None, idempotency_key: str | None = None, options: RequestOptions | None = None, @@ -220,7 +336,9 @@ def raw_request( if "timeout" in request_options: kwargs["timeout"] = request_options["timeout"] response: httpx.Response | None = None - can_retry = request_options.get("retry", _retry_allowed(method, idempotency_key)) + can_retry = request_options.get( + "retry", _retry_allowed(method, idempotency_key) + ) max_retries = request_options.get("max_retries", self._max_retries) if max_retries < 0: raise ValueError("max_retries must be non-negative") @@ -232,7 +350,9 @@ def raw_request( for attempt in range(max_retries + 1): attempts = attempt + 1 try: - response = self._http.request(method, f"{self._base_url}{path}", **kwargs) + response = self._http.request( + method, f"{self._base_url}{path}", **kwargs + ) except httpx.RequestError: if not (can_retry and attempt < max_retries): raise @@ -286,7 +406,7 @@ def _request( *, params: Mapping[str, Any] | None = None, body: Body | None = None, - content: str | None = None, + content: Any | None = None, content_type: str | None = None, idempotency_key: str | None = None, options: RequestOptions | None = None, @@ -310,7 +430,7 @@ def _model_request( *, params: Mapping[str, Any] | None = None, body: Body | None = None, - content: str | None = None, + content: Any | None = None, content_type: str | None = None, idempotency_key: str | None = None, options: RequestOptions | None = None, diff --git a/lbb/_version.py b/lbb/_version.py index 04d5cb5..b4a88f5 100644 --- a/lbb/_version.py +++ b/lbb/_version.py @@ -1,3 +1,3 @@ """Package version shared by build metadata and runtime telemetry.""" -__version__ = "0.8.1" +__version__ = "0.9.0" diff --git a/lbb/client.py b/lbb/client.py index 2297785..8b4a82e 100644 --- a/lbb/client.py +++ b/lbb/client.py @@ -7,6 +7,7 @@ from ._async_client import AsyncLbbClient from ._client_base import ( IndexLineageObservation, + LbbCapabilityError, LbbError, ListPage, RawLbbResponse, @@ -20,6 +21,7 @@ "LbbClient", "AsyncLbbClient", "LbbError", + "LbbCapabilityError", "IndexLineageObservation", "ListPage", "RawLbbResponse", diff --git a/lbb/models.py b/lbb/models.py index 4bac8ff..1374a35 100644 --- a/lbb/models.py +++ b/lbb/models.py @@ -809,6 +809,36 @@ class GraphForkResponse(BaseModel): src_graph_id: str +class GraphImportJobFailure(BaseModel): + """ + Terminal failure details for a durable import. The message is safe to expose + to the submitting client; internal object keys and worker identities stay in + structured server logs. + """ + + code: str + message: str + retryable: bool + + +class GraphImportJobState(Enum): + """ + Lifecycle of a durable NDJSON import submitted through + `POST /v1/graph/import-jobs`. + + A succeeded import has durably committed its records and enqueued the final + publication job. It does **not** mean the published read generation has + already caught up to [`GraphImportJobStatus::committed_commit_seq`]. + """ + + queued = 'queued' + running = 'running' + cancellation_requested = 'cancellation_requested' + cancelled = 'cancelled' + succeeded = 'succeeded' + failed = 'failed' + + class GraphImportLineError(BaseModel): """ A per-line parse/commit failure surfaced by the bulk importer. `line` is the @@ -3812,6 +3842,34 @@ class VectorMetric(Enum): l2 = 'l2' +class VersionResponse(BaseModel): + """ + Unauthenticated build, wire-capability, and persisted-format identity. + """ + + built_at: Annotated[ + str, Field(description='UTC build timestamp baked into the running binary.') + ] + capabilities: Annotated[ + list[str], + Field( + description='Fine-grained additive features safe for SDK capability gates.' + ), + ] + git_commit: Annotated[ + str, Field(description='Source revision baked into the running binary.') + ] + persisted_format: Annotated[ + str, + Field( + description='Deterministic fingerprint of all persisted format versions.' + ), + ] + version: Annotated[ + str, Field(description='Backward-compatibility date sent in `Lbb-Version`.') + ] + + class VocabExportResponse(BaseModel): """ `GET /v1/search/vocab` — the graph's grounding vocabulary as sorted, @@ -4490,6 +4548,48 @@ class GraphEdgeRow(BaseModel): valid_time: ValidTime +class GraphImportJobAccepted(BaseModel): + """ + Stable response returned when a durable import is accepted or replayed. + """ + + idempotent_replay: Annotated[ + bool, + Field( + description='True when this request resolved to an existing job with the same\nidempotency key and exact content hash.' + ), + ] + job_id: str + state: GraphImportJobState + upload_bytes: Annotated[int, Field(ge=0)] + + +class GraphImportJobCancelResponse(BaseModel): + """ + Response to `DELETE /v1/graph/import-jobs`. + """ + + job_id: str + state: GraphImportJobState + + +class GraphImportJobProgress(BaseModel): + """ + Bounded progress persisted by an import worker after every grouped commit. + """ + + bytes_processed: Annotated[int, Field(ge=0)] + committed_commit_seq: CommitSeq | None = None + error_count: Annotated[int, Field(ge=0)] + errors: list[GraphImportLineError] | None = None + groups_committed: Annotated[int, Field(ge=0)] + lines_read: Annotated[int, Field(ge=0)] + observations: Annotated[int, Field(ge=0)] + properties: Annotated[int, Field(ge=0)] + triplets: Annotated[int, Field(ge=0)] + upload_bytes: Annotated[int, Field(ge=0)] + + class GraphImportPublishedGenerationOutcome(BaseModel): """ Durable publication accepted after `POST /v1/graph/import`. @@ -6403,6 +6503,21 @@ class GraphExportResponse(BaseModel): ] +class GraphImportJobStatus(BaseModel): + """ + Queryable status for a durable import job. + """ + + committed_commit_seq: CommitSeq | None = None + enqueued_at_micros: int + failure: GraphImportJobFailure | None = None + job_id: str + progress: GraphImportJobProgress + publication_job: GraphImportPublishedGenerationOutcome | None = None + state: GraphImportJobState + updated_at_micros: int + + class GraphImportLine(RootModel[TripletInput | EntityPropertiesInput]): root: Annotated[ TripletInput | EntityPropertiesInput, diff --git a/tests/test_client.py b/tests/test_client.py index 9e08f46..25a8490 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -10,7 +10,7 @@ import httpx from pydantic import ValidationError -from lbb import AsyncLbbClient, LbbClient, LbbError, __version__ +from lbb import AsyncLbbClient, LbbCapabilityError, LbbClient, LbbError, __version__ from lbb.models import ( AddEntityTypeOp, AdditiveOntologyEvolveRequest, @@ -98,6 +98,7 @@ def edge_list_payload() -> dict[str, Any]: "total_count": 1, } + def schema_view_payload() -> dict[str, Any]: return { "graph": GRAPH, @@ -180,6 +181,53 @@ def handler(request: httpx.Request) -> httpx.Response: class SyncClientTests(unittest.TestCase): + def test_durable_import_capability_gates_and_streams(self) -> None: + seen: list[httpx.Request] = [] + produced = 0 + + def lines() -> Any: + nonlocal produced + produced += 1 + yield {"type": "Service", "name": "api", "properties": {}} + produced += 1 + yield b'{"type":"Service","name":"db","properties":{}}' + + def handler(request: httpx.Request) -> httpx.Response: + seen.append(request) + if request.url.path == "/version": + return httpx.Response( + 200, json={"capabilities": ["durable_import_jobs_v1"]} + ) + request.read() + return httpx.Response( + 202, + json={ + "job_id": "import:1", + "state": "queued", + "idempotent_replay": False, + "upload_bytes": len(request.content), + }, + ) + + with LbbClient("http://h", transport=httpx.MockTransport(handler)) as client: + accepted = client.submit_import_ndjson(lines(), idempotency_key="source:1") + + self.assertEqual(accepted.job_id, "import:1") + self.assertEqual(produced, 2) + self.assertEqual(seen[1].headers["idempotency-key"], "source:1") + self.assertEqual(seen[1].headers["content-type"], "application/x-ndjson") + self.assertEqual(len(seen[1].content.splitlines()), 2) + + def test_durable_import_does_not_fallback_without_capability(self) -> None: + seen: list[httpx.Request] = [] + with LbbClient( + "http://h", + transport=capturing_transport(seen, {"json": {"capabilities": []}}), + ) as client: + with self.assertRaises(LbbCapabilityError): + client.submit_import_ndjson([], idempotency_key="source:2") + self.assertEqual([request.url.path for request in seen], ["/version"]) + def test_metadata_exposes_only_bounded_index_detail_option(self) -> None: seen: list[httpx.Request] = [] with LbbClient( @@ -524,9 +572,7 @@ def test_schema_namespace_returns_typed_models(self) -> None: published = client.schema.publish_model( { "desired_mode": "warn", - "shapes": { - "source": "@prefix sh: ." - }, + "shapes": {"source": "@prefix sh: ."}, }, idempotency_key="schema-1", ) @@ -1891,6 +1937,45 @@ def test_sync_scoped_backfill_exposes_detached_job_control(self) -> None: class AsyncClientTests(unittest.IsolatedAsyncioTestCase): + async def test_async_durable_import_streams_async_iterable(self) -> None: + seen: list[httpx.Request] = [] + produced = 0 + + async def lines() -> Any: + nonlocal produced + produced += 1 + yield {"type": "Service", "name": "api", "properties": {}} + produced += 1 + yield b'{"type":"Service","name":"db","properties":{}}' + + async def handler(request: httpx.Request) -> httpx.Response: + seen.append(request) + if request.url.path == "/version": + return httpx.Response( + 200, json={"capabilities": ["durable_import_jobs_v1"]} + ) + await request.aread() + return httpx.Response( + 202, + json={ + "job_id": "import:async", + "state": "queued", + "idempotent_replay": False, + "upload_bytes": len(request.content), + }, + ) + + async with AsyncLbbClient( + "http://h", transport=httpx.MockTransport(handler) + ) as client: + accepted = await client.submit_import_ndjson( + lines(), idempotency_key="source:async" + ) + + self.assertEqual(accepted.job_id, "import:async") + self.assertEqual(produced, 2) + self.assertEqual(len(seen[1].content.splitlines()), 2) + async def test_async_create_graph_returns_typed_response(self) -> None: payload = {"commit_seq": 0, "graph": GRAPH, "ontology_version": 1} async with AsyncLbbClient( diff --git a/tests/test_public_api.py b/tests/test_public_api.py index b7b769b..151ddff 100644 --- a/tests/test_public_api.py +++ b/tests/test_public_api.py @@ -13,6 +13,7 @@ def test_public_exports_are_explicit_and_stable() -> None: "LbbClient", "AsyncLbbClient", "LbbError", + "LbbCapabilityError", "IndexLineageObservation", "ListPage", "RawLbbResponse", @@ -25,7 +26,7 @@ def test_public_exports_are_explicit_and_stable() -> None: def test_package_version_and_primary_clients_are_available() -> None: - assert lbb.__version__ == "0.8.1" + assert lbb.__version__ == "0.9.0" try: distribution_version = version("littlebigbrain") except PackageNotFoundError: @@ -42,6 +43,7 @@ def test_client_module_keeps_the_documented_import_surface() -> None: assert lbb.client.LbbClient is lbb.LbbClient assert lbb.client.AsyncLbbClient is lbb.AsyncLbbClient assert lbb.client.LbbError is lbb.LbbError + assert lbb.client.LbbCapabilityError is lbb.LbbCapabilityError assert lbb.client.ListPage is lbb.ListPage assert lbb.client.RawLbbResponse is lbb.RawLbbResponse assert lbb.client.RequestOptions is lbb.RequestOptions