From 1e21f859e68f9b7225fbc3106ad74b5e41ce3dcd Mon Sep 17 00:00:00 2001 From: Doug Rennehan Date: Sun, 5 Jul 2026 12:23:13 -0400 Subject: [PATCH 1/2] =?UTF-8?q?feat(node):=20boundary=20language=20flag=20?= =?UTF-8?q?=E2=80=94=20--language=20/=20--clear-language?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Syncs openapi.json (boundary `language` field, h2o T0.1) and regenerates the wire crate (node_data/wire_node_data gain `language: Option>`), then mirrors path_prefix → language across the hand-written layer for full CRUD parity with /v1: - cli.rs: `--language` on node add + set; `--clear-language` (conflicts_with). - staging: NodeSpec/NodeEdit/NodeUpdated + OpSummary carry language; add/update delta build via scalar_or_clear; boundary-only guards reject it on behavior/state/io (mirrored error text). - cmd/node.rs + diff.rs: human + JSON rendering of language. - 4 new tests: add sets language, state/io reject it, set + clear round-trip. cargo build + clippy -D warnings clean; 292 lib + 7 help tests pass. Wire regen is idempotent against the pinned 7.18.0 generator (verified: regen on main's own openapi produces zero drift, so CI wire-drift will pass). Co-Authored-By: Claude Opus 4.8 (1M context) --- openapi.json | 2 +- src/cli.rs | 12 ++++ src/cmd/diff.rs | 11 +++ src/cmd/node.rs | 13 ++++ src/staging/mod.rs | 112 ++++++++++++++++++++++++++++-- wire/src/models/node_data.rs | 3 + wire/src/models/wire_node_data.rs | 3 + 7 files changed, 149 insertions(+), 7 deletions(-) diff --git a/openapi.json b/openapi.json index 79757b2..6517bae 100644 --- a/openapi.json +++ b/openapi.json @@ -1 +1 @@ -{"components": {"responses": {"Forbidden": {"content": {"application/json": {"schema": {"properties": {"detail": {"properties": {"code": {"type": "string"}, "message": {"type": "string"}}, "type": "object"}}, "type": "object"}}}, "description": "Credentials are valid but lack the scope required for this route, or the principal lacks membership in the target project."}, "NotFound": {"content": {"application/json": {"schema": {"properties": {"detail": {"properties": {"code": {"example": "not_found", "type": "string"}, "message": {"type": "string"}}, "type": "object"}}, "type": "object"}}}, "description": "Resource not found OR not accessible to this principal. Leak parity: the response is identical in both cases so an attacker cannot enumerate resources via 404-vs-403 timing."}, "RateLimited": {"content": {"application/json": {"schema": {"properties": {"detail": {"example": "rate_limited", "type": "string"}}, "type": "object"}}}, "description": "Per-bucket rate limit exceeded. The response carries ``Retry-After`` and the standard ``X-RateLimit-*`` headers (Limit / Remaining / Reset).", "headers": {"Retry-After": {"schema": {"type": "integer"}}, "X-RateLimit-Limit": {"schema": {"type": "integer"}}, "X-RateLimit-Remaining": {"schema": {"type": "integer"}}, "X-RateLimit-Reset": {"schema": {"type": "integer"}}}}, "Unauthenticated": {"content": {"application/json": {"schema": {"properties": {"detail": {"example": "unauthenticated", "type": "string"}}, "required": ["detail"], "type": "object"}}}, "description": "No credentials, malformed credentials, or revoked credentials. The envelope is leak-parity (same shape across all 401 paths) so an attacker cannot distinguish revoked vs. unknown via the body."}, "Unprocessable": {"content": {"application/json": {"schema": {"properties": {"detail": {"oneOf": [{"type": "string"}, {"properties": {"delta_ordinal": {"type": "integer"}, "delta_type": {"type": ["string", "null"]}, "error": {"type": "string"}, "field": {"type": ["string", "null"]}, "reason": {"type": ["string", "null"]}, "recoverable": {"type": "boolean"}, "recovery_hint": {"type": ["string", "null"]}}, "type": "object"}]}}, "type": "object"}}}, "description": "Request body failed validation. For delta routes the response carries ``error``, ``delta_ordinal``, ``delta_type``, ``field``, ``reason``, ``recoverable``, and ``recovery_hint`` so a client can auto-recover or surface a typed error."}, "VersionConflict": {"content": {"application/json": {"schema": {"properties": {"detail": {"properties": {"current_version": {"type": "integer"}, "error": {"example": "version_conflict", "type": "string"}}, "type": "object"}}, "type": "object"}}}, "description": "Optimistic concurrency check failed \u2014 the branch's current version is not the one the client sent in ``expected_version``. The response carries ``current_version`` so the client can refetch + retry."}}, "schemas": {"AddEdgeDelta": {"description": "Insert an edge.", "properties": {"edge": {"$ref": "#/components/schemas/Edge"}, "type": {"const": "add_edge", "title": "Type", "type": "string"}}, "required": ["type", "edge"], "title": "AddEdgeDelta", "type": "object"}, "AddNodeDelta": {"description": "Insert a node.", "properties": {"node": {"$ref": "#/components/schemas/Node"}, "type": {"const": "add_node", "title": "Type", "type": "string"}}, "required": ["type", "node"], "title": "AddNodeDelta", "type": "object"}, "BoundaryResponse": {"additionalProperties": false, "properties": {"boundary": {"$ref": "#/components/schemas/WireNode"}, "branch": {"$ref": "#/components/schemas/BranchRef"}, "children": {"items": {"$ref": "#/components/schemas/WireNode"}, "title": "Children", "type": "array"}, "edges": {"items": {"$ref": "#/components/schemas/WireEdge"}, "title": "Edges", "type": "array"}, "project_id": {"format": "uuid", "title": "Project Id", "type": "string"}, "version": {"title": "Version", "type": "string"}}, "required": ["version", "project_id", "branch", "boundary", "children", "edges"], "title": "BoundaryResponse", "type": "object"}, "BranchCreateResponse": {"additionalProperties": false, "properties": {"branch": {"$ref": "#/components/schemas/BranchMeta"}, "project_id": {"format": "uuid", "title": "Project Id", "type": "string"}, "version": {"title": "Version", "type": "string"}}, "required": ["version", "project_id", "branch"], "title": "BranchCreateResponse", "type": "object"}, "BranchListResponse": {"additionalProperties": false, "properties": {"branches": {"items": {"$ref": "#/components/schemas/BranchMeta"}, "title": "Branches", "type": "array"}, "project_id": {"format": "uuid", "title": "Project Id", "type": "string"}, "version": {"title": "Version", "type": "string"}}, "required": ["version", "project_id", "branches"], "title": "BranchListResponse", "type": "object"}, "BranchMeta": {"additionalProperties": false, "properties": {"base_main_version": {"title": "Base Main Version", "type": "integer"}, "created_at": {"title": "Created At", "type": "string"}, "id": {"format": "uuid", "title": "Id", "type": "string"}, "is_main": {"title": "Is Main", "type": "boolean"}, "last_active_at": {"title": "Last Active At", "type": "string"}, "merged_at": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Merged At"}, "name": {"title": "Name", "type": "string"}, "owner_id": {"anyOf": [{"format": "uuid", "type": "string"}, {"type": "null"}], "title": "Owner Id"}, "project_id": {"format": "uuid", "title": "Project Id", "type": "string"}, "status": {"title": "Status", "type": "string"}, "version": {"title": "Version", "type": "integer"}}, "required": ["id", "project_id", "owner_id", "name", "is_main", "version", "base_main_version", "status", "last_active_at", "created_at", "merged_at"], "title": "BranchMeta", "type": "object"}, "BranchNotActiveBody": {"additionalProperties": false, "properties": {"current_status": {"title": "Current Status", "type": "string"}, "error": {"const": "branch_not_active", "title": "Error", "type": "string"}}, "required": ["error", "current_status"], "title": "BranchNotActiveBody", "type": "object"}, "BranchRef": {"additionalProperties": false, "properties": {"id": {"format": "uuid", "title": "Id", "type": "string"}, "version": {"title": "Version", "type": "integer"}}, "required": ["id", "version"], "title": "BranchRef", "type": "object"}, "DeleteEdgeDelta": {"description": "Remove an edge.", "properties": {"edge_id": {"format": "uuid", "title": "Edge Id", "type": "string"}, "type": {"const": "delete_edge", "title": "Type", "type": "string"}}, "required": ["type", "edge_id"], "title": "DeleteEdgeDelta", "type": "object"}, "DeleteNodeDelta": {"description": "Cascade-delete a node and its descendant subtree.", "properties": {"node_id": {"format": "uuid", "title": "Node Id", "type": "string"}, "type": {"const": "delete_node", "title": "Type", "type": "string"}}, "required": ["type", "node_id"], "title": "DeleteNodeDelta", "type": "object"}, "DeltaApplyResponse": {"additionalProperties": false, "properties": {"applied": {"title": "Applied", "type": "boolean"}, "branch": {"$ref": "#/components/schemas/BranchRef"}, "delta_count": {"title": "Delta Count", "type": "integer"}, "positions_applied": {"anyOf": [{"type": "integer"}, {"type": "null"}], "default": null, "title": "Positions Applied"}, "project_id": {"format": "uuid", "title": "Project Id", "type": "string"}, "version": {"title": "Version", "type": "string"}}, "required": ["version", "project_id", "branch", "applied", "delta_count"], "title": "DeltaApplyResponse", "type": "object"}, "DeltaFieldErrorBody": {"additionalProperties": false, "description": "The 7-key per-delta envelope (always all keys present) the frontend\nauto-recovery consumes. Shared by every 422 delta error; `error`\ndiscriminates the cause.", "properties": {"delta_ordinal": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Delta Ordinal"}, "delta_type": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Delta Type"}, "error": {"enum": ["malformed_delta_field", "unknown_delta_type", "delta_not_object", "delta_too_large", "batch_too_large", "batch_count_exceeded"], "title": "Error", "type": "string"}, "field": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Field"}, "reason": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Reason"}, "recoverable": {"title": "Recoverable", "type": "boolean"}, "recovery_hint": {"anyOf": [{"$ref": "#/components/schemas/RecoveryHint"}, {"type": "null"}]}}, "required": ["error", "delta_ordinal", "delta_type", "field", "reason", "recoverable", "recovery_hint"], "title": "DeltaFieldErrorBody", "type": "object"}, "Edge": {"description": "An edge connecting two nodes' ports.\n\nEndpoints are given as `source_handle` / `target_handle`, each a port\nid. Unknown keys are ignored rather than rejected.", "properties": {"id": {"format": "uuid", "title": "Id", "type": "string"}, "source_handle": {"anyOf": [{"format": "uuid", "type": "string"}, {"type": "null"}], "default": null, "title": "Source Handle"}, "target_handle": {"anyOf": [{"format": "uuid", "type": "string"}, {"type": "null"}], "default": null, "title": "Target Handle"}}, "required": ["id"], "title": "Edge", "type": "object"}, "FlattenBoundaryDelta": {"description": "Flatten a boundary node: delete it and promote its children to its parent.", "properties": {"node_id": {"format": "uuid", "title": "Node Id", "type": "string"}, "type": {"const": "flatten_boundary", "title": "Type", "type": "string"}}, "required": ["type", "node_id"], "title": "FlattenBoundaryDelta", "type": "object"}, "GraphResponse": {"additionalProperties": false, "properties": {"branch": {"$ref": "#/components/schemas/BranchRef"}, "edges": {"items": {"$ref": "#/components/schemas/WireEdge"}, "title": "Edges", "type": "array"}, "nodes": {"items": {"$ref": "#/components/schemas/WireNode"}, "title": "Nodes", "type": "array"}, "project_id": {"format": "uuid", "title": "Project Id", "type": "string"}, "version": {"title": "Version", "type": "string"}}, "required": ["version", "project_id", "branch", "nodes", "edges"], "title": "GraphResponse", "type": "object"}, "HTTPValidationError": {"properties": {"detail": {"items": {"$ref": "#/components/schemas/ValidationError"}, "title": "Detail", "type": "array"}}, "title": "HTTPValidationError", "type": "object"}, "HealthzResponse": {"additionalProperties": false, "properties": {"ok": {"title": "Ok", "type": "boolean"}}, "required": ["ok"], "title": "HealthzResponse", "type": "object"}, "Node": {"description": "A behavior or boundary node.", "properties": {"data": {"$ref": "#/components/schemas/NodeData"}, "id": {"format": "uuid", "title": "Id", "type": "string"}, "kind": {"enum": ["behavior", "boundary", "state", "io"], "title": "Kind", "type": "string"}, "parent_id": {"anyOf": [{"format": "uuid", "type": "string"}, {"type": "null"}], "default": null, "title": "Parent Id"}}, "required": ["id", "kind"], "title": "Node", "type": "object"}, "NodeData": {"description": "A node's data payload; its shape varies by kind and whether the node is\nexternal.\n\nOn update, key-presence semantics apply: a field present with a null value is\nset to null, while an omitted field is left unchanged. All fields are optional\nat the type level \u2014 cross-field rules (e.g. behavior nodes reject `user_kind`;\nexternal nodes require `external_kind`) are enforced server-side, so a partial\nupdate is not blocked by \"missing required field\" errors that don't apply to it.", "properties": {"config": {"items": {"$ref": "#/components/schemas/Port"}, "title": "Config", "type": "array"}, "constraints": {"items": {"type": "string"}, "title": "Constraints", "type": "array"}, "description": {"default": "", "title": "Description", "type": "string"}, "documentation_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Documentation Url"}, "external_kind": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "External Kind"}, "inputs": {"items": {"$ref": "#/components/schemas/Port"}, "title": "Inputs", "type": "array"}, "is_external": {"default": false, "title": "Is External", "type": "boolean"}, "is_test_node": {"default": false, "title": "Is Test Node", "type": "boolean"}, "kind": {"anyOf": [{"enum": ["behavior", "boundary", "state", "io"], "type": "string"}, {"type": "null"}], "default": null, "title": "Kind"}, "name": {"default": "", "title": "Name", "type": "string"}, "outputs": {"items": {"$ref": "#/components/schemas/Port"}, "title": "Outputs", "type": "array"}, "parent_id": {"anyOf": [{"format": "uuid", "type": "string"}, {"type": "null"}], "default": null, "title": "Parent Id"}, "path_prefix": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Path Prefix"}, "protocol": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Protocol"}, "source_decisions": {"items": {"format": "uuid", "type": "string"}, "title": "Source Decisions", "type": "array"}, "status": {"default": "idle", "title": "Status", "type": "string"}, "user_kind": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "User Kind"}, "verifications": {"items": {"$ref": "#/components/schemas/Verification"}, "title": "Verifications", "type": "array"}}, "title": "NodeData", "type": "object"}, "NodeNeighborhoodResponse": {"additionalProperties": false, "properties": {"branch": {"$ref": "#/components/schemas/BranchRef"}, "edges": {"items": {"$ref": "#/components/schemas/WireEdge"}, "title": "Edges", "type": "array"}, "neighbors": {"items": {"$ref": "#/components/schemas/WireNode"}, "title": "Neighbors", "type": "array"}, "node": {"$ref": "#/components/schemas/WireNode"}, "project_id": {"format": "uuid", "title": "Project Id", "type": "string"}, "version": {"title": "Version", "type": "string"}}, "required": ["version", "project_id", "branch", "node", "neighbors", "edges"], "title": "NodeNeighborhoodResponse", "type": "object"}, "NotFoundBody": {"additionalProperties": false, "description": "Returned when a delta targets a node that doesn't exist (404). `node_id`\nidentifies the missing target.", "properties": {"error": {"const": "not_found", "title": "Error", "type": "string"}, "node_id": {"title": "Node Id", "type": "string"}, "reason": {"title": "Reason", "type": "string"}}, "required": ["error", "node_id", "reason"], "title": "NotFoundBody", "type": "object"}, "Port": {"description": "An input, output, or config port on a node.", "properties": {"description": {"default": "", "title": "Description", "type": "string"}, "id": {"format": "uuid", "title": "Id", "type": "string"}, "name": {"default": "", "title": "Name", "type": "string"}, "type": {"default": "", "title": "Type", "type": "string"}}, "required": ["id"], "title": "Port", "type": "object"}, "Position": {"additionalProperties": false, "properties": {"x": {"title": "X", "type": "number"}, "y": {"title": "Y", "type": "number"}}, "required": ["x", "y"], "title": "Position", "type": "object"}, "ProjectOut": {"additionalProperties": false, "properties": {"archived": {"title": "Archived", "type": "boolean"}, "created_at": {"title": "Created At", "type": "string"}, "h2o_schema_version": {"title": "H2O Schema Version", "type": "integer"}, "id": {"format": "uuid", "title": "Id", "type": "string"}, "intent": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Intent"}, "language": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Language"}, "last_opened_at": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Last Opened At"}, "name": {"title": "Name", "type": "string"}, "updated_at": {"title": "Updated At", "type": "string"}}, "required": ["id", "name", "language", "intent", "archived", "h2o_schema_version", "created_at", "updated_at", "last_opened_at"], "title": "ProjectOut", "type": "object"}, "ProjectsListResponse": {"additionalProperties": false, "properties": {"projects": {"items": {"$ref": "#/components/schemas/ProjectOut"}, "title": "Projects", "type": "array"}, "version": {"title": "Version", "type": "string"}}, "required": ["version", "projects"], "title": "ProjectsListResponse", "type": "object"}, "RecoveryHint": {"description": "The finite set of recovery hints a client may receive in\n`ApiError.recovery_hint`:\n\n - \"regenerate_uuid\" \u2014 mint a new UUID at `field` and retry\n - \"drop_field\" \u2014 delete `field` and retry\n - null \u2014 not auto-recoverable; user intervention needed\n\nA closed set, so integrations can switch over the value; a new hint is added\nhere deliberately, giving clients a chance to handle it.", "enum": ["regenerate_uuid", "drop_field"], "title": "RecoveryHint", "type": "string"}, "ReparentNodeDelta": {"description": "Move a node to a new parent (or to top-level).\n\n`parent_id` must be present on the wire (use null for top-level); an absent key\nis rejected, so a client bug surfaces as a clean parse error rather than an\nambiguous no-op.", "properties": {"node_id": {"format": "uuid", "title": "Node Id", "type": "string"}, "parent_id": {"anyOf": [{"format": "uuid", "type": "string"}, {"type": "null"}], "title": "Parent Id"}, "type": {"const": "reparent_node", "title": "Type", "type": "string"}}, "required": ["type", "node_id", "parent_id"], "title": "ReparentNodeDelta", "type": "object"}, "UpdateNodeDataDelta": {"description": "Update a node's data, with key-presence partial semantics.\n\n`after` may be empty; whether a field is present in `after` distinguishes\n\"set this field\" from \"leave it unchanged\".", "properties": {"after": {"$ref": "#/components/schemas/NodeData"}, "node_id": {"format": "uuid", "title": "Node Id", "type": "string"}, "type": {"const": "update_node_data", "title": "Type", "type": "string"}}, "required": ["type", "node_id", "after"], "title": "UpdateNodeDataDelta", "type": "object"}, "V1CreateBranchBody": {"description": "Body for ``POST /v1/projects/{pid}/branches``. ``name`` defaults to the\nnext ``main-work[-N]`` for the caller, so a bodyless POST works.", "properties": {"name": {"anyOf": [{"maxLength": 200, "minLength": 1, "type": "string"}, {"type": "null"}], "title": "Name"}}, "title": "V1CreateBranchBody", "type": "object"}, "V1DeltasBody": {"description": "Request body for ``POST /v1/branches/{bid}/deltas``.", "properties": {"deltas": {"description": "Ordered list of delta operations to apply. Each must carry a `type` field in the closed write-surface vocabulary (add_node / delete_node / update_node_data / reparent_node / add_edge / delete_edge / flatten_boundary). Empty list returns 200 with `applied: false` and no version bump.", "items": {"discriminator": {"mapping": {"add_edge": "#/components/schemas/AddEdgeDelta", "add_node": "#/components/schemas/AddNodeDelta", "delete_edge": "#/components/schemas/DeleteEdgeDelta", "delete_node": "#/components/schemas/DeleteNodeDelta", "flatten_boundary": "#/components/schemas/FlattenBoundaryDelta", "reparent_node": "#/components/schemas/ReparentNodeDelta", "update_node_data": "#/components/schemas/UpdateNodeDataDelta"}, "propertyName": "type"}, "oneOf": [{"$ref": "#/components/schemas/AddNodeDelta"}, {"$ref": "#/components/schemas/DeleteNodeDelta"}, {"$ref": "#/components/schemas/UpdateNodeDataDelta"}, {"$ref": "#/components/schemas/ReparentNodeDelta"}, {"$ref": "#/components/schemas/AddEdgeDelta"}, {"$ref": "#/components/schemas/DeleteEdgeDelta"}, {"$ref": "#/components/schemas/FlattenBoundaryDelta"}]}, "title": "Deltas", "type": "array"}, "expected_version": {"description": "The branch.version the client believes is current. If it doesn't match the server's view at commit time, the route 409s with the actual current_version in the detail.", "minimum": 0.0, "title": "Expected Version", "type": "integer"}, "positions": {"anyOf": [{"additionalProperties": {"$ref": "#/components/schemas/V1NodePosition"}, "type": "object"}, {"type": "null"}], "description": "Optional explicit cell pins as `node_id -> {x, y}`, applied to the branch owner's view AFTER the deltas. **Additive**: a pin is introduced only for a node the owner hasn't already placed \u2014 it never overwrites a hand-placement and never deletes. Use only to override the derived grid placement for nodes you create.", "title": "Positions"}}, "required": ["expected_version"], "title": "V1DeltasBody", "type": "object"}, "V1NodePosition": {"description": "A single explicit pin coordinate on the `/v1/` write surface.", "properties": {"x": {"title": "X", "type": "number"}, "y": {"title": "Y", "type": "number"}}, "required": ["x", "y"], "title": "V1NodePosition", "type": "object"}, "V1ValidateBody": {"description": "Request body for ``POST /v1/branches/{bid}/validate``.", "properties": {"deltas": {"items": {"discriminator": {"mapping": {"add_edge": "#/components/schemas/AddEdgeDelta", "add_node": "#/components/schemas/AddNodeDelta", "delete_edge": "#/components/schemas/DeleteEdgeDelta", "delete_node": "#/components/schemas/DeleteNodeDelta", "flatten_boundary": "#/components/schemas/FlattenBoundaryDelta", "reparent_node": "#/components/schemas/ReparentNodeDelta", "update_node_data": "#/components/schemas/UpdateNodeDataDelta"}, "propertyName": "type"}, "oneOf": [{"$ref": "#/components/schemas/AddNodeDelta"}, {"$ref": "#/components/schemas/DeleteNodeDelta"}, {"$ref": "#/components/schemas/UpdateNodeDataDelta"}, {"$ref": "#/components/schemas/ReparentNodeDelta"}, {"$ref": "#/components/schemas/AddEdgeDelta"}, {"$ref": "#/components/schemas/DeleteEdgeDelta"}, {"$ref": "#/components/schemas/FlattenBoundaryDelta"}]}, "title": "Deltas", "type": "array"}}, "title": "V1ValidateBody", "type": "object"}, "ValidateResponse": {"additionalProperties": false, "properties": {"branch": {"$ref": "#/components/schemas/BranchRef"}, "delta_count": {"title": "Delta Count", "type": "integer"}, "project_id": {"format": "uuid", "title": "Project Id", "type": "string"}, "valid": {"title": "Valid", "type": "boolean"}, "version": {"title": "Version", "type": "string"}}, "required": ["version", "project_id", "branch", "valid", "delta_count"], "title": "ValidateResponse", "type": "object"}, "ValidationError": {"properties": {"ctx": {"title": "Context", "type": "object"}, "input": {"title": "Input"}, "loc": {"items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, "title": "Location", "type": "array"}, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}}, "required": ["loc", "msg", "type"], "title": "ValidationError", "type": "object"}, "Verification": {"description": "A test specification attached to a behavior node.", "properties": {"author": {"enum": ["user", "agent"], "title": "Author", "type": "string"}, "id": {"format": "uuid", "title": "Id", "type": "string"}, "text": {"title": "Text", "type": "string"}, "type": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Type"}}, "required": ["id", "author", "text"], "title": "Verification", "type": "object"}, "VersionConflictBody": {"additionalProperties": false, "properties": {"current_version": {"title": "Current Version", "type": "integer"}, "error": {"const": "version_conflict", "title": "Error", "type": "string"}}, "required": ["error", "current_version"], "title": "VersionConflictBody", "type": "object"}, "WireEdge": {"additionalProperties": false, "description": "Read-path edge: `source`/`target` are server-derived NODE ids\n(absent from the client-authored `h2o.types.Edge`); `*_handle` are the\nPORT ids.", "properties": {"id": {"format": "uuid", "title": "Id", "type": "string"}, "source": {"format": "uuid", "title": "Source", "type": "string"}, "source_handle": {"anyOf": [{"format": "uuid", "type": "string"}, {"type": "null"}], "title": "Source Handle"}, "target": {"format": "uuid", "title": "Target", "type": "string"}, "target_handle": {"anyOf": [{"format": "uuid", "type": "string"}, {"type": "null"}], "title": "Target Handle"}}, "required": ["id", "source", "target", "source_handle", "target_handle"], "title": "WireEdge", "type": "object"}, "WireNode": {"additionalProperties": false, "properties": {"data": {"$ref": "#/components/schemas/WireNodeData"}, "id": {"format": "uuid", "title": "Id", "type": "string"}, "kind": {"enum": ["behavior", "boundary", "state", "io"], "title": "Kind", "type": "string"}, "parent_id": {"anyOf": [{"format": "uuid", "type": "string"}, {"type": "null"}], "title": "Parent Id"}, "position": {"$ref": "#/components/schemas/Position"}}, "required": ["id", "kind", "parent_id", "position", "data"], "title": "WireNode", "type": "object"}, "WireNodeData": {"additionalProperties": false, "description": "Read-path node `data`. NOT `h2o.NodeData`: the response omits\n`kind`/`parent_id` (those live on the wrapper) and emits the boundary/\nexternal scalars matrix-conditionally \u2014 so the matrix fields are\noptional (absent = not-applicable-to-kind).", "properties": {"config": {"items": {"$ref": "#/components/schemas/WirePort"}, "title": "Config", "type": "array"}, "constraints": {"items": {"type": "string"}, "title": "Constraints", "type": "array"}, "description": {"title": "Description", "type": "string"}, "documentation_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Documentation Url"}, "external_kind": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "External Kind"}, "inputs": {"items": {"$ref": "#/components/schemas/WirePort"}, "title": "Inputs", "type": "array"}, "is_external": {"title": "Is External", "type": "boolean"}, "is_test_node": {"title": "Is Test Node", "type": "boolean"}, "name": {"title": "Name", "type": "string"}, "outputs": {"items": {"$ref": "#/components/schemas/WirePort"}, "title": "Outputs", "type": "array"}, "path_prefix": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Path Prefix"}, "protocol": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Protocol"}, "source_decisions": {"anyOf": [{"items": {"type": "string"}, "type": "array"}, {"type": "null"}], "default": null, "title": "Source Decisions"}, "status": {"title": "Status", "type": "string"}, "user_kind": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "User Kind"}, "verifications": {"items": {"$ref": "#/components/schemas/WireVerification"}, "title": "Verifications", "type": "array"}}, "required": ["name", "description", "status", "is_test_node", "is_external"], "title": "WireNodeData", "type": "object"}, "WirePort": {"additionalProperties": false, "properties": {"description": {"default": "", "title": "Description", "type": "string"}, "id": {"format": "uuid", "title": "Id", "type": "string"}, "name": {"default": "", "title": "Name", "type": "string"}, "type": {"default": "", "title": "Type", "type": "string"}}, "required": ["id"], "title": "WirePort", "type": "object"}, "WireVerification": {"additionalProperties": false, "properties": {"author": {"enum": ["user", "agent"], "title": "Author", "type": "string"}, "id": {"format": "uuid", "title": "Id", "type": "string"}, "text": {"title": "Text", "type": "string"}, "type": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Type"}}, "required": ["id", "author", "text"], "title": "WireVerification", "type": "object"}}, "securitySchemes": {"Bearer": {"description": "API key. Send as `Authorization: Bearer `.", "scheme": "bearer", "type": "http"}}}, "info": {"description": "The hydrate.sh public API. All routes are scoped to the `/v1/` prefix and require a Bearer API key (see Authentication).\n\n**Stability:** `v1` is the wire-stable surface. Breaking changes will rebase to `/v2/`.", "title": "hydrate.sh v1 API", "version": "1.0.0-rc.1"}, "openapi": "3.1.0", "paths": {"/v1/__healthz": {"get": {"description": "Returns `{\"ok\": true}` when the /v1/ surface is serving traffic. Unauthenticated. Use this for load-balancer and uptime-monitor checks; a 404 from this path means the /v1/ surface is disabled on the responding instance, not that the server is down.", "operationId": "healthz_v1___healthz_get", "responses": {"200": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/HealthzResponse"}}}, "description": "Successful Response"}}, "security": [], "summary": "Liveness probe", "tags": ["health"]}}, "/v1/branches/{branch_id}/deltas": {"post": {"description": "Apply an ordered batch of structural changes to a branch under optimistic concurrency. Pass the `version` from the branch read as `expected_version`; a 409 with `current_version` is returned if the branch moved underneath you. Pair with the `Idempotency-Key` header for at-most-once apply across retries.", "operationId": "apply_branch_deltas_v1_branches__branch_id__deltas_post", "parameters": [{"description": "The branch to apply to.", "in": "path", "name": "branch_id", "required": true, "schema": {"description": "The branch to apply to.", "format": "uuid", "title": "Branch Id", "type": "string"}}], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/V1DeltasBody"}}}, "required": true}, "responses": {"200": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/DeltaApplyResponse"}}}, "description": "Successful Response"}, "401": {"$ref": "#/components/responses/Unauthenticated"}, "403": {"$ref": "#/components/responses/Forbidden"}, "404": {"content": {"application/json": {"schema": {"properties": {"detail": {"oneOf": [{"properties": {"code": {"type": "string"}, "message": {"type": "string"}}, "required": ["code", "message"], "type": "object"}, {"$ref": "#/components/schemas/NotFoundBody"}]}}, "required": ["detail"], "type": "object"}}}, "description": "Branch/project not found or not accessible, OR a delta targets a node that doesn't exist."}, "409": {"content": {"application/json": {"schema": {"properties": {"detail": {"oneOf": [{"$ref": "#/components/schemas/VersionConflictBody"}, {"$ref": "#/components/schemas/BranchNotActiveBody"}]}}, "required": ["detail"], "type": "object"}}}, "description": "OCC version conflict, or the branch is not active."}, "422": {"content": {"application/json": {"schema": {"oneOf": [{"$ref": "#/components/schemas/HTTPValidationError"}, {"properties": {"detail": {"$ref": "#/components/schemas/DeltaFieldErrorBody"}}, "required": ["detail"], "type": "object"}]}}}, "description": "Request body failed validation, OR a delta is malformed / unknown / not an object."}, "429": {"$ref": "#/components/responses/RateLimited"}}, "summary": "Apply a delta batch to a branch (OCC)", "tags": ["branches"]}}, "/v1/branches/{branch_id}/graph": {"get": {"description": "Read the full graph (nodes, ports, edges, version) of a specific branch. Use the returned `version` as `expected_version` on a subsequent apply to that branch.", "operationId": "fetch_branch_graph_v1_branches__branch_id__graph_get", "parameters": [{"description": "The branch to read.", "in": "path", "name": "branch_id", "required": true, "schema": {"description": "The branch to read.", "format": "uuid", "title": "Branch Id", "type": "string"}}], "responses": {"200": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/GraphResponse"}}}, "description": "Successful Response"}, "401": {"$ref": "#/components/responses/Unauthenticated"}, "403": {"$ref": "#/components/responses/Forbidden"}, "404": {"$ref": "#/components/responses/NotFound"}, "422": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}, "description": "Validation Error"}, "429": {"$ref": "#/components/responses/RateLimited"}}, "summary": "Read a specific branch's graph", "tags": ["branches"]}}, "/v1/branches/{branch_id}/validate": {"post": {"description": "Run the same validation pipeline as the branch apply, but roll back before commit. Returns 200 on success (with the projected version) or a structured 422 identifying the first failing delta. Use it before applying a batch you can't recover from.", "operationId": "validate_branch_deltas_v1_branches__branch_id__validate_post", "parameters": [{"description": "The branch to validate against.", "in": "path", "name": "branch_id", "required": true, "schema": {"description": "The branch to validate against.", "format": "uuid", "title": "Branch Id", "type": "string"}}], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/V1ValidateBody"}}}, "required": true}, "responses": {"200": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/ValidateResponse"}}}, "description": "Successful Response"}, "401": {"$ref": "#/components/responses/Unauthenticated"}, "403": {"$ref": "#/components/responses/Forbidden"}, "404": {"content": {"application/json": {"schema": {"properties": {"detail": {"oneOf": [{"properties": {"code": {"type": "string"}, "message": {"type": "string"}}, "required": ["code", "message"], "type": "object"}, {"$ref": "#/components/schemas/NotFoundBody"}]}}, "required": ["detail"], "type": "object"}}}, "description": "Branch/project not found or not accessible, OR a delta targets a node that doesn't exist."}, "422": {"content": {"application/json": {"schema": {"oneOf": [{"$ref": "#/components/schemas/HTTPValidationError"}, {"properties": {"detail": {"$ref": "#/components/schemas/DeltaFieldErrorBody"}}, "required": ["detail"], "type": "object"}]}}}, "description": "Request body failed validation, OR a delta is malformed / unknown / not an object."}, "429": {"$ref": "#/components/responses/RateLimited"}}, "summary": "Dry-run a delta batch on a branch", "tags": ["branches"]}}, "/v1/graph/{project_id}": {"get": {"description": "Returns the active main-branch graph (nodes, ports, edges, version) for the project. Read-only: the main branch is merge-only. To make changes, fork a branch (`POST /v1/projects/{project_id}/branches`) and apply deltas to it.", "operationId": "fetch_graph_v1_graph__project_id__get", "parameters": [{"description": "The project whose main-branch graph to fetch.", "in": "path", "name": "project_id", "required": true, "schema": {"description": "The project whose main-branch graph to fetch.", "format": "uuid", "title": "Project Id", "type": "string"}}], "responses": {"200": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/GraphResponse"}}}, "description": "Successful Response"}, "401": {"$ref": "#/components/responses/Unauthenticated"}, "403": {"$ref": "#/components/responses/Forbidden"}, "404": {"$ref": "#/components/responses/NotFound"}, "422": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}, "description": "Validation Error"}, "429": {"$ref": "#/components/responses/RateLimited"}}, "summary": "Get the canonical graph for a project", "tags": ["graph"]}}, "/v1/graph/{project_id}/boundary/{node_id}": {"get": {"description": "Returns the boundary view for a single node: the inputs and outputs that cross the node's enforcement perimeter. Used by clients that visualize a node's contract surface in isolation from the rest of the graph.", "operationId": "fetch_boundary_v1_graph__project_id__boundary__node_id__get", "parameters": [{"description": "The project the boundary lives in.", "in": "path", "name": "project_id", "required": true, "schema": {"description": "The project the boundary lives in.", "format": "uuid", "title": "Project Id", "type": "string"}}, {"description": "The boundary node to inspect.", "in": "path", "name": "node_id", "required": true, "schema": {"description": "The boundary node to inspect.", "format": "uuid", "title": "Node Id", "type": "string"}}], "responses": {"200": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/BoundaryResponse"}}}, "description": "Successful Response"}, "401": {"$ref": "#/components/responses/Unauthenticated"}, "403": {"$ref": "#/components/responses/Forbidden"}, "404": {"$ref": "#/components/responses/NotFound"}, "422": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}, "description": "Validation Error"}, "429": {"$ref": "#/components/responses/RateLimited"}}, "summary": "Get the boundary topology around a node", "tags": ["graph"]}}, "/v1/graph/{project_id}/node/{node_id}": {"get": {"description": "Returns a single node along with its directly connected neighbors and edges (the 1-hop topology). The node must belong to the project's active main branch.", "operationId": "fetch_node_with_neighbors_v1_graph__project_id__node__node_id__get", "parameters": [{"description": "The project the node lives in.", "in": "path", "name": "project_id", "required": true, "schema": {"description": "The project the node lives in.", "format": "uuid", "title": "Project Id", "type": "string"}}, {"description": "The node to fetch.", "in": "path", "name": "node_id", "required": true, "schema": {"description": "The node to fetch.", "format": "uuid", "title": "Node Id", "type": "string"}}], "responses": {"200": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/NodeNeighborhoodResponse"}}}, "description": "Successful Response"}, "401": {"$ref": "#/components/responses/Unauthenticated"}, "403": {"$ref": "#/components/responses/Forbidden"}, "404": {"$ref": "#/components/responses/NotFound"}, "422": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}, "description": "Validation Error"}, "429": {"$ref": "#/components/responses/RateLimited"}}, "summary": "Get a node with its 1-hop neighborhood", "tags": ["graph"]}}, "/v1/projects": {"get": {"description": "Returns the projects the caller can see. For API-key callers the list is further filtered by the per-key project allowlist (if configured). Archived projects are excluded. An empty list is a normal response when the caller has no visible projects.", "operationId": "list_projects_v1_projects_get", "parameters": [{"description": "Maximum projects to return. Defaults to 100; max 200.", "in": "query", "name": "limit", "required": false, "schema": {"default": 100, "description": "Maximum projects to return. Defaults to 100; max 200.", "maximum": 200, "minimum": 1, "title": "Limit", "type": "integer"}}], "responses": {"200": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/ProjectsListResponse"}}}, "description": "Successful Response"}, "401": {"$ref": "#/components/responses/Unauthenticated"}, "403": {"$ref": "#/components/responses/Forbidden"}, "422": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}, "description": "Validation Error"}, "429": {"$ref": "#/components/responses/RateLimited"}}, "summary": "List your projects", "tags": ["projects"]}}, "/v1/projects/{project_id}/branches": {"get": {"description": "List your active working branches on this project. The protected main branch is not included. Each branch is a unit of work you can read and apply changes to before merging into main.", "operationId": "list_branches_v1_projects__project_id__branches_get", "parameters": [{"description": "The project to list branches for.", "in": "path", "name": "project_id", "required": true, "schema": {"description": "The project to list branches for.", "format": "uuid", "title": "Project Id", "type": "string"}}], "responses": {"200": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/BranchListResponse"}}}, "description": "Successful Response"}, "401": {"$ref": "#/components/responses/Unauthenticated"}, "403": {"$ref": "#/components/responses/Forbidden"}, "404": {"$ref": "#/components/responses/NotFound"}, "422": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}, "description": "Validation Error"}, "429": {"$ref": "#/components/responses/RateLimited"}}, "summary": "List the caller's working branches on a project", "tags": ["branches"]}, "post": {"description": "Fork a new working branch from the project's main branch. The branch is copied from main's current state and owned by you; your changes stay isolated on it until merged. Requires an editor role, and there is a per-project limit on how many active branches you can hold at once.", "operationId": "create_branch_v1_projects__project_id__branches_post", "parameters": [{"description": "The project to fork a branch in.", "in": "path", "name": "project_id", "required": true, "schema": {"description": "The project to fork a branch in.", "format": "uuid", "title": "Project Id", "type": "string"}}], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/V1CreateBranchBody"}}}, "required": true}, "responses": {"201": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/BranchCreateResponse"}}}, "description": "Successful Response"}, "401": {"$ref": "#/components/responses/Unauthenticated"}, "403": {"$ref": "#/components/responses/Forbidden"}, "404": {"$ref": "#/components/responses/NotFound"}, "422": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}, "description": "Validation Error"}, "429": {"$ref": "#/components/responses/RateLimited"}}, "summary": "Start a unit of work (fork a branch from main)", "tags": ["branches"]}}}, "security": [{"Bearer": []}], "servers": [{"description": "Production", "url": "https://api.hydrate.sh"}], "tags": [{"description": "Work on a branch (a unit of work) rather than the protected main branch: fork a branch from main, list your branches, and read / validate / apply deltas against a specific branch.", "name": "branches"}, {"description": "Read the canonical graph for a project, validate a delta batch (dry-run), or apply a delta batch through the OCC + idempotency middleware chain.", "name": "graph"}, {"description": "Liveness probes. Unauthenticated. Used by load balancers and uptime monitors.", "name": "health"}, {"description": "List the projects your principal can see. Filtered by membership + (for API keys) the per-key project allowlist.", "name": "projects"}]} \ No newline at end of file +{"components": {"responses": {"Forbidden": {"content": {"application/json": {"schema": {"properties": {"detail": {"properties": {"code": {"type": "string"}, "message": {"type": "string"}}, "type": "object"}}, "type": "object"}}}, "description": "Credentials are valid but lack the scope required for this route, or the principal lacks membership in the target project."}, "NotFound": {"content": {"application/json": {"schema": {"properties": {"detail": {"properties": {"code": {"example": "not_found", "type": "string"}, "message": {"type": "string"}}, "type": "object"}}, "type": "object"}}}, "description": "Resource not found OR not accessible to this principal. Leak parity: the response is identical in both cases so an attacker cannot enumerate resources via 404-vs-403 timing."}, "RateLimited": {"content": {"application/json": {"schema": {"properties": {"detail": {"example": "rate_limited", "type": "string"}}, "type": "object"}}}, "description": "Per-bucket rate limit exceeded. The response carries ``Retry-After`` and the standard ``X-RateLimit-*`` headers (Limit / Remaining / Reset).", "headers": {"Retry-After": {"schema": {"type": "integer"}}, "X-RateLimit-Limit": {"schema": {"type": "integer"}}, "X-RateLimit-Remaining": {"schema": {"type": "integer"}}, "X-RateLimit-Reset": {"schema": {"type": "integer"}}}}, "Unauthenticated": {"content": {"application/json": {"schema": {"properties": {"detail": {"example": "unauthenticated", "type": "string"}}, "required": ["detail"], "type": "object"}}}, "description": "No credentials, malformed credentials, or revoked credentials. The envelope is leak-parity (same shape across all 401 paths) so an attacker cannot distinguish revoked vs. unknown via the body."}, "Unprocessable": {"content": {"application/json": {"schema": {"properties": {"detail": {"oneOf": [{"type": "string"}, {"properties": {"delta_ordinal": {"type": "integer"}, "delta_type": {"type": ["string", "null"]}, "error": {"type": "string"}, "field": {"type": ["string", "null"]}, "reason": {"type": ["string", "null"]}, "recoverable": {"type": "boolean"}, "recovery_hint": {"type": ["string", "null"]}}, "type": "object"}]}}, "type": "object"}}}, "description": "Request body failed validation. For delta routes the response carries ``error``, ``delta_ordinal``, ``delta_type``, ``field``, ``reason``, ``recoverable``, and ``recovery_hint`` so a client can auto-recover or surface a typed error."}, "VersionConflict": {"content": {"application/json": {"schema": {"properties": {"detail": {"properties": {"current_version": {"type": "integer"}, "error": {"example": "version_conflict", "type": "string"}}, "type": "object"}}, "type": "object"}}}, "description": "Optimistic concurrency check failed \u2014 the branch's current version is not the one the client sent in ``expected_version``. The response carries ``current_version`` so the client can refetch + retry."}}, "schemas": {"AddEdgeDelta": {"description": "Insert an edge.", "properties": {"edge": {"$ref": "#/components/schemas/Edge"}, "type": {"const": "add_edge", "title": "Type", "type": "string"}}, "required": ["type", "edge"], "title": "AddEdgeDelta", "type": "object"}, "AddNodeDelta": {"description": "Insert a node.", "properties": {"node": {"$ref": "#/components/schemas/Node"}, "type": {"const": "add_node", "title": "Type", "type": "string"}}, "required": ["type", "node"], "title": "AddNodeDelta", "type": "object"}, "BoundaryResponse": {"additionalProperties": false, "properties": {"boundary": {"$ref": "#/components/schemas/WireNode"}, "branch": {"$ref": "#/components/schemas/BranchRef"}, "children": {"items": {"$ref": "#/components/schemas/WireNode"}, "title": "Children", "type": "array"}, "edges": {"items": {"$ref": "#/components/schemas/WireEdge"}, "title": "Edges", "type": "array"}, "project_id": {"format": "uuid", "title": "Project Id", "type": "string"}, "version": {"title": "Version", "type": "string"}}, "required": ["version", "project_id", "branch", "boundary", "children", "edges"], "title": "BoundaryResponse", "type": "object"}, "BranchCreateResponse": {"additionalProperties": false, "properties": {"branch": {"$ref": "#/components/schemas/BranchMeta"}, "project_id": {"format": "uuid", "title": "Project Id", "type": "string"}, "version": {"title": "Version", "type": "string"}}, "required": ["version", "project_id", "branch"], "title": "BranchCreateResponse", "type": "object"}, "BranchListResponse": {"additionalProperties": false, "properties": {"branches": {"items": {"$ref": "#/components/schemas/BranchMeta"}, "title": "Branches", "type": "array"}, "project_id": {"format": "uuid", "title": "Project Id", "type": "string"}, "version": {"title": "Version", "type": "string"}}, "required": ["version", "project_id", "branches"], "title": "BranchListResponse", "type": "object"}, "BranchMeta": {"additionalProperties": false, "properties": {"base_main_version": {"title": "Base Main Version", "type": "integer"}, "created_at": {"title": "Created At", "type": "string"}, "id": {"format": "uuid", "title": "Id", "type": "string"}, "is_main": {"title": "Is Main", "type": "boolean"}, "last_active_at": {"title": "Last Active At", "type": "string"}, "merged_at": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Merged At"}, "name": {"title": "Name", "type": "string"}, "owner_id": {"anyOf": [{"format": "uuid", "type": "string"}, {"type": "null"}], "title": "Owner Id"}, "project_id": {"format": "uuid", "title": "Project Id", "type": "string"}, "status": {"title": "Status", "type": "string"}, "version": {"title": "Version", "type": "integer"}}, "required": ["id", "project_id", "owner_id", "name", "is_main", "version", "base_main_version", "status", "last_active_at", "created_at", "merged_at"], "title": "BranchMeta", "type": "object"}, "BranchNotActiveBody": {"additionalProperties": false, "properties": {"current_status": {"title": "Current Status", "type": "string"}, "error": {"const": "branch_not_active", "title": "Error", "type": "string"}}, "required": ["error", "current_status"], "title": "BranchNotActiveBody", "type": "object"}, "BranchRef": {"additionalProperties": false, "properties": {"id": {"format": "uuid", "title": "Id", "type": "string"}, "version": {"title": "Version", "type": "integer"}}, "required": ["id", "version"], "title": "BranchRef", "type": "object"}, "DeleteEdgeDelta": {"description": "Remove an edge.", "properties": {"edge_id": {"format": "uuid", "title": "Edge Id", "type": "string"}, "type": {"const": "delete_edge", "title": "Type", "type": "string"}}, "required": ["type", "edge_id"], "title": "DeleteEdgeDelta", "type": "object"}, "DeleteNodeDelta": {"description": "Cascade-delete a node and its descendant subtree.", "properties": {"node_id": {"format": "uuid", "title": "Node Id", "type": "string"}, "type": {"const": "delete_node", "title": "Type", "type": "string"}}, "required": ["type", "node_id"], "title": "DeleteNodeDelta", "type": "object"}, "DeltaApplyResponse": {"additionalProperties": false, "properties": {"applied": {"title": "Applied", "type": "boolean"}, "branch": {"$ref": "#/components/schemas/BranchRef"}, "delta_count": {"title": "Delta Count", "type": "integer"}, "positions_applied": {"anyOf": [{"type": "integer"}, {"type": "null"}], "default": null, "title": "Positions Applied"}, "project_id": {"format": "uuid", "title": "Project Id", "type": "string"}, "version": {"title": "Version", "type": "string"}}, "required": ["version", "project_id", "branch", "applied", "delta_count"], "title": "DeltaApplyResponse", "type": "object"}, "DeltaFieldErrorBody": {"additionalProperties": false, "description": "The 7-key per-delta envelope (always all keys present) the frontend\nauto-recovery consumes. Shared by every 422 delta error; `error`\ndiscriminates the cause.", "properties": {"delta_ordinal": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Delta Ordinal"}, "delta_type": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Delta Type"}, "error": {"enum": ["malformed_delta_field", "unknown_delta_type", "delta_not_object", "delta_too_large", "batch_too_large", "batch_count_exceeded"], "title": "Error", "type": "string"}, "field": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Field"}, "reason": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Reason"}, "recoverable": {"title": "Recoverable", "type": "boolean"}, "recovery_hint": {"anyOf": [{"$ref": "#/components/schemas/RecoveryHint"}, {"type": "null"}]}}, "required": ["error", "delta_ordinal", "delta_type", "field", "reason", "recoverable", "recovery_hint"], "title": "DeltaFieldErrorBody", "type": "object"}, "Edge": {"description": "An edge connecting two nodes' ports.\n\nEndpoints are given as `source_handle` / `target_handle`, each a port\nid. Unknown keys are ignored rather than rejected.", "properties": {"id": {"format": "uuid", "title": "Id", "type": "string"}, "source_handle": {"anyOf": [{"format": "uuid", "type": "string"}, {"type": "null"}], "default": null, "title": "Source Handle"}, "target_handle": {"anyOf": [{"format": "uuid", "type": "string"}, {"type": "null"}], "default": null, "title": "Target Handle"}}, "required": ["id"], "title": "Edge", "type": "object"}, "FlattenBoundaryDelta": {"description": "Flatten a boundary node: delete it and promote its children to its parent.", "properties": {"node_id": {"format": "uuid", "title": "Node Id", "type": "string"}, "type": {"const": "flatten_boundary", "title": "Type", "type": "string"}}, "required": ["type", "node_id"], "title": "FlattenBoundaryDelta", "type": "object"}, "GraphResponse": {"additionalProperties": false, "properties": {"branch": {"$ref": "#/components/schemas/BranchRef"}, "edges": {"items": {"$ref": "#/components/schemas/WireEdge"}, "title": "Edges", "type": "array"}, "nodes": {"items": {"$ref": "#/components/schemas/WireNode"}, "title": "Nodes", "type": "array"}, "project_id": {"format": "uuid", "title": "Project Id", "type": "string"}, "version": {"title": "Version", "type": "string"}}, "required": ["version", "project_id", "branch", "nodes", "edges"], "title": "GraphResponse", "type": "object"}, "HTTPValidationError": {"properties": {"detail": {"items": {"$ref": "#/components/schemas/ValidationError"}, "title": "Detail", "type": "array"}}, "title": "HTTPValidationError", "type": "object"}, "HealthzResponse": {"additionalProperties": false, "properties": {"ok": {"title": "Ok", "type": "boolean"}}, "required": ["ok"], "title": "HealthzResponse", "type": "object"}, "Node": {"description": "A behavior or boundary node.", "properties": {"data": {"$ref": "#/components/schemas/NodeData"}, "id": {"format": "uuid", "title": "Id", "type": "string"}, "kind": {"enum": ["behavior", "boundary", "state", "io"], "title": "Kind", "type": "string"}, "parent_id": {"anyOf": [{"format": "uuid", "type": "string"}, {"type": "null"}], "default": null, "title": "Parent Id"}}, "required": ["id", "kind"], "title": "Node", "type": "object"}, "NodeData": {"description": "A node's data payload; its shape varies by kind and whether the node is\nexternal.\n\nOn update, key-presence semantics apply: a field present with a null value is\nset to null, while an omitted field is left unchanged. All fields are optional\nat the type level \u2014 cross-field rules (e.g. behavior nodes reject `user_kind`;\nexternal nodes require `external_kind`) are enforced server-side, so a partial\nupdate is not blocked by \"missing required field\" errors that don't apply to it.", "properties": {"config": {"items": {"$ref": "#/components/schemas/Port"}, "title": "Config", "type": "array"}, "constraints": {"items": {"type": "string"}, "title": "Constraints", "type": "array"}, "description": {"default": "", "title": "Description", "type": "string"}, "documentation_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Documentation Url"}, "external_kind": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "External Kind"}, "inputs": {"items": {"$ref": "#/components/schemas/Port"}, "title": "Inputs", "type": "array"}, "is_external": {"default": false, "title": "Is External", "type": "boolean"}, "is_test_node": {"default": false, "title": "Is Test Node", "type": "boolean"}, "kind": {"anyOf": [{"enum": ["behavior", "boundary", "state", "io"], "type": "string"}, {"type": "null"}], "default": null, "title": "Kind"}, "language": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Language"}, "name": {"default": "", "title": "Name", "type": "string"}, "outputs": {"items": {"$ref": "#/components/schemas/Port"}, "title": "Outputs", "type": "array"}, "parent_id": {"anyOf": [{"format": "uuid", "type": "string"}, {"type": "null"}], "default": null, "title": "Parent Id"}, "path_prefix": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Path Prefix"}, "protocol": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Protocol"}, "source_decisions": {"items": {"format": "uuid", "type": "string"}, "title": "Source Decisions", "type": "array"}, "status": {"default": "idle", "title": "Status", "type": "string"}, "user_kind": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "User Kind"}, "verifications": {"items": {"$ref": "#/components/schemas/Verification"}, "title": "Verifications", "type": "array"}}, "title": "NodeData", "type": "object"}, "NodeNeighborhoodResponse": {"additionalProperties": false, "properties": {"branch": {"$ref": "#/components/schemas/BranchRef"}, "edges": {"items": {"$ref": "#/components/schemas/WireEdge"}, "title": "Edges", "type": "array"}, "neighbors": {"items": {"$ref": "#/components/schemas/WireNode"}, "title": "Neighbors", "type": "array"}, "node": {"$ref": "#/components/schemas/WireNode"}, "project_id": {"format": "uuid", "title": "Project Id", "type": "string"}, "version": {"title": "Version", "type": "string"}}, "required": ["version", "project_id", "branch", "node", "neighbors", "edges"], "title": "NodeNeighborhoodResponse", "type": "object"}, "NotFoundBody": {"additionalProperties": false, "description": "Returned when a delta targets a node that doesn't exist (404). `node_id`\nidentifies the missing target.", "properties": {"error": {"const": "not_found", "title": "Error", "type": "string"}, "node_id": {"title": "Node Id", "type": "string"}, "reason": {"title": "Reason", "type": "string"}}, "required": ["error", "node_id", "reason"], "title": "NotFoundBody", "type": "object"}, "Port": {"description": "An input, output, or config port on a node.", "properties": {"description": {"default": "", "title": "Description", "type": "string"}, "id": {"format": "uuid", "title": "Id", "type": "string"}, "name": {"default": "", "title": "Name", "type": "string"}, "type": {"default": "", "title": "Type", "type": "string"}}, "required": ["id"], "title": "Port", "type": "object"}, "Position": {"additionalProperties": false, "properties": {"x": {"title": "X", "type": "number"}, "y": {"title": "Y", "type": "number"}}, "required": ["x", "y"], "title": "Position", "type": "object"}, "ProjectOut": {"additionalProperties": false, "properties": {"archived": {"title": "Archived", "type": "boolean"}, "created_at": {"title": "Created At", "type": "string"}, "h2o_schema_version": {"title": "H2O Schema Version", "type": "integer"}, "id": {"format": "uuid", "title": "Id", "type": "string"}, "intent": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Intent"}, "language": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Language"}, "last_opened_at": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Last Opened At"}, "name": {"title": "Name", "type": "string"}, "updated_at": {"title": "Updated At", "type": "string"}}, "required": ["id", "name", "language", "intent", "archived", "h2o_schema_version", "created_at", "updated_at", "last_opened_at"], "title": "ProjectOut", "type": "object"}, "ProjectsListResponse": {"additionalProperties": false, "properties": {"projects": {"items": {"$ref": "#/components/schemas/ProjectOut"}, "title": "Projects", "type": "array"}, "version": {"title": "Version", "type": "string"}}, "required": ["version", "projects"], "title": "ProjectsListResponse", "type": "object"}, "RecoveryHint": {"description": "The finite set of recovery hints a client may receive in\n`ApiError.recovery_hint`:\n\n - \"regenerate_uuid\" \u2014 mint a new UUID at `field` and retry\n - \"drop_field\" \u2014 delete `field` and retry\n - null \u2014 not auto-recoverable; user intervention needed\n\nA closed set, so integrations can switch over the value; a new hint is added\nhere deliberately, giving clients a chance to handle it.", "enum": ["regenerate_uuid", "drop_field"], "title": "RecoveryHint", "type": "string"}, "ReparentNodeDelta": {"description": "Move a node to a new parent (or to top-level).\n\n`parent_id` must be present on the wire (use null for top-level); an absent key\nis rejected, so a client bug surfaces as a clean parse error rather than an\nambiguous no-op.", "properties": {"node_id": {"format": "uuid", "title": "Node Id", "type": "string"}, "parent_id": {"anyOf": [{"format": "uuid", "type": "string"}, {"type": "null"}], "title": "Parent Id"}, "type": {"const": "reparent_node", "title": "Type", "type": "string"}}, "required": ["type", "node_id", "parent_id"], "title": "ReparentNodeDelta", "type": "object"}, "UpdateNodeDataDelta": {"description": "Update a node's data, with key-presence partial semantics.\n\n`after` may be empty; whether a field is present in `after` distinguishes\n\"set this field\" from \"leave it unchanged\".", "properties": {"after": {"$ref": "#/components/schemas/NodeData"}, "node_id": {"format": "uuid", "title": "Node Id", "type": "string"}, "type": {"const": "update_node_data", "title": "Type", "type": "string"}}, "required": ["type", "node_id", "after"], "title": "UpdateNodeDataDelta", "type": "object"}, "V1CreateBranchBody": {"description": "Body for ``POST /v1/projects/{pid}/branches``. ``name`` defaults to the\nnext ``main-work[-N]`` for the caller, so a bodyless POST works.", "properties": {"name": {"anyOf": [{"maxLength": 200, "minLength": 1, "type": "string"}, {"type": "null"}], "title": "Name"}}, "title": "V1CreateBranchBody", "type": "object"}, "V1DeltasBody": {"description": "Request body for ``POST /v1/branches/{bid}/deltas``.", "properties": {"deltas": {"description": "Ordered list of delta operations to apply. Each must carry a `type` field in the closed write-surface vocabulary (add_node / delete_node / update_node_data / reparent_node / add_edge / delete_edge / flatten_boundary). Empty list returns 200 with `applied: false` and no version bump.", "items": {"discriminator": {"mapping": {"add_edge": "#/components/schemas/AddEdgeDelta", "add_node": "#/components/schemas/AddNodeDelta", "delete_edge": "#/components/schemas/DeleteEdgeDelta", "delete_node": "#/components/schemas/DeleteNodeDelta", "flatten_boundary": "#/components/schemas/FlattenBoundaryDelta", "reparent_node": "#/components/schemas/ReparentNodeDelta", "update_node_data": "#/components/schemas/UpdateNodeDataDelta"}, "propertyName": "type"}, "oneOf": [{"$ref": "#/components/schemas/AddNodeDelta"}, {"$ref": "#/components/schemas/DeleteNodeDelta"}, {"$ref": "#/components/schemas/UpdateNodeDataDelta"}, {"$ref": "#/components/schemas/ReparentNodeDelta"}, {"$ref": "#/components/schemas/AddEdgeDelta"}, {"$ref": "#/components/schemas/DeleteEdgeDelta"}, {"$ref": "#/components/schemas/FlattenBoundaryDelta"}]}, "title": "Deltas", "type": "array"}, "expected_version": {"description": "The branch.version the client believes is current. If it doesn't match the server's view at commit time, the route 409s with the actual current_version in the detail.", "minimum": 0.0, "title": "Expected Version", "type": "integer"}, "positions": {"anyOf": [{"additionalProperties": {"$ref": "#/components/schemas/V1NodePosition"}, "type": "object"}, {"type": "null"}], "description": "Optional explicit cell pins as `node_id -> {x, y}`, applied to the branch owner's view AFTER the deltas. **Additive**: a pin is introduced only for a node the owner hasn't already placed \u2014 it never overwrites a hand-placement and never deletes. Use only to override the derived grid placement for nodes you create.", "title": "Positions"}}, "required": ["expected_version"], "title": "V1DeltasBody", "type": "object"}, "V1NodePosition": {"description": "A single explicit pin coordinate on the `/v1/` write surface.", "properties": {"x": {"title": "X", "type": "number"}, "y": {"title": "Y", "type": "number"}}, "required": ["x", "y"], "title": "V1NodePosition", "type": "object"}, "V1ValidateBody": {"description": "Request body for ``POST /v1/branches/{bid}/validate``.", "properties": {"deltas": {"items": {"discriminator": {"mapping": {"add_edge": "#/components/schemas/AddEdgeDelta", "add_node": "#/components/schemas/AddNodeDelta", "delete_edge": "#/components/schemas/DeleteEdgeDelta", "delete_node": "#/components/schemas/DeleteNodeDelta", "flatten_boundary": "#/components/schemas/FlattenBoundaryDelta", "reparent_node": "#/components/schemas/ReparentNodeDelta", "update_node_data": "#/components/schemas/UpdateNodeDataDelta"}, "propertyName": "type"}, "oneOf": [{"$ref": "#/components/schemas/AddNodeDelta"}, {"$ref": "#/components/schemas/DeleteNodeDelta"}, {"$ref": "#/components/schemas/UpdateNodeDataDelta"}, {"$ref": "#/components/schemas/ReparentNodeDelta"}, {"$ref": "#/components/schemas/AddEdgeDelta"}, {"$ref": "#/components/schemas/DeleteEdgeDelta"}, {"$ref": "#/components/schemas/FlattenBoundaryDelta"}]}, "title": "Deltas", "type": "array"}}, "title": "V1ValidateBody", "type": "object"}, "ValidateResponse": {"additionalProperties": false, "properties": {"branch": {"$ref": "#/components/schemas/BranchRef"}, "delta_count": {"title": "Delta Count", "type": "integer"}, "project_id": {"format": "uuid", "title": "Project Id", "type": "string"}, "valid": {"title": "Valid", "type": "boolean"}, "version": {"title": "Version", "type": "string"}}, "required": ["version", "project_id", "branch", "valid", "delta_count"], "title": "ValidateResponse", "type": "object"}, "ValidationError": {"properties": {"ctx": {"title": "Context", "type": "object"}, "input": {"title": "Input"}, "loc": {"items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, "title": "Location", "type": "array"}, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}}, "required": ["loc", "msg", "type"], "title": "ValidationError", "type": "object"}, "Verification": {"description": "A test specification attached to a behavior node.", "properties": {"author": {"enum": ["user", "agent"], "title": "Author", "type": "string"}, "id": {"format": "uuid", "title": "Id", "type": "string"}, "text": {"title": "Text", "type": "string"}, "type": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Type"}}, "required": ["id", "author", "text"], "title": "Verification", "type": "object"}, "VersionConflictBody": {"additionalProperties": false, "properties": {"current_version": {"title": "Current Version", "type": "integer"}, "error": {"const": "version_conflict", "title": "Error", "type": "string"}}, "required": ["error", "current_version"], "title": "VersionConflictBody", "type": "object"}, "WireEdge": {"additionalProperties": false, "description": "Read-path edge: `source`/`target` are server-derived NODE ids\n(absent from the client-authored `h2o.types.Edge`); `*_handle` are the\nPORT ids.", "properties": {"id": {"format": "uuid", "title": "Id", "type": "string"}, "source": {"format": "uuid", "title": "Source", "type": "string"}, "source_handle": {"anyOf": [{"format": "uuid", "type": "string"}, {"type": "null"}], "title": "Source Handle"}, "target": {"format": "uuid", "title": "Target", "type": "string"}, "target_handle": {"anyOf": [{"format": "uuid", "type": "string"}, {"type": "null"}], "title": "Target Handle"}}, "required": ["id", "source", "target", "source_handle", "target_handle"], "title": "WireEdge", "type": "object"}, "WireNode": {"additionalProperties": false, "properties": {"data": {"$ref": "#/components/schemas/WireNodeData"}, "id": {"format": "uuid", "title": "Id", "type": "string"}, "kind": {"enum": ["behavior", "boundary", "state", "io"], "title": "Kind", "type": "string"}, "parent_id": {"anyOf": [{"format": "uuid", "type": "string"}, {"type": "null"}], "title": "Parent Id"}, "position": {"$ref": "#/components/schemas/Position"}}, "required": ["id", "kind", "parent_id", "position", "data"], "title": "WireNode", "type": "object"}, "WireNodeData": {"additionalProperties": false, "description": "Read-path node `data`. NOT `h2o.NodeData`: the response omits\n`kind`/`parent_id` (those live on the wrapper) and emits the boundary/\nexternal scalars matrix-conditionally \u2014 so the matrix fields are\noptional (absent = not-applicable-to-kind).", "properties": {"config": {"items": {"$ref": "#/components/schemas/WirePort"}, "title": "Config", "type": "array"}, "constraints": {"items": {"type": "string"}, "title": "Constraints", "type": "array"}, "description": {"title": "Description", "type": "string"}, "documentation_url": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Documentation Url"}, "external_kind": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "External Kind"}, "inputs": {"items": {"$ref": "#/components/schemas/WirePort"}, "title": "Inputs", "type": "array"}, "is_external": {"title": "Is External", "type": "boolean"}, "is_test_node": {"title": "Is Test Node", "type": "boolean"}, "language": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Language"}, "name": {"title": "Name", "type": "string"}, "outputs": {"items": {"$ref": "#/components/schemas/WirePort"}, "title": "Outputs", "type": "array"}, "path_prefix": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Path Prefix"}, "protocol": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Protocol"}, "source_decisions": {"anyOf": [{"items": {"type": "string"}, "type": "array"}, {"type": "null"}], "default": null, "title": "Source Decisions"}, "status": {"title": "Status", "type": "string"}, "user_kind": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "User Kind"}, "verifications": {"items": {"$ref": "#/components/schemas/WireVerification"}, "title": "Verifications", "type": "array"}}, "required": ["name", "description", "status", "is_test_node", "is_external"], "title": "WireNodeData", "type": "object"}, "WirePort": {"additionalProperties": false, "properties": {"description": {"default": "", "title": "Description", "type": "string"}, "id": {"format": "uuid", "title": "Id", "type": "string"}, "name": {"default": "", "title": "Name", "type": "string"}, "type": {"default": "", "title": "Type", "type": "string"}}, "required": ["id"], "title": "WirePort", "type": "object"}, "WireVerification": {"additionalProperties": false, "properties": {"author": {"enum": ["user", "agent"], "title": "Author", "type": "string"}, "id": {"format": "uuid", "title": "Id", "type": "string"}, "text": {"title": "Text", "type": "string"}, "type": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Type"}}, "required": ["id", "author", "text"], "title": "WireVerification", "type": "object"}}, "securitySchemes": {"Bearer": {"description": "API key. Send as `Authorization: Bearer `.", "scheme": "bearer", "type": "http"}}}, "info": {"description": "The hydrate.sh public API. All routes are scoped to the `/v1/` prefix and require a Bearer API key (see Authentication).\n\n**Stability:** `v1` is the wire-stable surface. Breaking changes will rebase to `/v2/`.", "title": "hydrate.sh v1 API", "version": "1.0.0-rc.1"}, "openapi": "3.1.0", "paths": {"/v1/__healthz": {"get": {"description": "Returns `{\"ok\": true}` when the /v1/ surface is serving traffic. Unauthenticated. Use this for load-balancer and uptime-monitor checks; a 404 from this path means the /v1/ surface is disabled on the responding instance, not that the server is down.", "operationId": "healthz_v1___healthz_get", "responses": {"200": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/HealthzResponse"}}}, "description": "Successful Response"}}, "security": [], "summary": "Liveness probe", "tags": ["health"]}}, "/v1/branches/{branch_id}/deltas": {"post": {"description": "Apply an ordered batch of structural changes to a branch under optimistic concurrency. Pass the `version` from the branch read as `expected_version`; a 409 with `current_version` is returned if the branch moved underneath you. Pair with the `Idempotency-Key` header for at-most-once apply across retries.", "operationId": "apply_branch_deltas_v1_branches__branch_id__deltas_post", "parameters": [{"description": "The branch to apply to.", "in": "path", "name": "branch_id", "required": true, "schema": {"description": "The branch to apply to.", "format": "uuid", "title": "Branch Id", "type": "string"}}], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/V1DeltasBody"}}}, "required": true}, "responses": {"200": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/DeltaApplyResponse"}}}, "description": "Successful Response"}, "401": {"$ref": "#/components/responses/Unauthenticated"}, "403": {"$ref": "#/components/responses/Forbidden"}, "404": {"content": {"application/json": {"schema": {"properties": {"detail": {"oneOf": [{"properties": {"code": {"type": "string"}, "message": {"type": "string"}}, "required": ["code", "message"], "type": "object"}, {"$ref": "#/components/schemas/NotFoundBody"}]}}, "required": ["detail"], "type": "object"}}}, "description": "Branch/project not found or not accessible, OR a delta targets a node that doesn't exist."}, "409": {"content": {"application/json": {"schema": {"properties": {"detail": {"oneOf": [{"$ref": "#/components/schemas/VersionConflictBody"}, {"$ref": "#/components/schemas/BranchNotActiveBody"}]}}, "required": ["detail"], "type": "object"}}}, "description": "OCC version conflict, or the branch is not active."}, "422": {"content": {"application/json": {"schema": {"oneOf": [{"$ref": "#/components/schemas/HTTPValidationError"}, {"properties": {"detail": {"$ref": "#/components/schemas/DeltaFieldErrorBody"}}, "required": ["detail"], "type": "object"}]}}}, "description": "Request body failed validation, OR a delta is malformed / unknown / not an object."}, "429": {"$ref": "#/components/responses/RateLimited"}}, "summary": "Apply a delta batch to a branch (OCC)", "tags": ["branches"]}}, "/v1/branches/{branch_id}/graph": {"get": {"description": "Read the full graph (nodes, ports, edges, version) of a specific branch. Use the returned `version` as `expected_version` on a subsequent apply to that branch.", "operationId": "fetch_branch_graph_v1_branches__branch_id__graph_get", "parameters": [{"description": "The branch to read.", "in": "path", "name": "branch_id", "required": true, "schema": {"description": "The branch to read.", "format": "uuid", "title": "Branch Id", "type": "string"}}], "responses": {"200": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/GraphResponse"}}}, "description": "Successful Response"}, "401": {"$ref": "#/components/responses/Unauthenticated"}, "403": {"$ref": "#/components/responses/Forbidden"}, "404": {"$ref": "#/components/responses/NotFound"}, "422": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}, "description": "Validation Error"}, "429": {"$ref": "#/components/responses/RateLimited"}}, "summary": "Read a specific branch's graph", "tags": ["branches"]}}, "/v1/branches/{branch_id}/validate": {"post": {"description": "Run the same validation pipeline as the branch apply, but roll back before commit. Returns 200 on success (with the projected version) or a structured 422 identifying the first failing delta. Use it before applying a batch you can't recover from.", "operationId": "validate_branch_deltas_v1_branches__branch_id__validate_post", "parameters": [{"description": "The branch to validate against.", "in": "path", "name": "branch_id", "required": true, "schema": {"description": "The branch to validate against.", "format": "uuid", "title": "Branch Id", "type": "string"}}], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/V1ValidateBody"}}}, "required": true}, "responses": {"200": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/ValidateResponse"}}}, "description": "Successful Response"}, "401": {"$ref": "#/components/responses/Unauthenticated"}, "403": {"$ref": "#/components/responses/Forbidden"}, "404": {"content": {"application/json": {"schema": {"properties": {"detail": {"oneOf": [{"properties": {"code": {"type": "string"}, "message": {"type": "string"}}, "required": ["code", "message"], "type": "object"}, {"$ref": "#/components/schemas/NotFoundBody"}]}}, "required": ["detail"], "type": "object"}}}, "description": "Branch/project not found or not accessible, OR a delta targets a node that doesn't exist."}, "422": {"content": {"application/json": {"schema": {"oneOf": [{"$ref": "#/components/schemas/HTTPValidationError"}, {"properties": {"detail": {"$ref": "#/components/schemas/DeltaFieldErrorBody"}}, "required": ["detail"], "type": "object"}]}}}, "description": "Request body failed validation, OR a delta is malformed / unknown / not an object."}, "429": {"$ref": "#/components/responses/RateLimited"}}, "summary": "Dry-run a delta batch on a branch", "tags": ["branches"]}}, "/v1/graph/{project_id}": {"get": {"description": "Returns the active main-branch graph (nodes, ports, edges, version) for the project. Read-only: the main branch is merge-only. To make changes, fork a branch (`POST /v1/projects/{project_id}/branches`) and apply deltas to it.", "operationId": "fetch_graph_v1_graph__project_id__get", "parameters": [{"description": "The project whose main-branch graph to fetch.", "in": "path", "name": "project_id", "required": true, "schema": {"description": "The project whose main-branch graph to fetch.", "format": "uuid", "title": "Project Id", "type": "string"}}], "responses": {"200": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/GraphResponse"}}}, "description": "Successful Response"}, "401": {"$ref": "#/components/responses/Unauthenticated"}, "403": {"$ref": "#/components/responses/Forbidden"}, "404": {"$ref": "#/components/responses/NotFound"}, "422": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}, "description": "Validation Error"}, "429": {"$ref": "#/components/responses/RateLimited"}}, "summary": "Get the canonical graph for a project", "tags": ["graph"]}}, "/v1/graph/{project_id}/boundary/{node_id}": {"get": {"description": "Returns the boundary view for a single node: the inputs and outputs that cross the node's enforcement perimeter. Used by clients that visualize a node's contract surface in isolation from the rest of the graph.", "operationId": "fetch_boundary_v1_graph__project_id__boundary__node_id__get", "parameters": [{"description": "The project the boundary lives in.", "in": "path", "name": "project_id", "required": true, "schema": {"description": "The project the boundary lives in.", "format": "uuid", "title": "Project Id", "type": "string"}}, {"description": "The boundary node to inspect.", "in": "path", "name": "node_id", "required": true, "schema": {"description": "The boundary node to inspect.", "format": "uuid", "title": "Node Id", "type": "string"}}], "responses": {"200": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/BoundaryResponse"}}}, "description": "Successful Response"}, "401": {"$ref": "#/components/responses/Unauthenticated"}, "403": {"$ref": "#/components/responses/Forbidden"}, "404": {"$ref": "#/components/responses/NotFound"}, "422": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}, "description": "Validation Error"}, "429": {"$ref": "#/components/responses/RateLimited"}}, "summary": "Get the boundary topology around a node", "tags": ["graph"]}}, "/v1/graph/{project_id}/node/{node_id}": {"get": {"description": "Returns a single node along with its directly connected neighbors and edges (the 1-hop topology). The node must belong to the project's active main branch.", "operationId": "fetch_node_with_neighbors_v1_graph__project_id__node__node_id__get", "parameters": [{"description": "The project the node lives in.", "in": "path", "name": "project_id", "required": true, "schema": {"description": "The project the node lives in.", "format": "uuid", "title": "Project Id", "type": "string"}}, {"description": "The node to fetch.", "in": "path", "name": "node_id", "required": true, "schema": {"description": "The node to fetch.", "format": "uuid", "title": "Node Id", "type": "string"}}], "responses": {"200": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/NodeNeighborhoodResponse"}}}, "description": "Successful Response"}, "401": {"$ref": "#/components/responses/Unauthenticated"}, "403": {"$ref": "#/components/responses/Forbidden"}, "404": {"$ref": "#/components/responses/NotFound"}, "422": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}, "description": "Validation Error"}, "429": {"$ref": "#/components/responses/RateLimited"}}, "summary": "Get a node with its 1-hop neighborhood", "tags": ["graph"]}}, "/v1/projects": {"get": {"description": "Returns the projects the caller can see. For API-key callers the list is further filtered by the per-key project allowlist (if configured). Archived projects are excluded. An empty list is a normal response when the caller has no visible projects.", "operationId": "list_projects_v1_projects_get", "parameters": [{"description": "Maximum projects to return. Defaults to 100; max 200.", "in": "query", "name": "limit", "required": false, "schema": {"default": 100, "description": "Maximum projects to return. Defaults to 100; max 200.", "maximum": 200, "minimum": 1, "title": "Limit", "type": "integer"}}], "responses": {"200": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/ProjectsListResponse"}}}, "description": "Successful Response"}, "401": {"$ref": "#/components/responses/Unauthenticated"}, "403": {"$ref": "#/components/responses/Forbidden"}, "422": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}, "description": "Validation Error"}, "429": {"$ref": "#/components/responses/RateLimited"}}, "summary": "List your projects", "tags": ["projects"]}}, "/v1/projects/{project_id}/branches": {"get": {"description": "List your active working branches on this project. The protected main branch is not included. Each branch is a unit of work you can read and apply changes to before merging into main.", "operationId": "list_branches_v1_projects__project_id__branches_get", "parameters": [{"description": "The project to list branches for.", "in": "path", "name": "project_id", "required": true, "schema": {"description": "The project to list branches for.", "format": "uuid", "title": "Project Id", "type": "string"}}], "responses": {"200": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/BranchListResponse"}}}, "description": "Successful Response"}, "401": {"$ref": "#/components/responses/Unauthenticated"}, "403": {"$ref": "#/components/responses/Forbidden"}, "404": {"$ref": "#/components/responses/NotFound"}, "422": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}, "description": "Validation Error"}, "429": {"$ref": "#/components/responses/RateLimited"}}, "summary": "List the caller's working branches on a project", "tags": ["branches"]}, "post": {"description": "Fork a new working branch from the project's main branch. The branch is copied from main's current state and owned by you; your changes stay isolated on it until merged. Requires an editor role, and there is a per-project limit on how many active branches you can hold at once.", "operationId": "create_branch_v1_projects__project_id__branches_post", "parameters": [{"description": "The project to fork a branch in.", "in": "path", "name": "project_id", "required": true, "schema": {"description": "The project to fork a branch in.", "format": "uuid", "title": "Project Id", "type": "string"}}], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/V1CreateBranchBody"}}}, "required": true}, "responses": {"201": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/BranchCreateResponse"}}}, "description": "Successful Response"}, "401": {"$ref": "#/components/responses/Unauthenticated"}, "403": {"$ref": "#/components/responses/Forbidden"}, "404": {"$ref": "#/components/responses/NotFound"}, "422": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}, "description": "Validation Error"}, "429": {"$ref": "#/components/responses/RateLimited"}}, "summary": "Start a unit of work (fork a branch from main)", "tags": ["branches"]}}}, "security": [{"Bearer": []}], "servers": [{"description": "Production", "url": "https://api.hydrate.sh"}], "tags": [{"description": "Work on a branch (a unit of work) rather than the protected main branch: fork a branch from main, list your branches, and read / validate / apply deltas against a specific branch.", "name": "branches"}, {"description": "Read the canonical graph for a project, validate a delta batch (dry-run), or apply a delta batch through the OCC + idempotency middleware chain.", "name": "graph"}, {"description": "Liveness probes. Unauthenticated. Used by load balancers and uptime monitors.", "name": "health"}, {"description": "List the projects your principal can see. Filtered by membership + (for API keys) the per-key project allowlist.", "name": "projects"}]} \ No newline at end of file diff --git a/src/cli.rs b/src/cli.rs index 3199470..88a1a5a 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -226,6 +226,10 @@ pub struct NodeSetArgs { #[arg(long)] pub path_prefix: Option, + /// Codegen language for a boundary node (e.g. `go`, `python`). Boundary-only. + #[arg(long)] + pub language: Option, + /// Mark the node external (an outside system the graph depends on). #[arg(long, conflicts_with = "no_external")] pub external: bool, @@ -276,6 +280,10 @@ pub struct NodeSetArgs { #[arg(long, conflicts_with = "path_prefix")] pub clear_path_prefix: bool, + /// Clear the boundary language (set it null). + #[arg(long, conflicts_with = "language")] + pub clear_language: bool, + /// Clear the external kind label (set it null). #[arg(long, conflicts_with = "external_kind")] pub clear_external_kind: bool, @@ -360,6 +368,10 @@ pub struct NodeAddArgs { #[arg(long)] pub path_prefix: Option, + /// Codegen language for a boundary node (e.g. `go`, `python`). Boundary-only. + #[arg(long)] + pub language: Option, + /// External-only: the system's protocol (e.g. `gRPC`, `HTTPS REST`). #[arg(long)] pub protocol: Option, diff --git a/src/cmd/diff.rs b/src/cmd/diff.rs index bcb5978..62dbf1b 100644 --- a/src/cmd/diff.rs +++ b/src/cmd/diff.rs @@ -98,6 +98,7 @@ fn op_line(op: &OpSummary) -> String { outputs, user_kind, path_prefix, + language, external, external_kind, protocol, @@ -137,6 +138,7 @@ fn op_line(op: &OpSummary) -> String { } line.push_str(&scalar_line(user_kind, "user-kind")); line.push_str(&scalar_line(path_prefix, "path-prefix")); + line.push_str(&scalar_line(language, "language")); line.push_str(&scalar_line(external_kind, "external-kind")); line.push_str(&scalar_line(protocol, "protocol")); line.push_str(&scalar_line(doc_url, "doc-url")); @@ -232,6 +234,7 @@ fn op_json(op: &OpSummary) -> serde_json::Value { outputs, user_kind, path_prefix, + language, external, external_kind, protocol, @@ -259,6 +262,7 @@ fn op_json(op: &OpSummary) -> serde_json::Value { for (key, field) in [ ("user_kind", user_kind), ("path_prefix", path_prefix), + ("language", language), ("external_kind", external_kind), ("protocol", protocol), ("doc_url", doc_url), @@ -454,6 +458,7 @@ mod tests { ]), user_kind: None, path_prefix: None, + language: None, external: None, external_kind: None, protocol: None, @@ -540,6 +545,7 @@ mod tests { outputs: None, user_kind: Some(Some("subsystem".to_string())), path_prefix: Some(Some("src/api/".to_string())), + language: None, external: Some(true), external_kind: Some(Some("rest-api".to_string())), protocol: None, @@ -575,6 +581,7 @@ mod tests { outputs: None, user_kind: Some(None), path_prefix: None, + language: None, external: None, external_kind: None, protocol: Some(Some("gRPC".to_string())), @@ -636,6 +643,7 @@ mod tests { outputs: None, user_kind: None, path_prefix: None, + language: None, external: None, external_kind: None, protocol: None, @@ -694,6 +702,7 @@ mod tests { outputs: None, user_kind: None, path_prefix: None, + language: None, external: None, external_kind: None, protocol: None, @@ -711,6 +720,7 @@ mod tests { outputs: None, user_kind: None, path_prefix: None, + language: None, external: None, external_kind: None, protocol: None, @@ -739,6 +749,7 @@ mod tests { outputs: None, user_kind: None, path_prefix: None, + language: None, external: None, external_kind: None, protocol: None, diff --git a/src/cmd/node.rs b/src/cmd/node.rs index 8500d39..e32d60b 100644 --- a/src/cmd/node.rs +++ b/src/cmd/node.rs @@ -31,6 +31,7 @@ pub fn add(args: NodeAddArgs, mode: OutputMode) -> Result<(), CliError> { config: parse_ports(&args.config)?, user_kind: args.user_kind.as_deref(), path_prefix: args.path_prefix.as_deref(), + language: args.language.as_deref(), description: args.description.as_deref(), constraints: args.constraints.clone(), is_external: args.external, @@ -92,6 +93,7 @@ pub fn set(args: NodeSetArgs, mode: OutputMode) -> Result<(), CliError> { // Scalars: a blank value is "untouched", mirroring `--description ""`. user_kind: blank_to_none(args.user_kind.as_deref()), path_prefix: blank_to_none(args.path_prefix.as_deref()), + language: blank_to_none(args.language.as_deref()), // --external / --no-external toggle is_external; neither = untouched. is_external: toggle_flag(args.external, args.no_external), external_kind: blank_to_none(args.external_kind.as_deref()), @@ -102,6 +104,7 @@ pub fn set(args: NodeSetArgs, mode: OutputMode) -> Result<(), CliError> { clear_description: args.clear_description, clear_user_kind: args.clear_user_kind, clear_path_prefix: args.clear_path_prefix, + clear_language: args.clear_language, clear_external_kind: args.clear_external_kind, clear_protocol: args.clear_protocol, clear_doc_url: args.clear_doc_url, @@ -198,6 +201,7 @@ fn render_updated(u: &NodeUpdated, mode: OutputMode) -> String { for (key, field) in [ ("user_kind", &u.user_kind), ("path_prefix", &u.path_prefix), + ("language", &u.language), ("external_kind", &u.external_kind), ("protocol", &u.protocol), ("doc_url", &u.doc_url), @@ -237,6 +241,7 @@ fn render_updated(u: &NodeUpdated, mode: OutputMode) -> String { } fields.extend(scalar_label(&u.user_kind, "user-kind")); fields.extend(scalar_label(&u.path_prefix, "path-prefix")); + fields.extend(scalar_label(&u.language, "language")); fields.extend(scalar_label(&u.external_kind, "external-kind")); fields.extend(scalar_label(&u.protocol, "protocol")); fields.extend(scalar_label(&u.doc_url, "doc-url")); @@ -433,6 +438,7 @@ mod tests { constraints: None, user_kind: None, path_prefix: None, + language: None, is_external: None, external_kind: None, protocol: None, @@ -453,6 +459,7 @@ mod tests { constraints: None, user_kind: None, path_prefix: None, + language: None, is_external: None, external_kind: None, protocol: None, @@ -478,6 +485,7 @@ mod tests { constraints: Some(vec!["c".to_string()]), user_kind: None, path_prefix: None, + language: None, is_external: None, external_kind: None, protocol: None, @@ -500,6 +508,7 @@ mod tests { constraints: Some(vec![]), user_kind: None, path_prefix: None, + language: None, is_external: None, external_kind: None, protocol: None, @@ -521,6 +530,7 @@ mod tests { constraints: None, user_kind: None, path_prefix: None, + language: None, is_external: None, external_kind: None, protocol: None, @@ -573,6 +583,7 @@ mod tests { constraints: None, user_kind: Some(Some("subsystem".to_string())), path_prefix: Some(Some("src/api/".to_string())), + language: None, is_external: Some(true), external_kind: Some(Some("rest-api".to_string())), protocol: None, @@ -609,6 +620,7 @@ mod tests { constraints: None, user_kind: Some(None), // cleared path_prefix: None, // untouched + language: None, is_external: None, external_kind: None, protocol: Some(Some("gRPC".to_string())), // set @@ -647,6 +659,7 @@ mod tests { constraints: None, user_kind: None, path_prefix: None, + language: None, is_external: None, external_kind: None, protocol: None, diff --git a/src/staging/mod.rs b/src/staging/mod.rs index c7e37b6..4753cb1 100644 --- a/src/staging/mod.rs +++ b/src/staging/mod.rs @@ -74,6 +74,8 @@ pub struct NodeSpec<'a> { pub config: Vec, pub user_kind: Option<&'a str>, pub path_prefix: Option<&'a str>, + /// Codegen language for a boundary node (e.g. `go`). Boundary-only. + pub language: Option<&'a str>, /// The node's description (free text). `None` omits it (server default). pub description: Option<&'a str>, /// Plain-text constraints; empty omits the field. @@ -115,6 +117,8 @@ pub struct NodeEdit { pub user_kind: Option, /// Boundary path prefix (`--path-prefix`). `None` = untouched. pub path_prefix: Option, + /// Boundary codegen language (`--language`). `None` = untouched. + pub language: Option, /// External marker toggle. `None` = untouched, `Some(true)` = `--external`, /// `Some(false)` = `--no-external`. pub is_external: Option, @@ -134,6 +138,7 @@ pub struct NodeEdit { pub clear_description: bool, pub clear_user_kind: bool, pub clear_path_prefix: bool, + pub clear_language: bool, pub clear_external_kind: bool, pub clear_protocol: bool, pub clear_doc_url: bool, @@ -169,6 +174,7 @@ impl NodeEdit { && self.name.is_none() && self.user_kind.is_none() && self.path_prefix.is_none() + && self.language.is_none() && self.is_external.is_none() && self.external_kind.is_none() && self.verifications.is_none() @@ -178,6 +184,7 @@ impl NodeEdit { && !self.clear_description && !self.clear_user_kind && !self.clear_path_prefix + && !self.clear_language && !self.clear_external_kind && !self.clear_protocol && !self.clear_doc_url @@ -273,10 +280,11 @@ impl Changeset { // Classifier/ownership flags must only appear on the kinds that accept // them — a command-grammar mistake we can catch locally and clearly. - // - boundary: both --user-kind and --path-prefix are allowed. - // - state: --user-kind carries the state kind; --path-prefix is forbidden. - // - behavior: neither is allowed. - // - io: neither is allowed (an io node is defined by its + // - boundary: --user-kind, --path-prefix, and --language are allowed. + // - state: --user-kind carries the state kind; --path-prefix and + // --language are forbidden. + // - behavior: none is allowed. + // - io: none is allowed (an io node is defined by its // description and its single typed port — no kind label). match spec.kind { models::node::Kind::Boundary => {} @@ -287,12 +295,18 @@ impl Changeset { .to_string(), )); } + if spec.language.is_some() { + return Err(CliError::InvalidArgument( + "--language applies only to --kind boundary, not --kind state".to_string(), + )); + } } models::node::Kind::Behavior | models::node::Kind::Io => { - if spec.user_kind.is_some() || spec.path_prefix.is_some() { + if spec.user_kind.is_some() || spec.path_prefix.is_some() || spec.language.is_some() + { return Err(CliError::InvalidArgument( "--user-kind applies to --kind boundary or state; \ - --path-prefix applies only to --kind boundary" + --path-prefix and --language apply only to --kind boundary" .to_string(), )); } @@ -339,6 +353,7 @@ impl Changeset { config: Some(config.deltas), user_kind: spec.user_kind.map(|k| Some(k.to_string())), path_prefix: spec.path_prefix.map(|p| Some(p.to_string())), + language: spec.language.map(|p| Some(p.to_string())), // Description and constraints: omit when absent OR empty // so the server applies its own defaults rather than us forcing "". // Filtering here (not just in the diff preview) keeps the staged @@ -596,6 +611,7 @@ impl Changeset { // untouched → None. user_kind: scalar_or_clear(&edit.user_kind, edit.clear_user_kind), path_prefix: scalar_or_clear(&edit.path_prefix, edit.clear_path_prefix), + language: scalar_or_clear(&edit.language, edit.clear_language), is_external: edit.is_external, is_test_node: edit.is_test_node, external_kind: scalar_or_clear(&edit.external_kind, edit.clear_external_kind), @@ -645,6 +661,7 @@ impl Changeset { constraints: edit.constraints.clone(), user_kind: scalar_or_clear(&edit.user_kind, edit.clear_user_kind), path_prefix: scalar_or_clear(&edit.path_prefix, edit.clear_path_prefix), + language: scalar_or_clear(&edit.language, edit.clear_language), is_external: edit.is_external, external_kind: scalar_or_clear(&edit.external_kind, edit.clear_external_kind), protocol: scalar_or_clear(&edit.protocol, edit.clear_protocol), @@ -1051,6 +1068,7 @@ pub struct NodeUpdated { /// `Some(Some(v))` = set. pub user_kind: Option>, pub path_prefix: Option>, + pub language: Option>, pub is_external: Option, pub external_kind: Option>, pub protocol: Option>, @@ -1416,6 +1434,7 @@ pub enum OpSummary { /// `Some(Some(v))` = set. user_kind: Option>, path_prefix: Option>, + language: Option>, external: Option, external_kind: Option>, protocol: Option>, @@ -1572,6 +1591,7 @@ pub fn summarize(stage: &Stage, index: Option<&Index>) -> Result, #[serde(rename = "kind", default, with = "::serde_with::rust::double_option", skip_serializing_if = "Option::is_none")] pub kind: Option>, + #[serde(rename = "language", default, with = "::serde_with::rust::double_option", skip_serializing_if = "Option::is_none")] + pub language: Option>, #[serde(rename = "name", skip_serializing_if = "Option::is_none")] pub name: Option, #[serde(rename = "outputs", skip_serializing_if = "Option::is_none")] @@ -65,6 +67,7 @@ impl NodeData { is_external: None, is_test_node: None, kind: None, + language: None, name: None, outputs: None, parent_id: None, diff --git a/wire/src/models/wire_node_data.rs b/wire/src/models/wire_node_data.rs index db12221..54c08d6 100644 --- a/wire/src/models/wire_node_data.rs +++ b/wire/src/models/wire_node_data.rs @@ -30,6 +30,8 @@ pub struct WireNodeData { pub is_external: bool, #[serde(rename = "is_test_node")] pub is_test_node: bool, + #[serde(rename = "language", default, with = "::serde_with::rust::double_option", skip_serializing_if = "Option::is_none")] + pub language: Option>, #[serde(rename = "name")] pub name: String, #[serde(rename = "outputs", skip_serializing_if = "Option::is_none")] @@ -60,6 +62,7 @@ impl WireNodeData { inputs: None, is_external, is_test_node, + language: None, name, outputs: None, path_prefix: None, From a0dbb77b57652a79fa6d9049858f3614e687c1b4 Mon Sep 17 00:00:00 2001 From: Doug Rennehan Date: Sun, 5 Jul 2026 12:28:35 -0400 Subject: [PATCH 2/2] =?UTF-8?q?fix(node):=20address=20review=20findings=20?= =?UTF-8?q?=E2=80=94=20language=20in=20set-hint=20+=20diff=20render=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The update "nothing to set" hint now lists --language among the field flags. - The scalar-render test asserts a non-null language renders in both human (language: go) and JSON output (was language: None, so the render path was unpinned). - Completeness: the lone-scalar and lone-clear parametrized tables now include language / clear_language. clippy -D warnings clean; 292 lib tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/cmd/diff.rs | 4 +++- src/staging/mod.rs | 25 +++++++++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/cmd/diff.rs b/src/cmd/diff.rs index 62dbf1b..94aa2c8 100644 --- a/src/cmd/diff.rs +++ b/src/cmd/diff.rs @@ -545,7 +545,7 @@ mod tests { outputs: None, user_kind: Some(Some("subsystem".to_string())), path_prefix: Some(Some("src/api/".to_string())), - language: None, + language: Some(Some("go".to_string())), external: Some(true), external_kind: Some(Some("rest-api".to_string())), protocol: None, @@ -557,6 +557,7 @@ mod tests { let human = render(&summary(vec![op.clone()]), OutputMode::Human); assert!(human.contains("user-kind: subsystem"), "{human}"); assert!(human.contains("path-prefix: src/api/"), "{human}"); + assert!(human.contains("language: go"), "{human}"); assert!(human.contains("external: true"), "{human}"); assert!(human.contains("external-kind: rest-api"), "{human}"); assert!(human.contains("verification: responds in 50ms"), "{human}"); @@ -565,6 +566,7 @@ mod tests { serde_json::from_str(&render(&summary(vec![op]), OutputMode::Json)).unwrap(); assert_eq!(v["ops"][0]["user_kind"], "subsystem"); assert_eq!(v["ops"][0]["path_prefix"], "src/api/"); + assert_eq!(v["ops"][0]["language"], "go"); assert_eq!(v["ops"][0]["external"], true); assert_eq!(v["ops"][0]["external_kind"], "rest-api"); assert_eq!(v["ops"][0]["verifications"][0], "responds in 50ms"); diff --git a/src/staging/mod.rs b/src/staging/mod.rs index 4753cb1..8c7816d 100644 --- a/src/staging/mod.rs +++ b/src/staging/mod.rs @@ -574,9 +574,10 @@ impl Changeset { if edit.is_empty() { return Err(CliError::InvalidArgument( "nothing to set — pass a field flag (--description, --constraint, \ - --name, --user-kind, --path-prefix, --external/--no-external, \ - --external-kind, --protocol, --doc-url, --test-node/--no-test-node, \ - --verification, or a --clear-* flag) or a port flag" + --name, --user-kind, --path-prefix, --language, \ + --external/--no-external, --external-kind, --protocol, --doc-url, \ + --test-node/--no-test-node, --verification, or a --clear-* flag) \ + or a port flag" .to_string(), )); } @@ -3945,7 +3946,7 @@ mod tests { fn update_node_accepts_a_lone_boundary_scalar() { // Each new scalar alone must satisfy is_empty (not be rejected as // "nothing to set") and land in the delta. - let cases: [(&str, NodeEdit, Option>); 3] = [ + let cases: [(&str, NodeEdit, Option>); 4] = [ ( "user_kind", NodeEdit { @@ -3962,6 +3963,14 @@ mod tests { }, Some(Some("src/".to_string())), ), + ( + "language", + NodeEdit { + language: Some("go".to_string()), + ..Default::default() + }, + Some(Some("go".to_string())), + ), ( "external_kind", NodeEdit { @@ -3979,6 +3988,7 @@ mod tests { let got = match label { "user_kind" => d.after.user_kind, "path_prefix" => d.after.path_prefix, + "language" => d.after.language, _ => d.after.external_kind, }; assert_eq!(got, want, "{label}"); @@ -4011,6 +4021,13 @@ mod tests { ..Default::default() }, ), + ( + "clear_language", + NodeEdit { + clear_language: true, + ..Default::default() + }, + ), ( "clear_external_kind", NodeEdit {