{
  "schema_version": "2",
  "package": "@unified-product-graph/cloud-server",
  "package_version": "0.41.0",
  "tool_count": 54,
  "domains": [
    "products",
    "context",
    "nodes",
    "edges",
    "frameworks",
    "areas",
    "schema",
    "collaboration",
    "analytics",
    "webhooks",
    "spec",
    "portfolio",
    "batch",
    "validation",
    "migrations"
  ],
  "tools": [
    {
      "name": "create_product",
      "description": "Create a new product graph.",
      "domain": "products",
      "inputSchema": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string",
            "description": "Product name"
          },
          "description": {
            "type": "string",
            "description": "Optional description"
          },
          "stage": {
            "type": "string",
            "description": "idea | mvp | growth | scale"
          }
        },
        "required": [
          "title"
        ]
      },
      "throws": [
        "textError when `title` is missing."
      ],
      "examples": [],
      "warnings": [
        "Billing-relevant: product count typically drives plan tier;\ncreation may trigger a tier upgrade or hit the plan's product cap."
      ],
      "see": [
        "list_products",
        "grant_access",
        "list_product_stages"
      ],
      "source": "src/tools/products.ts:39",
      "symbol": "createProduct",
      "returns": "JSON: `{ product: { id, title, description?, stage? } }`.",
      "return_shape": "{ product: { id, title, description?, stage? } }",
      "atomicity": "atomic"
    },
    {
      "name": "get_audit_log",
      "description": "Get recent changes (audit log) for a product.",
      "domain": "products",
      "inputSchema": {
        "type": "object",
        "properties": {
          "product_id": {
            "type": "string",
            "description": "Product ID"
          },
          "limit": {
            "type": "number",
            "description": "Max entries (default 50)"
          }
        },
        "required": [
          "product_id"
        ]
      },
      "throws": [
        "textError when `product_id` is missing."
      ],
      "examples": [],
      "warnings": [
        "Retention-windowed: entries beyond the plan-tier retention period\nare pruned. An empty window may mean \"out of retention\", not \"no activity\"."
      ],
      "see": [
        "get_graph_analytics",
        "get_graph_digest",
        "list_products"
      ],
      "source": "src/tools/products.ts:62",
      "symbol": "getAuditLog",
      "returns": "JSON: `{ entries: Array<{ ...mutation }> }`.",
      "return_shape": "{ entries: Array<{...mutation }> }",
      "atomicity": "atomic (read-only)"
    },
    {
      "name": "list_products",
      "description": "List all products in this UPG cloud instance.",
      "domain": "products",
      "inputSchema": {
        "type": "object",
        "properties": {}
      },
      "throws": [],
      "examples": [],
      "warnings": [
        "RLS-bounded; an empty list can mean \"no products\" or \"no access\".\nPair with `list_collaborators` to confirm scope on a specific product."
      ],
      "see": [
        "create_product",
        "get_product_context",
        "get_graph_digest",
        "list_collaborators"
      ],
      "source": "src/tools/products.ts:22",
      "symbol": "listProducts",
      "returns": "JSON: `{ products: Array<{ id, title, description?, stage? }> }`.",
      "return_shape": "{ products: Array<{ id, title, description?, stage? }> }",
      "atomicity": "atomic (read-only)"
    },
    {
      "name": "get_changes",
      "description": "Get a log of recent changes from the audit log.",
      "domain": "context",
      "inputSchema": {
        "type": "object",
        "properties": {
          "product_id": {
            "type": "string",
            "description": "The product ID"
          },
          "since": {
            "type": "string",
            "description": "ISO 8601 timestamp; only return changes after this time"
          },
          "limit": {
            "type": "number",
            "description": "Max results (default 50)"
          }
        },
        "required": [
          "product_id"
        ]
      },
      "throws": [
        "textError when `product_id` is missing."
      ],
      "examples": [],
      "warnings": [
        "Backed by the audit log: entries beyond the plan-tier\nretention window are pruned and stay out of this surface. The `since`\nfilter runs in-memory after the store fetches up to `limit` entries,\nso narrow `since` windows on busy products may surface fewer rows\nthan expected (raise `limit` to compensate)."
      ],
      "see": [
        "get_audit_log",
        "get_graph_digest",
        "get_product_context"
      ],
      "source": "src/tools/context.ts:329",
      "symbol": "getChanges",
      "returns": "JSON: `{ changes: AuditEntry[], total }`.",
      "return_shape": "{ changes: AuditEntry[], total }",
      "atomicity": "atomic (read-only)"
    },
    {
      "name": "get_graph_digest",
      "description": "Pre-computed graph analytics: counts, health metrics, chain completeness, business area coverage, lifecycle balance. ~500 tokens vs ~5-8K for equivalent manual fetches.",
      "domain": "context",
      "inputSchema": {
        "type": "object",
        "properties": {
          "product_id": {
            "type": "string",
            "description": "The product ID"
          }
        },
        "required": [
          "product_id"
        ]
      },
      "throws": [
        "textError when `product_id` is missing."
      ],
      "examples": [],
      "warnings": [
        "Chain keys carry v0.1 names (`persona_with_jtbd`,\n`hypothesis_total`) pending a canonical rename. Lifecycle bucketing\nuses local heuristics (`BUSINESS_AREAS` / `LIFECYCLE_PHASES`\nconstants in this file) rather than the canonical `UPG_DOMAINS` ring,\nso it may drift from spec across versions."
      ],
      "see": [
        "get_product_context",
        "get_graph_analytics",
        "list_benchmarks",
        "validate_graph"
      ],
      "source": "src/tools/context.ts:100",
      "symbol": "getGraphDigest",
      "returns": "JSON with `product`, `counts`, `health`, `chains`, `coverage`,\n`lifecycle` keys (~500 tokens of summary). Note: chain keys still use the\nv0.1 names (`persona_with_jtbd` etc.) pending a canonical rename.",
      "return_notes": [
        "JSON with `product`, `counts`, `health`, `chains`, `coverage`, `lifecycle` keys (~500 tokens of summary).",
        "Note: chain keys still use the v0.1 names (`persona_with_jtbd` etc.) pending a canonical rename."
      ],
      "atomicity": "atomic (read-only)"
    },
    {
      "name": "get_product_context",
      "description": "Returns the product summary, entity counts by type, and a human-readable overview of the graph. Use this first to understand what is in the graph.",
      "domain": "context",
      "inputSchema": {
        "type": "object",
        "properties": {
          "product_id": {
            "type": "string",
            "description": "Product ID"
          }
        },
        "required": [
          "product_id"
        ]
      },
      "throws": [
        "textError when `product_id` is missing or the product\nis not visible to the caller."
      ],
      "examples": [],
      "warnings": [],
      "see": [
        "get_graph_digest",
        "get_graph_analytics",
        "get_entity_schema",
        "list_nodes"
      ],
      "source": "src/tools/context.ts:45",
      "symbol": "getProductContext",
      "returns": "Text: `## <product title>` followed by description/stage,\ngraph stats (node/edge/type counts), and a sorted breakdown of entities\nper type. Errors with `Product not found: <id>` for unknown products.",
      "return_notes": [
        "Text: `## <product title>` followed by description/stage, graph stats (node/edge/type counts), and a sorted breakdown of entities per type.",
        "Errors with `Product not found: <id>` for unknown products."
      ],
      "atomicity": "atomic (read-only)"
    },
    {
      "name": "get_tree",
      "description": "Assemble a canonical tree pattern (ost, okr, user, product, validation, strategy, feature_areas, delivery, architecture, journey, design_system, commercial, north_star, org) from the product graph. Walks the pattern's type-driven child map over the live graph (drift-proof, follows whatever edge wired each parent to a child of the expected type), roots at the pattern anchor with fallback, and reports structural gaps. Returns nested data, not rendered text.",
      "domain": "context",
      "inputSchema": {
        "type": "object",
        "properties": {
          "product_id": {
            "type": "string",
            "description": "The product ID"
          },
          "pattern": {
            "type": "string",
            "description": "Tree pattern id: ost, okr, user, product, validation, strategy, feature_areas, delivery, architecture, journey, design_system, commercial, north_star, org"
          },
          "from_id": {
            "type": "string",
            "description": "Explicit root node id. Defaults to the pattern's canonical anchor type."
          },
          "depth": {
            "type": "number",
            "description": "Max levels. Defaults to the pattern's natural depth."
          },
          "include_properties": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Node property keys to inline on each tree node."
          },
          "max_nodes": {
            "type": "number",
            "description": "Cap on assembled nodes. The tree is summarised (stats.truncated) rather than silently cut."
          }
        },
        "required": [
          "product_id",
          "pattern"
        ]
      },
      "throws": [
        "textError when `product_id` or `pattern` is missing or `pattern` is unknown."
      ],
      "examples": [],
      "warnings": [],
      "see": [
        "query",
        "list_playbooks"
      ],
      "source": "src/tools/tree.ts:49",
      "symbol": "getTree",
      "returns": "JSON: `{ pattern, framework_id?, anchor_type, anchor_used,\nanchor_resolved_from?, roots: TreeNode[], stats: { nodes, levels, truncated },\ngaps: [{ node_id, type, title, missing }] }`. Structured data, never rendered text.",
      "return_shape": "{ pattern, framework_id?, anchor_type, anchor_used, anchor_resolved_from?, roots: TreeNode[], stats: { nodes, levels, truncated }, gaps: [{ node_id, type, title, missing }] }",
      "return_notes": [
        "Structured data, never rendered text."
      ],
      "atomicity": "atomic (read-only). Reads the named product only."
    },
    {
      "name": "query",
      "description": "Traverse the graph following typed edges. Returns a subgraph in a single call. Replaces multi-step fetch patterns. Supports edge type filtering (including !negation), field projection, and truncation metadata.",
      "domain": "context",
      "inputSchema": {
        "type": "object",
        "properties": {
          "product_id": {
            "type": "string",
            "description": "The product ID"
          },
          "from": {
            "type": "string",
            "description": "Start from all nodes of this type"
          },
          "from_id": {
            "type": "string",
            "description": "Start from a specific node ID"
          },
          "traverse": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Edge types to follow per level. Prefix with ! to exclude."
          },
          "depth": {
            "type": "number",
            "description": "Max depth (default 3, max 10)"
          },
          "include": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Node fields: \"title\", \"status\", \"tags\", \"description\", \"properties\""
          },
          "limit": {
            "type": "number",
            "description": "Max nodes (default 200, max 1000)"
          },
          "edge_include": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Edge fields to return. Empty = no edges."
          }
        },
        "required": [
          "product_id"
        ]
      },
      "throws": [
        "textError when `product_id` is missing, or when\nneither `from` nor `from_id` is provided, or when `from_id` does not\nresolve."
      ],
      "examples": [],
      "warnings": [
        "Pre-loads the entire product graph into memory before\nfiltering; for products beyond ~10K nodes this can be heavy. Use\n`from_id` plus a tight `depth` for narrow slices, and pair with\n`include` / `edge_include` to trim wire payload. Truncation is silent\nbeyond `limit`, so check `truncated` before assuming the result is\ncomplete."
      ],
      "see": [
        "list_nodes",
        "get_node",
        "get_area_graph",
        "search_nodes",
        "resolve_edge_for_pair",
        "trace"
      ],
      "source": "src/tools/context.ts:203",
      "symbol": "query",
      "returns": "JSON: `{ nodes, edges, total_nodes, total_edges,\ntruncated?, truncated_at_depth?, hint? }`. Truncates with a hint when\n`limit` is reached.",
      "return_shape": "{ nodes, edges, total_nodes, total_edges, truncated?, truncated_at_depth?, hint? }",
      "return_notes": [
        "Truncates with a hint when `limit` is reached."
      ],
      "atomicity": "atomic (read-only)"
    },
    {
      "name": "create_node",
      "description": "Create a new entity in the graph. Optionally connect it to a parent node.",
      "domain": "nodes",
      "inputSchema": {
        "type": "object",
        "properties": {
          "product_id": {
            "type": "string",
            "description": "Product ID"
          },
          "type": {
            "type": "string",
            "description": "UPG entity type (e.g. \"persona\", \"opportunity\")"
          },
          "title": {
            "type": "string",
            "description": "Entity title"
          },
          "description": {
            "type": "string",
            "description": "Optional description"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Freeform tags"
          },
          "status": {
            "type": "string",
            "description": "Lifecycle status"
          },
          "properties": {
            "type": "object",
            "description": "Type-specific fields"
          },
          "parent_id": {
            "type": "string",
            "description": "Parent node ID; creates an edge automatically"
          }
        },
        "required": [
          "product_id",
          "type",
          "title"
        ]
      },
      "throws": [
        "textError when `product_id`, `type`, or `title` is\nmissing."
      ],
      "examples": [],
      "warnings": [
        "Pass `parent_id` to auto-create a containment edge with inferred\ntype; missing parents are reported via `warning` rather than failing\nthe create."
      ],
      "see": [
        "batch_create_nodes",
        "update_node",
        "get_entity_schema",
        "list_entity_types",
        "get_valid_children"
      ],
      "source": "src/tools/nodes.ts:325",
      "symbol": "createNode",
      "returns": "JSON: `{ node, edge?, warning? }`. `edge` is null when no\n`parent_id` is passed; `warning` is present on lifecycle/parent issues.",
      "return_shape": "{ node, edge?, warning? }",
      "return_notes": [
        "`edge` is null when no `parent_id` is passed; `warning` is present on lifecycle/parent issues."
      ],
      "atomicity": "atomic-with-rollback"
    },
    {
      "name": "deduplicate_nodes",
      "description": "Merge a set of duplicate nodes into a canonical node. Rebinds all edges from duplicates to canonical, removes self-loops and duplicate edges, merges properties (canonical wins on conflicts), then deletes the duplicates inside a single atomic Postgres transaction. Default dry_run: true previews the operation without modifying data.",
      "domain": "nodes",
      "inputSchema": {
        "type": "object",
        "properties": {
          "product_id": {
            "type": "string",
            "description": "Product ID"
          },
          "canonical_id": {
            "type": "string",
            "description": "The node to keep"
          },
          "duplicate_ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Nodes to merge into canonical and delete (max 20)"
          },
          "dry_run": {
            "type": "boolean",
            "description": "Default true: report what would happen without changing anything."
          }
        },
        "required": [
          "product_id",
          "canonical_id",
          "duplicate_ids"
        ]
      },
      "throws": [
        "textError when `product_id`, `canonical_id`, or\n`duplicate_ids` are missing, when the arrays exceed limits, when\n`canonical_id` appears in `duplicate_ids`, or when any node does not\nexist / does not belong to the product."
      ],
      "examples": [],
      "warnings": [
        "Default `dry_run: true`; pass `dry_run: false` to commit. The\nmerge is permanent: duplicates are deleted, their edges rebound to\nthe canonical, self-loops removed, and duplicate edges deduplicated.\nThe change stands once committed (no undo); the audit log records\neach merge for the retention window."
      ],
      "see": [
        "search_nodes",
        "get_nodes",
        "delete_node",
        "validate_graph"
      ],
      "source": "src/tools/nodes.ts:602",
      "symbol": "deduplicateNodes",
      "returns": "With `dry_run: true` (default): `{ canonical_id, duplicate_ids,\nedges_to_rebind, nodes_to_delete, dry_run }`. With `dry_run: false`:\n`{ canonical_id, merged_ids, rebound_edges, removed_self_loops,\nremoved_duplicate_edges, dry_run }`.",
      "return_notes": [
        "With `dry_run: true` (default): `{ canonical_id, duplicate_ids, edges_to_rebind, nodes_to_delete, dry_run }`.",
        "With `dry_run: false`: `{ canonical_id, merged_ids, rebound_edges, removed_self_loops, removed_duplicate_edges, dry_run }`."
      ],
      "atomicity": "atomic-with-rollback (all mutations committed or rolled back\ntogether)."
    },
    {
      "name": "delete_node",
      "description": "Remove an entity and all its connected edges from the graph.",
      "domain": "nodes",
      "inputSchema": {
        "type": "object",
        "properties": {
          "node_id": {
            "type": "string",
            "description": "The node ID to delete"
          }
        },
        "required": [
          "node_id"
        ]
      },
      "throws": [
        "textError when `node_id` is missing or the store\nrejects the deletion."
      ],
      "examples": [],
      "warnings": [
        "Cascade-deletes ALL incident edges, including cross-product\nedges where the node is an endpoint. The operation is permanent (no\nsoft-delete or undo); the audit log records the removal. Pair with\n`get_node` first if you need a snapshot."
      ],
      "see": [
        "batch_delete_nodes",
        "get_node",
        "deduplicate_nodes"
      ],
      "source": "src/tools/nodes.ts:506",
      "symbol": "deleteNode",
      "returns": "JSON: `{ deleted_node_id, deleted_node_title, deleted_edge_ids }`.\nErrors propagate from the store (e.g. unknown id).",
      "return_shape": "{ deleted_node_id, deleted_node_title, deleted_edge_ids }",
      "return_notes": [
        "Errors propagate from the store (e.g. unknown id)."
      ],
      "atomicity": "atomic-with-rollback"
    },
    {
      "name": "export_upg_document",
      "description": "Export the full product graph as a UPG document: product metadata, all nodes, and all edges. Used by the upg pull CLI and apply_pull_changeset for sync/backup. Supports cursor pagination for large products (1000+ nodes): default limit 1000, max 10000. Pass next_cursor from a previous response as cursor to advance. Edges are returned in full on every page.",
      "domain": "nodes",
      "inputSchema": {
        "type": "object",
        "properties": {
          "product_id": {
            "type": "string",
            "description": "Product ID"
          },
          "limit": {
            "type": "number",
            "description": "Max nodes per page (default 1000, max 10000)"
          },
          "cursor": {
            "type": "string",
            "description": "Opaque pagination cursor; pass next_cursor from a previous response to advance."
          }
        },
        "required": [
          "product_id"
        ]
      },
      "throws": [
        "textError when `product_id` is missing or the product is\nnot visible to the caller."
      ],
      "examples": [],
      "warnings": [
        "For very large products (10 000+ nodes) iterate via `cursor`\nplus `next_cursor` rather than relying on a single call. Every page\nreturns the full edge set, so deduplicate on the client when\nassembling multiple pages."
      ],
      "see": [
        "apply_pull_changeset",
        "get_product_graph",
        "list_nodes"
      ],
      "source": "src/tools/nodes.ts:132",
      "symbol": "exportUpgDocument",
      "returns": "JSON: `{ product, nodes, edges, total_nodes, limit, next_cursor? }`.\n`next_cursor` is present when more node pages remain.",
      "return_shape": "{ product, nodes, edges, total_nodes, limit, next_cursor? }",
      "return_notes": [
        "`next_cursor` is present when more node pages remain."
      ],
      "atomicity": "atomic (read-only)"
    },
    {
      "name": "get_node",
      "description": "Get a single entity by ID with its full properties and all connected edges.",
      "domain": "nodes",
      "inputSchema": {
        "type": "object",
        "properties": {
          "node_id": {
            "type": "string",
            "description": "The node ID"
          }
        },
        "required": [
          "node_id"
        ]
      },
      "throws": [
        "textError when `node_id` is missing or the node does\nnot exist (or the caller has no access; RLS shares the same shape for\nboth)."
      ],
      "examples": [],
      "warnings": [],
      "see": [
        "list_nodes",
        "get_nodes",
        "search_nodes",
        "query"
      ],
      "source": "src/tools/nodes.ts:177",
      "symbol": "getNode",
      "returns": "JSON: `{ node, edges_out, edges_in }`. Errors with\n`Node not found: <id>` for unknown ids.",
      "return_shape": "{ node, edges_out, edges_in }",
      "return_notes": [
        "Errors with `Node not found: <id>` for unknown ids."
      ],
      "atomicity": "atomic (read-only)"
    },
    {
      "name": "get_nodes",
      "description": "Batch-fetch multiple entities by ID with edges. More efficient than multiple get_node calls.",
      "domain": "nodes",
      "inputSchema": {
        "type": "object",
        "properties": {
          "product_id": {
            "type": "string",
            "description": "The product ID"
          },
          "ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Node IDs to fetch (max 50)"
          },
          "compact_edges": {
            "type": "boolean",
            "description": "Omit titles from edges"
          }
        },
        "required": [
          "product_id",
          "ids"
        ]
      },
      "throws": [
        "textError when `product_id` or `ids` is missing/empty,\nor when `ids` exceeds 50."
      ],
      "examples": [],
      "warnings": [
        "`not_found` shares the same shape for \"node doesn't exist\" and\n\"node exists but caller lacks access\" (RLS treats them alike). Pass\n`compact_edges: true` to drop neighbour-title hydration on edge-heavy\nnodes (~30% smaller wire payload)."
      ],
      "see": [
        "get_node",
        "list_nodes",
        "query"
      ],
      "source": "src/tools/nodes.ts:220",
      "symbol": "getNodes",
      "returns": "JSON: `{ nodes, total, not_found? }`. `not_found` lists any\nrequested ids that did not resolve and appears only when at least one\nmiss occurred.",
      "return_shape": "{ nodes, total, not_found? }",
      "return_notes": [
        "`not_found` lists any requested ids that did not resolve and appears only when at least one miss occurred."
      ],
      "atomicity": "atomic (read-only)"
    },
    {
      "name": "get_product_graph",
      "description": "Export the full graph for a product (all nodes + edges).",
      "domain": "nodes",
      "inputSchema": {
        "type": "object",
        "properties": {
          "product_id": {
            "type": "string",
            "description": "Product ID"
          }
        },
        "required": [
          "product_id"
        ]
      },
      "throws": [
        "textError when `product_id` is missing or the product\nis not visible to the caller."
      ],
      "examples": [],
      "warnings": [
        "Returns the **entire** graph in one payload; for products\nwith thousands of nodes/edges this can be tens of MB. Prefer `query`\nwith a depth limit plus `include` projection for slices, or\n`list_nodes` plus cursor pagination for full enumeration without the\nwire-size hit."
      ],
      "see": [
        "query",
        "list_nodes",
        "get_graph_digest",
        "get_graph_analytics"
      ],
      "source": "src/tools/nodes.ts:667",
      "symbol": "getProductGraph",
      "returns": "JSON: `{ product, nodes, edges }`.",
      "return_shape": "{ product, nodes, edges }",
      "atomicity": "atomic (read-only)"
    },
    {
      "name": "list_nodes",
      "description": "List entities in the graph, optionally filtered by type. Supports cursor pagination for large products (1000+ nodes). Default limit 1000, max 10000. Pass next_cursor from a previous response as cursor to advance to the next page. Returns next_cursor in the response when more results remain. Legacy offset param still accepted when cursor is absent.",
      "domain": "nodes",
      "inputSchema": {
        "type": "object",
        "properties": {
          "product_id": {
            "type": "string",
            "description": "Product ID"
          },
          "type": {
            "type": "string",
            "description": "Filter by entity type"
          },
          "limit": {
            "type": "number",
            "description": "Max results (default 1000, max 10000)"
          },
          "cursor": {
            "type": "string",
            "description": "Opaque pagination cursor; pass next_cursor from a previous response to advance."
          },
          "offset": {
            "type": "number",
            "description": "Legacy: skip N results (default 0). Use cursor instead for new callers."
          }
        },
        "required": [
          "product_id"
        ]
      },
      "throws": [
        "textError when `product_id` is missing."
      ],
      "examples": [],
      "warnings": [
        "RLS-bounded: only nodes in products the caller has read access\nto are returned. An empty list can mean \"no nodes\" or \"no access\".\nDefault `limit: 1000`, max 10000. For products with 1000+ nodes use\n`cursor` pagination: keep calling with the returned `next_cursor` until\nit is absent."
      ],
      "see": [
        "get_node",
        "get_nodes",
        "search_nodes",
        "query"
      ],
      "source": "src/tools/nodes.ts:75",
      "symbol": "listNodes",
      "returns": "JSON: `{ nodes, total, limit, next_cursor? }`. `next_cursor` is\npresent when more results remain. `total` reflects the filtered count\nbefore pagination.",
      "return_shape": "{ nodes, total, limit, next_cursor? }",
      "return_notes": [
        "`next_cursor` is present when more results remain.",
        "`total` reflects the filtered count before pagination."
      ],
      "atomicity": "atomic (read-only)"
    },
    {
      "name": "move_node",
      "description": "Reparent a node to a new parent within the same product. Removes the existing containment edge (if any) and creates a new one with an inferred type. Runs inside a single Postgres transaction.",
      "domain": "nodes",
      "inputSchema": {
        "type": "object",
        "properties": {
          "product_id": {
            "type": "string",
            "description": "Product ID"
          },
          "node_id": {
            "type": "string",
            "description": "The node to reparent"
          },
          "new_parent_id": {
            "type": "string",
            "description": "The new parent node ID"
          }
        },
        "required": [
          "product_id",
          "node_id",
          "new_parent_id"
        ]
      },
      "throws": [
        "textError when either node is missing, the nodes belong\nto different products (cross-product reparenting is not allowed), or\nthe caller tries to move a node onto itself."
      ],
      "examples": [],
      "warnings": [],
      "see": [
        "batch_move_nodes",
        "resolve_edge_for_pair",
        "get_valid_children"
      ],
      "source": "src/tools/nodes.ts:536",
      "symbol": "moveNode",
      "returns": "JSON: `{ node_id, old_parent_id, new_parent_id, edge_created }`.\n`old_parent_id` is `null` when the node had no prior containment edge.",
      "return_shape": "{ node_id, old_parent_id, new_parent_id, edge_created }",
      "return_notes": [
        "`old_parent_id` is `null` when the node had no prior containment edge."
      ],
      "atomicity": "atomic-with-rollback"
    },
    {
      "name": "search_nodes",
      "description": "Full-text search across node titles and descriptions. Title matches rank higher.",
      "domain": "nodes",
      "inputSchema": {
        "type": "object",
        "properties": {
          "product_id": {
            "type": "string",
            "description": "Product ID"
          },
          "query": {
            "type": "string",
            "description": "Search text"
          },
          "type": {
            "type": "string",
            "description": "Optional type filter"
          },
          "limit": {
            "type": "number",
            "description": "Max results (default 20)"
          }
        },
        "required": [
          "product_id",
          "query"
        ]
      },
      "throws": [
        "textError when `product_id` or `query` is missing."
      ],
      "examples": [],
      "warnings": [
        "RLS-bounded: only nodes in products the caller has read\naccess to participate. Substring match is case-insensitive and runs\nin-memory after a full product fetch; for very large products this\ncan be heavy. A Postgres-side full-text index is a future optimisation."
      ],
      "see": [
        "list_nodes",
        "get_node",
        "query"
      ],
      "source": "src/tools/nodes.ts:272",
      "symbol": "searchNodes",
      "returns": "JSON: `{ results: Array<node & { match_field }>, total }`.",
      "return_shape": "{ results: Array<node & { match_field }>, total }",
      "atomicity": "atomic (read-only)"
    },
    {
      "name": "update_node",
      "description": "Update an existing entity. Unspecified fields are preserved.",
      "domain": "nodes",
      "inputSchema": {
        "type": "object",
        "properties": {
          "node_id": {
            "type": "string",
            "description": "The node ID to update"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "status": {
            "type": "string"
          },
          "properties": {
            "type": "object",
            "description": "Merged with existing properties"
          }
        },
        "required": [
          "node_id"
        ]
      },
      "throws": [
        "textError when `node_id` is missing or the store\nrejects the update (unknown id)."
      ],
      "examples": [],
      "warnings": [
        "Lifecycle-aware: invalid status values produce a `warning` but\nthe update still applies. For type changes, use `migrate_type`\ninstead; direct type mutation via this tool is unsupported."
      ],
      "see": [
        "migrate_type",
        "batch_update_nodes",
        "get_lifecycle"
      ],
      "source": "src/tools/nodes.ts:434",
      "symbol": "updateNode",
      "returns": "JSON: `{ node: updatedNode, warning? }`. Errors propagate from\nthe store (e.g. unknown node id).",
      "return_shape": "{ node: updatedNode, warning? }",
      "return_notes": [
        "Errors propagate from the store (e.g. unknown node id)."
      ],
      "atomicity": "atomic-with-rollback"
    },
    {
      "name": "create_edge",
      "description": "Create a relationship between two nodes. Edge type is auto-inferred if omitted.",
      "domain": "edges",
      "inputSchema": {
        "type": "object",
        "properties": {
          "source_id": {
            "type": "string",
            "description": "Source node ID"
          },
          "target_id": {
            "type": "string",
            "description": "Target node ID"
          },
          "type": {
            "type": "string",
            "description": "Edge type; auto-inferred if omitted"
          }
        },
        "required": [
          "source_id",
          "target_id"
        ]
      },
      "throws": [
        "textError when `source_id`/`target_id` is missing, an endpoint\nlookup fails, source and target resolve to the same node, an explicit\n`type` violates the catalog's source/target pair, or no canonical edge\nexists for the pair and no `type` was supplied (enriched with resolver\nhints)."
      ],
      "examples": [],
      "warnings": [],
      "see": [
        "resolve_edge_for_pair",
        "list_edge_types",
        "get_edge_type",
        "batch_create_edges"
      ],
      "source": "src/tools/edges.ts:57",
      "symbol": "createEdge",
      "returns": "JSON: `{ edge: { id, source, target, type }, warning? }`.",
      "return_shape": "{ edge: { id, source, target, type }, warning? }",
      "atomicity": "atomic-with-rollback"
    },
    {
      "name": "delete_edge",
      "description": "Remove a relationship between two nodes.",
      "domain": "edges",
      "inputSchema": {
        "type": "object",
        "properties": {
          "edge_id": {
            "type": "string",
            "description": "The edge ID to delete"
          }
        },
        "required": [
          "edge_id"
        ]
      },
      "throws": [
        "textError when `edge_id` is missing or unknown."
      ],
      "examples": [],
      "warnings": [],
      "see": [
        "batch_delete_edges",
        "export_edges"
      ],
      "source": "src/tools/edges.ts:129",
      "symbol": "deleteEdge",
      "returns": "JSON: `{ deleted_edge_id }`.",
      "return_shape": "{ deleted_edge_id }",
      "atomicity": "atomic-with-rollback"
    },
    {
      "name": "export_edges",
      "description": "Flat enumeration of all edges for a product, optionally filtered by type. Returns lightweight { id, source, target, type } rows ordered by id, intended for migration passes.",
      "domain": "edges",
      "inputSchema": {
        "type": "object",
        "properties": {
          "product_id": {
            "type": "string",
            "description": "Product ID"
          },
          "types": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Optional edge type filter"
          }
        },
        "required": [
          "product_id"
        ]
      },
      "throws": [
        "textError when `product_id` is missing or the store rejects the read."
      ],
      "examples": [],
      "warnings": [],
      "see": [
        "list_edge_types",
        "rename_edge_type",
        "query"
      ],
      "source": "src/tools/edges.ts:151",
      "symbol": "exportEdges",
      "returns": "JSON: `{ edges: [{ id, source, target, type }], total: number }`.",
      "return_shape": "{ edges: [{ id, source, target, type }], total: number }",
      "atomicity": "atomic (read-only)"
    },
    {
      "name": "rename_edge_type",
      "description": "Rename all edges of one type to another across a product. dry_run (default: true) previews the count.",
      "domain": "edges",
      "inputSchema": {
        "type": "object",
        "properties": {
          "product_id": {
            "type": "string",
            "description": "Product ID"
          },
          "from": {
            "type": "string",
            "description": "Current edge type"
          },
          "to": {
            "type": "string",
            "description": "New edge type"
          },
          "dry_run": {
            "type": "boolean",
            "description": "If true, only count (default: true); pass false to apply."
          }
        },
        "required": [
          "product_id",
          "from",
          "to"
        ]
      },
      "throws": [
        "textError when `product_id`, `from`, or `to` is missing."
      ],
      "examples": [],
      "warnings": [],
      "see": [
        "list_edge_types",
        "get_edge_type",
        "export_edges",
        "migrate_type"
      ],
      "source": "src/tools/edges.ts:176",
      "symbol": "renameEdgeType",
      "returns": "JSON: `{ from, to, affected: number, dry_run: boolean }`.",
      "return_shape": "{ from, to, affected: number, dry_run: boolean }",
      "atomicity": "atomic-with-rollback (write path)"
    },
    {
      "name": "apply_framework",
      "description": "Apply a framework (MoSCoW, RICE, Kano, ...) to a set of entities in a product: creates a framework_exercise node and an `includes` edge to each entity. The per-entity result is recorded on the edge via score_entity, never on the entity node, so the same entity can sit in many exercises and any entity type can be scored. Returns { exercise_id, exercise, included, warnings }.",
      "domain": "frameworks",
      "inputSchema": {
        "type": "object",
        "properties": {
          "product_id": {
            "type": "string",
            "description": "Required. Product the exercise belongs to."
          },
          "framework_id": {
            "type": "string",
            "description": "Required. UPGFramework.id (e.g. \"moscow\", \"rice-scoring\")."
          },
          "title": {
            "type": "string",
            "description": "Human label for the exercise (default \"<Framework> exercise\")."
          },
          "entity_ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Entities to pull into the exercise (any type)."
          },
          "status": {
            "type": "string",
            "description": "Lifecycle phase: draft | active | archived (default draft)."
          }
        },
        "required": [
          "product_id",
          "framework_id"
        ]
      },
      "throws": [
        "textError on a missing product_id/framework_id or an unknown framework_id."
      ],
      "examples": [],
      "warnings": [],
      "see": [
        "score_entity"
      ],
      "source": "src/tools/frameworks.ts:27",
      "symbol": "applyFramework",
      "returns": "JSON: `{ exercise_id, exercise, included: [{ edge_id, entity_id }], warnings }`.",
      "return_shape": "{ exercise_id, exercise, included: [{ edge_id, entity_id }], warnings }",
      "atomicity": "per-write atomic; the exercise node and each includes edge commit\nindependently (a target that cannot be included is reported in `warnings`)."
    },
    {
      "name": "score_entity",
      "description": "Record a framework's result for one entity on the exercise's includes edge (a MoSCoW bucket, a RICE score, a canvas slot). Auto-includes the entity if not already in scope. Merges into existing edge properties unless replace is set. The product is resolved from the exercise node. Returns { edge, warnings }.",
      "domain": "frameworks",
      "inputSchema": {
        "type": "object",
        "properties": {
          "exercise_id": {
            "type": "string",
            "description": "Required. The framework_exercise id."
          },
          "entity_id": {
            "type": "string",
            "description": "Required. The entity being scored."
          },
          "values": {
            "type": "object",
            "description": "Required. The result as { input: value }, e.g. { \"moscow\": \"must\" } or { \"reach\": 800, \"impact\": 3 }."
          },
          "replace": {
            "type": "boolean",
            "description": "Replace the edge properties instead of merging (default false)."
          }
        },
        "required": [
          "exercise_id",
          "entity_id",
          "values"
        ]
      },
      "throws": [
        "textError when the exercise/entity is missing or the node is not a\nframework_exercise."
      ],
      "examples": [],
      "warnings": [],
      "see": [
        "apply_framework"
      ],
      "source": "src/tools/frameworks.ts:97",
      "symbol": "scoreEntity",
      "returns": "JSON: `{ edge, warnings }`.",
      "return_shape": "{ edge, warnings }",
      "atomicity": "atomic-with-rollback (single edge upsert)."
    },
    {
      "name": "create_area",
      "description": "Create a new product area node (type 'area') in a product. Product areas are top-level organisational units within a product.",
      "domain": "areas",
      "inputSchema": {
        "type": "object",
        "properties": {
          "product_id": {
            "type": "string",
            "description": "Product ID"
          },
          "title": {
            "type": "string",
            "description": "Area title"
          },
          "description": {
            "type": "string",
            "description": "Optional description"
          }
        },
        "required": [
          "product_id",
          "title"
        ]
      },
      "throws": [
        "textError when `product_id` or `title` is missing."
      ],
      "examples": [],
      "warnings": [],
      "see": [
        "list_product_areas",
        "get_area_context"
      ],
      "source": "src/tools/areas.ts:63",
      "symbol": "createArea",
      "returns": "JSON: `{ node }`.",
      "return_shape": "{ node }",
      "atomicity": "atomic"
    },
    {
      "name": "get_area_context",
      "description": "Returns a summary of a product area: entity counts by type within it, child area count, and description. Traverses containment edges up to depth 2.",
      "domain": "areas",
      "inputSchema": {
        "type": "object",
        "properties": {
          "product_id": {
            "type": "string",
            "description": "Product ID"
          },
          "area_id": {
            "type": "string",
            "description": "The area node ID"
          }
        },
        "required": [
          "product_id",
          "area_id"
        ]
      },
      "throws": [
        "textError when `product_id` or `area_id` is missing, or the area lookup fails."
      ],
      "examples": [],
      "warnings": [],
      "see": [
        "create_area",
        "get_area_graph"
      ],
      "source": "src/tools/areas.ts:93",
      "symbol": "getAreaContext",
      "returns": "JSON: `{ area: { id, title, description }, entity_counts, total_entities, child_areas }`.",
      "return_shape": "{ area: { id, title, description }, entity_counts, total_entities, child_areas }",
      "atomicity": "atomic (read-only)"
    },
    {
      "name": "get_area_graph",
      "description": "Get all entities and edges that belong to a product area. Returns the sub-graph scoped to that area.",
      "domain": "areas",
      "inputSchema": {
        "type": "object",
        "properties": {
          "product_id": {
            "type": "string",
            "description": "Product ID"
          },
          "area_id": {
            "type": "string",
            "description": "The product area node ID"
          },
          "depth": {
            "type": "number",
            "description": "How many levels deep to traverse (default 3, max 10)"
          }
        },
        "required": [
          "product_id",
          "area_id"
        ]
      },
      "throws": [
        "textError when `product_id` or `area_id` is missing, or store rejects."
      ],
      "examples": [],
      "warnings": [],
      "see": [
        "list_product_areas",
        "get_area_context",
        "query"
      ],
      "source": "src/tools/areas.ts:38",
      "symbol": "getAreaGraph",
      "returns": "JSON: `{ area, nodes, edges }`.",
      "return_shape": "{ area, nodes, edges }",
      "atomicity": "atomic (read-only)"
    },
    {
      "name": "list_product_areas",
      "description": "List all product areas in a product. Product areas are top-level organizational units within a product.",
      "domain": "areas",
      "inputSchema": {
        "type": "object",
        "properties": {
          "product_id": {
            "type": "string",
            "description": "Product ID"
          }
        },
        "required": [
          "product_id"
        ]
      },
      "throws": [
        "textError when `product_id` is missing."
      ],
      "examples": [],
      "warnings": [],
      "see": [
        "get_area_graph",
        "get_area_context",
        "create_area"
      ],
      "source": "src/tools/areas.ts:20",
      "symbol": "listProductAreas",
      "returns": "JSON: `{ areas, total }`.",
      "return_shape": "{ areas, total }",
      "atomicity": "atomic (read-only)"
    },
    {
      "name": "get_entity_schema",
      "description": "Return expected properties, valid statuses, valid edge types, and domain for an entity type. Lets agents construct valid entities without skill prompts. Each property carries its contract in `description`; pass `include_notes` for the longform rationale. Optional `include` folds in valid child types / super-domain region; optional `resolve_edge_to` folds in the canonical edge for this type → that target.",
      "domain": "schema",
      "inputSchema": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "description": "Entity type (e.g. \"hypothesis\", \"persona\", \"opportunity\")"
          },
          "include": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "valid_children",
                "region"
              ]
            },
            "description": "Optional extra blocks: \"valid_children\" (folds get_valid_children), \"region\" (folds get_region_for_entity_type)."
          },
          "include_notes": {
            "type": "boolean",
            "description": "Fold the longform half of each property doc (`notes`: rationale, edge cases, workflow recipes) into `expected_properties`. Default false, because `description` already carries the contract and the longform is rarely needed while constructing an entity. Ask for it when a property's exact semantics matter."
          },
          "resolve_edge_to": {
            "type": "string",
            "description": "Optional target entity type. Folds resolve_edge_for_pair(type → target) into a `resolve_edge` block."
          }
        },
        "required": [
          "type"
        ]
      },
      "throws": [
        "textError when `type` is missing or unknown\n(`UnknownEntityTypeError`)."
      ],
      "examples": [],
      "warnings": [],
      "see": [
        "get_entity_meta",
        "list_entity_types",
        "get_valid_children",
        "get_lifecycle",
        "get_domain_guide",
        "list_edge_types",
        "create_node"
      ],
      "source": "src/tools/schema.ts:66",
      "symbol": "getEntitySchema",
      "returns": "JSON: `{ type, alias_of?, domain, expected_properties,\nedges_out, edges_in, phases?, initial_phase?, terminal_phases?,\ndomain_guide?, valid_children?, region?, resolve_edge? }`.",
      "return_shape": "{ type, alias_of?, domain, expected_properties, edges_out, edges_in, phases?, initial_phase?, terminal_phases?, domain_guide?, valid_children?, region?, resolve_edge? }",
      "atomicity": "atomic (read-only)"
    },
    {
      "name": "add_comment",
      "description": "Add a comment on a node in the graph.",
      "domain": "collaboration",
      "inputSchema": {
        "type": "object",
        "properties": {
          "product_id": {
            "type": "string",
            "description": "Product ID"
          },
          "node_id": {
            "type": "string",
            "description": "Node to comment on"
          },
          "user_id": {
            "type": "string",
            "description": "Author user ID"
          },
          "body": {
            "type": "string",
            "description": "Comment text"
          }
        },
        "required": [
          "product_id",
          "node_id",
          "user_id",
          "body"
        ]
      },
      "throws": [
        "textError when `product_id`, `node_id`, `user_id`, or `body` is missing."
      ],
      "examples": [],
      "warnings": [
        "`user_id` MUST resolve to a member of the product's collaborator set,\nor downstream RLS rejects the insert."
      ],
      "see": [
        "list_comments",
        "list_collaborators",
        "grant_access"
      ],
      "source": "src/tools/collaboration.ts:20",
      "symbol": "addComment",
      "returns": "JSON: `{ comment: { id, product_id, node_id, user_id, body, created_at } }`.",
      "return_shape": "{ comment: { id, product_id, node_id, user_id, body, created_at } }",
      "atomicity": "atomic"
    },
    {
      "name": "grant_access",
      "description": "Grant or update a user's role on a product (owner, editor, viewer).",
      "domain": "collaboration",
      "inputSchema": {
        "type": "object",
        "properties": {
          "product_id": {
            "type": "string",
            "description": "Product ID"
          },
          "user_id": {
            "type": "string",
            "description": "User to grant access to"
          },
          "role": {
            "type": "string",
            "description": "Role: owner | editor | viewer",
            "enum": [
              "owner",
              "editor",
              "viewer"
            ]
          }
        },
        "required": [
          "product_id",
          "user_id",
          "role"
        ]
      },
      "throws": [
        "textError when `product_id`, `user_id`, or `role` is missing."
      ],
      "examples": [],
      "warnings": [
        "Billing-relevant: collaborator count typically drives plan tier;\na grant may trigger a tier upgrade or hit a seat-limit cap."
      ],
      "see": [
        "list_collaborators",
        "add_comment"
      ],
      "source": "src/tools/collaboration.ts:63",
      "symbol": "grantAccess",
      "returns": "JSON: `{ granted: { product_id, user_id, role } }`.",
      "return_shape": "{ granted: { product_id, user_id, role } }",
      "atomicity": "atomic"
    },
    {
      "name": "list_collaborators",
      "description": "List all collaborators and their roles for a product.",
      "domain": "collaboration",
      "inputSchema": {
        "type": "object",
        "properties": {
          "product_id": {
            "type": "string",
            "description": "Product ID"
          }
        },
        "required": [
          "product_id"
        ]
      },
      "throws": [
        "textError when `product_id` is missing."
      ],
      "examples": [],
      "warnings": [],
      "see": [
        "grant_access",
        "list_comments"
      ],
      "source": "src/tools/collaboration.ts:92",
      "symbol": "listCollaborators",
      "returns": "JSON: `{ collaborators: Array<{ user_id, role, granted_at }> }`.",
      "return_shape": "{ collaborators: Array<{ user_id, role, granted_at }> }",
      "atomicity": "atomic (read-only)"
    },
    {
      "name": "list_comments",
      "description": "List comments on a node, newest first.",
      "domain": "collaboration",
      "inputSchema": {
        "type": "object",
        "properties": {
          "node_id": {
            "type": "string",
            "description": "Node ID"
          }
        },
        "required": [
          "node_id"
        ]
      },
      "throws": [
        "textError when `node_id` is missing."
      ],
      "examples": [],
      "warnings": [
        "RLS-bounded; an empty array can mean \"no comments\" or \"no access\"."
      ],
      "see": [
        "add_comment",
        "list_collaborators"
      ],
      "source": "src/tools/collaboration.ts:44",
      "symbol": "listComments",
      "returns": "JSON: `{ comments: Comment[] }`.",
      "return_shape": "{ comments: Comment[] }",
      "atomicity": "atomic (read-only)"
    },
    {
      "name": "get_graph_analytics",
      "description": "Computed product thinking metrics: hypothesis velocity, persona coverage ratio, evidence density, stale entity rate, orphan rate.",
      "domain": "analytics",
      "inputSchema": {
        "type": "object",
        "properties": {
          "product_id": {
            "type": "string",
            "description": "Product ID"
          }
        },
        "required": [
          "product_id"
        ]
      },
      "throws": [
        "textError when `product_id` is missing or the product is invisible\nto the caller (RLS-bounded; \"not found\" and \"no access\" share wording)."
      ],
      "examples": [],
      "warnings": [],
      "see": [
        "get_graph_digest",
        "get_product_context",
        "get_audit_log"
      ],
      "source": "src/tools/analytics.ts:21",
      "symbol": "getGraphAnalytics",
      "returns": "JSON: `{ product: { id, title }, analytics }`.",
      "return_shape": "{ product: { id, title }, analytics }",
      "atomicity": "atomic (read-only)"
    },
    {
      "name": "list_webhooks",
      "description": "List all registered webhooks for a product.",
      "domain": "webhooks",
      "inputSchema": {
        "type": "object",
        "properties": {
          "product_id": {
            "type": "string",
            "description": "Product ID"
          }
        },
        "required": [
          "product_id"
        ]
      },
      "throws": [
        "textError when `product_id` is missing."
      ],
      "examples": [],
      "warnings": [],
      "see": [
        "register_webhook",
        "remove_webhook"
      ],
      "source": "src/tools/webhooks.ts:46",
      "symbol": "listWebhooks",
      "returns": "JSON: `{ webhooks: Webhook[] }`.",
      "return_shape": "{ webhooks: Webhook[] }",
      "atomicity": "atomic (read-only)"
    },
    {
      "name": "register_webhook",
      "description": "Register a webhook called when an event occurs on a product (node.created, node.updated, node.deleted, edge.created, edge.deleted; use '*' for all). Delivered async after commit, HMAC-signed via the optional secret (X-UPG-Signature header), with bounded retry; a persistent 4xx auto-disables the registration.",
      "domain": "webhooks",
      "inputSchema": {
        "type": "object",
        "properties": {
          "product_id": {
            "type": "string",
            "description": "Product ID"
          },
          "event": {
            "type": "string",
            "description": "Event name (e.g. node.created, node.updated, node.deleted, edge.created, edge.deleted)"
          },
          "url": {
            "type": "string",
            "description": "Webhook URL to POST to"
          },
          "secret": {
            "type": "string",
            "description": "Optional shared secret for HMAC signature verification"
          }
        },
        "required": [
          "product_id",
          "event",
          "url"
        ]
      },
      "throws": [
        "textError when `product_id`, `event`, or `url` is missing."
      ],
      "examples": [],
      "warnings": [],
      "see": [
        "list_webhooks",
        "remove_webhook"
      ],
      "source": "src/tools/webhooks.ts:24",
      "symbol": "registerWebhook",
      "returns": "JSON: `{ webhook: { id, product_id, event, url, secret?, created_at } }`.",
      "return_shape": "{ webhook: { id, product_id, event, url, secret?, created_at } }",
      "atomicity": "atomic"
    },
    {
      "name": "remove_webhook",
      "description": "Remove a registered webhook by ID.",
      "domain": "webhooks",
      "inputSchema": {
        "type": "object",
        "properties": {
          "webhook_id": {
            "type": "string",
            "description": "Webhook ID to remove"
          }
        },
        "required": [
          "webhook_id"
        ]
      },
      "throws": [
        "textError when `webhook_id` is missing or the store rejects the deletion."
      ],
      "examples": [],
      "warnings": [],
      "see": [
        "register_webhook",
        "list_webhooks"
      ],
      "source": "src/tools/webhooks.ts:63",
      "symbol": "removeWebhook",
      "returns": "JSON: `{ removed: <webhook_id> }`.",
      "return_shape": "{ removed: <webhook_id> }",
      "atomicity": "atomic"
    },
    {
      "name": "get_catalog_entry",
      "description": "Fetch one static spec catalog record by `kind` + `id` (one faceted tool replacing the 15 `get_*-by-id` spec-introspection tools). Reads `@unified-product-graph/core`. `id` is the record identifier for that kind: `playbook`/`framework`/`lens`/`scale`/`anti_pattern`/`tree_pattern`/`domain_ring`/`region`/`approach` take their record id; `entity_meta` takes an entity-type name; `edge_type` takes an edge-type key; `lifecycle`/`type_label` take an entity type; `domain_guide` takes a domain id; `template` takes a template id. Use `list_catalog` to enumerate a kind.",
      "domain": "spec",
      "inputSchema": {
        "type": "object",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "entity_meta",
              "edge_type",
              "region",
              "domain_guide",
              "domain_ring",
              "framework",
              "lens",
              "lifecycle",
              "playbook",
              "scale",
              "anti_pattern",
              "tree_pattern",
              "type_label",
              "template",
              "approach"
            ],
            "description": "Which static spec catalog to read one record from."
          },
          "id": {
            "type": "string",
            "description": "The record identifier for that kind (see the tool description for the per-kind id meaning)."
          }
        },
        "required": [
          "kind",
          "id"
        ]
      },
      "throws": [
        "textError when `kind` or `id` is missing, or the kind is unknown."
      ],
      "examples": [],
      "warnings": [],
      "see": [
        "list_catalog",
        "get_entity_schema"
      ],
      "source": "src/tools/catalog.ts:135",
      "symbol": "getCatalogEntry",
      "returns": "JSON: the delegated `get_<kind>` record verbatim (shape varies by kind).",
      "return_notes": [
        "JSON: the delegated `get_<kind>` record verbatim (shape varies by kind)."
      ],
      "atomicity": "atomic (read-only)"
    },
    {
      "name": "get_spec_version",
      "description": "Return spec-level metadata for adopter compatibility checks: upg_version, markdown_format_version, and canonical counts (entity types, edge types, atomic domains, super-domain regions). Pin against the version pair; counts are informational. Pass `changelog: true` to fold in the spec CHANGELOG (a `whats_new` surface); `since` (a version) returns only newer entries.",
      "domain": "spec",
      "inputSchema": {
        "type": "object",
        "properties": {
          "changelog": {
            "type": "boolean",
            "description": "When true, include a `changelog` array parsed from the spec CHANGELOG.md."
          },
          "since": {
            "type": "string",
            "description": "With changelog: return only entries strictly newer than this version (e.g. \"0.17.0\")."
          }
        }
      },
      "throws": [],
      "examples": [],
      "warnings": [],
      "see": [
        "list_entity_types",
        "list_edge_types",
        "list_regions"
      ],
      "source": "src/tools/spec.ts:819",
      "symbol": "getSpecVersion",
      "returns": "JSON: `{ upg_version, markdown_format_version, entity_count, edge_count, domain_count, region_count }`",
      "return_shape": "{ upg_version, markdown_format_version, entity_count, edge_count, domain_count, region_count }",
      "atomicity": "atomic (read-only)"
    },
    {
      "name": "list_catalog",
      "description": "List a static spec catalog by `kind` (one faceted tool replacing the 25 `list_*` spec-introspection tools). Reads `@unified-product-graph/core`; identical for every client on a given spec version. Kind-specific filters pass straight through: e.g. `playbooks` accepts `region` / `canonical_only` / `framework_id`; `entity_types` accepts `domain` / `maturity` / `deprecated` / `limit` / `cursor`; `benchmarks` requires `benchmark_kind` (`count` | `relationship` | `ratio` | `domain_activation`) plus optional `stage` / `domain`. Use `get_catalog_entry` to fetch one record by id.",
      "domain": "spec",
      "inputSchema": {
        "type": "object",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "entity_types",
              "edge_types",
              "cross_edge_types",
              "regions",
              "domains",
              "domain_rings",
              "frameworks",
              "framework_categories",
              "framework_structure_patterns",
              "lenses",
              "lifecycles",
              "playbooks",
              "scales",
              "anti_patterns",
              "tree_patterns",
              "templates",
              "approaches",
              "type_labels",
              "status_values",
              "product_stages",
              "benchmarks",
              "edge_migrations",
              "scalar_to_edge_migrations",
              "split_migrations",
              "type_migrations"
            ],
            "description": "Which static spec catalog to list."
          },
          "region": {
            "type": "string",
            "description": "Filter (playbooks): exact UPGRegionId."
          },
          "canonical_only": {
            "type": "boolean",
            "description": "Filter (playbooks): canonical playbook per region only."
          },
          "framework_id": {
            "type": "string",
            "description": "Filter (playbooks / approaches): exact framework id."
          },
          "domain": {
            "type": "string",
            "description": "Filter (entity_types / benchmarks): exact atomic-domain id."
          },
          "maturity": {
            "type": "string",
            "description": "Filter (entity_types): draft | proposed | stable | deprecated | removed."
          },
          "deprecated": {
            "type": "boolean",
            "description": "Filter (entity_types): keep only / exclude deprecated types."
          },
          "stage": {
            "type": "string",
            "description": "Filter (benchmarks): UPGProductStage."
          },
          "benchmark_kind": {
            "type": "string",
            "enum": [
              "count",
              "relationship",
              "ratio",
              "domain_activation"
            ],
            "description": "Required for kind=benchmarks: which benchmark catalog (remapped to the retired tool's `kind`)."
          },
          "limit": {
            "type": "number",
            "description": "Pagination (entity_types / type_labels / frameworks / anti_patterns): page size."
          },
          "cursor": {
            "type": "string",
            "description": "Pagination: opaque cursor from a prior `next_cursor`."
          }
        },
        "required": [
          "kind"
        ]
      },
      "throws": [
        "textError when `kind` is missing or unknown."
      ],
      "examples": [],
      "warnings": [],
      "see": [
        "get_catalog_entry",
        "get_entity_schema"
      ],
      "source": "src/tools/catalog.ts:122",
      "symbol": "listCatalog",
      "returns": "JSON: the delegated `list_<kind>` payload verbatim (shape varies by kind).",
      "return_notes": [
        "JSON: the delegated `list_<kind>` payload verbatim (shape varies by kind)."
      ],
      "atomicity": "atomic (read-only)"
    },
    {
      "name": "create_cross_product_edge",
      "description": "Create a cross-product edge linking entities across different products. Type must be one of the canonical UPG cross-edge types: shares_persona, shares_competitor, shares_metric, depends_on_product, cannibalises, succeeds, hosts, contributes_to.",
      "domain": "portfolio",
      "inputSchema": {
        "type": "object",
        "properties": {
          "product_id": {
            "type": "string",
            "description": "The product creating this cross-edge"
          },
          "source": {
            "type": "string",
            "description": "Qualified source: {product_id}/{node_id}"
          },
          "target": {
            "type": "string",
            "description": "Qualified target: {product_id}/{node_id}"
          },
          "type": {
            "type": "string",
            "description": "Cross-edge type",
            "enum": [
              "shares_persona",
              "shares_competitor",
              "shares_metric",
              "depends_on_product",
              "cannibalises",
              "succeeds",
              "hosts",
              "contributes_to"
            ]
          }
        },
        "required": [
          "product_id",
          "source",
          "target",
          "type"
        ]
      },
      "throws": [
        "textError when `product_id`, `source`, `target`, or\n`type` is missing, or `type` is not a UPG cross-edge type."
      ],
      "examples": [],
      "warnings": [
        "Source/target are qualified strings (`{product_id}/{node_id}`)\nand skip FK validation against the products table. A target\nreferencing a deleted product becomes a dangling cross-edge; sweep\nperiodically with `repair_dangling_edges`."
      ],
      "see": [
        "list_cross_edge_types",
        "list_portfolio_cross_edges",
        "repair_dangling_edges",
        "migrate_cross_edges"
      ],
      "source": "src/tools/portfolio.ts:83",
      "symbol": "createCrossProductEdge",
      "returns": "JSON: `{ edge: { id, source, target, type, created_by_product_id } }`",
      "return_shape": "{ edge: { id, source, target, type, created_by_product_id } }",
      "atomicity": "atomic"
    },
    {
      "name": "list_portfolio_cross_edges",
      "description": "List all cross-product edges created by a product. Cross-product edges link entities across different products (e.g. shares_persona, depends_on_product).",
      "domain": "portfolio",
      "inputSchema": {
        "type": "object",
        "properties": {
          "product_id": {
            "type": "string",
            "description": "Product ID"
          }
        },
        "required": [
          "product_id"
        ]
      },
      "throws": [
        "textError when `product_id` is missing."
      ],
      "examples": [],
      "warnings": [
        "Returns only edges this product **created**; edges another\nproduct created targeting this product surface through that product's\nown call. To audit all incident cross-edges, query each product in\nthe portfolio."
      ],
      "see": [
        "create_cross_product_edge",
        "list_cross_edge_types",
        "migrate_cross_edges"
      ],
      "source": "src/tools/portfolio.ts:53",
      "symbol": "listPortfolioCrossEdges",
      "returns": "JSON: `{ edges: [{ id, source, target, type }], total: number }`",
      "return_shape": "{ edges: [{ id, source, target, type }], total: number }",
      "atomicity": "atomic (read-only)"
    },
    {
      "name": "list_portfolios",
      "description": "List the product portfolio for this UPG cloud instance. For v1, returns all products as a single portfolio. Use before creating cross-product edges to discover valid product IDs.",
      "domain": "portfolio",
      "inputSchema": {
        "type": "object",
        "properties": {}
      },
      "throws": [],
      "examples": [],
      "warnings": [
        "v1 returns a single synthetic `'default'` portfolio per\ninstance; multi-portfolio scoping arrives once auth is wired.\nTreat the `id: 'default'` shape as transitional."
      ],
      "see": [
        "list_products",
        "list_portfolio_cross_edges"
      ],
      "source": "src/tools/portfolio.ts:27",
      "symbol": "listPortfolios",
      "returns": "JSON: `{ portfolios: [{ id, title, products: [{ id, title, stage? }] }], total: number }`",
      "return_shape": "{ portfolios: [{ id, title, products: [{ id, title, stage? }] }], total: number }",
      "atomicity": "atomic (read-only)"
    },
    {
      "name": "repair_dangling_edges",
      "description": "Find (and optionally remove) cross-product edges that reference a product that no longer exists. Default is dry_run=true.",
      "domain": "portfolio",
      "inputSchema": {
        "type": "object",
        "properties": {
          "product_id": {
            "type": "string",
            "description": "Product ID"
          },
          "dry_run": {
            "type": "boolean",
            "description": "Default true: report only."
          },
          "drop": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Categories to drop when dry_run=false"
          }
        },
        "required": [
          "product_id"
        ]
      },
      "throws": [
        "textError when `product_id` is missing."
      ],
      "examples": [],
      "warnings": [
        "Default is `dry_run: true`. Pass `dry_run: false` AND\n`drop: ['dangling_cross_edges']` to actually delete; the second\nguard prevents accidental drops. Per-edge errors during deletion\n(concurrent removal) are swallowed; check `dropped` against\n`dangling_count` to detect partial application."
      ],
      "see": [
        "create_cross_product_edge",
        "list_portfolio_cross_edges",
        "migrate_cross_edges"
      ],
      "source": "src/tools/portfolio.ts:162",
      "symbol": "repairDanglingEdges",
      "returns": "JSON: `{ dangling: [{ id, source, target, type }], dangling_count, dry_run, dropped }`",
      "return_shape": "{ dangling: [{ id, source, target, type }], dangling_count, dry_run, dropped }",
      "atomicity": "atomic-with-rollback (when drop is requested)"
    },
    {
      "name": "batch_create_edges",
      "description": "Create up to 50 edges in a single atomic Postgres transaction. Edge type is auto-inferred from source/target types when omitted. All-or-nothing: any failure rolls back the entire batch.",
      "domain": "batch",
      "inputSchema": {
        "type": "object",
        "properties": {
          "product_id": {
            "type": "string",
            "description": "Product ID"
          },
          "edges": {
            "type": "array",
            "description": "Edges to create (max 50)",
            "items": {
              "type": "object",
              "properties": {
                "source_id": {
                  "type": "string",
                  "description": "Source node ID"
                },
                "target_id": {
                  "type": "string",
                  "description": "Target node ID"
                },
                "type": {
                  "type": "string",
                  "description": "Edge type; auto-inferred if omitted"
                }
              },
              "required": [
                "source_id",
                "target_id"
              ]
            }
          }
        },
        "required": [
          "product_id",
          "edges"
        ]
      },
      "throws": [
        "textError when `edges` is missing / non-array / empty / >50, any\nitem is missing `source_id` / `target_id`, any endpoint does not exist, an\nexplicit `type` violates the catalog's source/target pair, or an inferred\npair has no canonical edge. Any such failure rejects the whole batch\nbefore BEGIN."
      ],
      "examples": [],
      "warnings": [
        "Inference is catalog-strict: an unmapped pair is refused rather\nthan fabricating a `${source}_contains_${target}` edge. Pass an explicit\n`type` (resolved via `resolve_edge_for_pair`) for non-catalog edges."
      ],
      "see": [
        "create_edge",
        "resolve_edge_for_pair",
        "batch_delete_edges"
      ],
      "source": "src/tools/batch.ts:405",
      "symbol": "batchCreateEdges",
      "returns": "JSON: `{ created: [{ id, source_id, target_id, type }], count }`.",
      "return_shape": "{ created: [{ id, source_id, target_id, type }], count }",
      "atomicity": "atomic-with-rollback (BEGIN / COMMIT / ROLLBACK)."
    },
    {
      "name": "batch_create_nodes",
      "description": "Create up to 50 entities in a single atomic Postgres transaction. For each node with a parent_id, a containment edge is created in the same transaction. All-or-nothing: any failure rolls back the entire batch.",
      "domain": "batch",
      "inputSchema": {
        "type": "object",
        "properties": {
          "product_id": {
            "type": "string",
            "description": "Product ID"
          },
          "nodes": {
            "type": "array",
            "description": "Nodes to create (max 50)",
            "items": {
              "type": "object",
              "properties": {
                "type": {
                  "type": "string",
                  "description": "UPG entity type"
                },
                "title": {
                  "type": "string",
                  "description": "Entity title"
                },
                "description": {
                  "type": "string",
                  "description": "Optional description"
                },
                "tags": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  },
                  "description": "Freeform tags"
                },
                "status": {
                  "type": "string",
                  "description": "Lifecycle status"
                },
                "properties": {
                  "type": "object",
                  "description": "Type-specific fields"
                },
                "parent_id": {
                  "type": "string",
                  "description": "Parent node ID; creates a containment edge automatically"
                }
              },
              "required": [
                "type",
                "title"
              ]
            }
          }
        },
        "required": [
          "product_id",
          "nodes"
        ]
      },
      "throws": [
        "textError when `nodes` is missing / non-array, any required field\n(`type`, `title`) is absent, or any node carries a declared property whose\nvalue type mismatches the schema (rejects the whole batch before BEGIN)."
      ],
      "examples": [],
      "warnings": [
        "Validation runs inline before BEGIN; a single bad item rejects\nthe entire batch before any database mutation. Parent containment edges\nare catalog-strict: a non-canonical parent→child pair skips the edge with\na warning rather than fabricating a `_contains_` edge (matches\n`create_node`). A missing parent likewise skips the edge."
      ],
      "see": [
        "create_node",
        "batch_create_edges",
        "batch_update_nodes"
      ],
      "source": "src/tools/batch.ts:63",
      "symbol": "batchCreateNodes",
      "returns": "JSON: `{ created: [{ id, type, title }], count, warnings? }`.",
      "return_shape": "{ created: [{ id, type, title }], count, warnings? }",
      "atomicity": "atomic-with-rollback (BEGIN / COMMIT / ROLLBACK)."
    },
    {
      "name": "batch_delete_edges",
      "description": "Delete up to 50 edges in a single atomic Postgres transaction. All-or-nothing: any failure rolls back the entire batch.",
      "domain": "batch",
      "inputSchema": {
        "type": "object",
        "properties": {
          "product_id": {
            "type": "string",
            "description": "Product ID"
          },
          "edge_ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Edge IDs to delete (max 50)"
          }
        },
        "required": [
          "product_id",
          "edge_ids"
        ]
      },
      "throws": [
        "textError when `edge_ids` is missing / non-array / empty / >50, or\nany ID does not resolve in the given product."
      ],
      "examples": [],
      "warnings": [],
      "see": [
        "delete_edge",
        "batch_create_edges",
        "export_edges"
      ],
      "source": "src/tools/batch.ts:502",
      "symbol": "batchDeleteEdges",
      "returns": "JSON: `{ deleted: [id], count }`.",
      "return_shape": "{ deleted: [id], count }",
      "atomicity": "atomic-with-rollback (BEGIN / COMMIT / ROLLBACK)."
    },
    {
      "name": "batch_delete_nodes",
      "description": "Delete up to 50 entities and all their connected edges in a single atomic Postgres transaction. All-or-nothing: any failure rolls back the entire batch.",
      "domain": "batch",
      "inputSchema": {
        "type": "object",
        "properties": {
          "product_id": {
            "type": "string",
            "description": "Product ID"
          },
          "node_ids": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Node IDs to delete (max 50)"
          }
        },
        "required": [
          "product_id",
          "node_ids"
        ]
      },
      "throws": [
        "textError when `node_ids` is missing / non-array / empty / >50, or\nany ID does not resolve."
      ],
      "examples": [],
      "warnings": [
        "Cascade-deletes ALL edges incident on each node, in either\ndirection. Removal is hard; recovery flows through the audit log,\nwhich records each removal for the retention window."
      ],
      "see": [
        "delete_node",
        "batch_delete_edges",
        "deduplicate_nodes"
      ],
      "source": "src/tools/batch.ts:338",
      "symbol": "batchDeleteNodes",
      "returns": "JSON: `{ deleted: [id], count }`.",
      "return_shape": "{ deleted: [id], count }",
      "atomicity": "atomic-with-rollback (BEGIN / COMMIT / ROLLBACK)."
    },
    {
      "name": "batch_move_nodes",
      "description": "Re-parent up to 50 nodes in a single atomic Postgres transaction. For each move, old containment edges are removed and a new containment edge to new_parent_id is created. All-or-nothing: any failure rolls back the entire batch.",
      "domain": "batch",
      "inputSchema": {
        "type": "object",
        "properties": {
          "product_id": {
            "type": "string",
            "description": "Product ID"
          },
          "moves": {
            "type": "array",
            "description": "Move operations (max 50)",
            "items": {
              "type": "object",
              "properties": {
                "node_id": {
                  "type": "string",
                  "description": "The node to re-parent"
                },
                "new_parent_id": {
                  "type": "string",
                  "description": "The new parent node ID"
                }
              },
              "required": [
                "node_id",
                "new_parent_id"
              ]
            }
          }
        },
        "required": [
          "product_id",
          "moves"
        ]
      },
      "throws": [
        "textError when `moves` is missing / non-array / empty / >50, or any\n`node_id` / `new_parent_id` does not resolve."
      ],
      "examples": [],
      "warnings": [
        "Heuristic deletion of \"old containment\" edges relies on LIKE\npatterns (`%_contains_%`, `%_has_%`, `%_produces_%`) rather than the\ncanonical edge catalog. Edges matching the patterns yet semantically\nnon-containment may be removed alongside the intended ones. A follow-up\nwill tighten this to catalog-aware classification."
      ],
      "see": [
        "move_node",
        "batch_create_edges",
        "resolve_edge_for_pair"
      ],
      "source": "src/tools/batch.ts:573",
      "symbol": "batchMoveNodes",
      "returns": "JSON: `{ moved: [{ node_id, new_parent_id }], count }`.",
      "return_shape": "{ moved: [{ node_id, new_parent_id }], count }",
      "atomicity": "atomic-with-rollback (BEGIN / COMMIT / ROLLBACK)."
    },
    {
      "name": "batch_update_nodes",
      "description": "Update up to 50 entities in a single atomic Postgres transaction. Properties are merged with existing (not replaced). Unspecified fields are preserved. All-or-nothing: any failure rolls back the entire batch.",
      "domain": "batch",
      "inputSchema": {
        "type": "object",
        "properties": {
          "product_id": {
            "type": "string",
            "description": "Product ID"
          },
          "nodes": {
            "type": "array",
            "description": "Nodes to update (max 50)",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "description": "Node ID to update"
                },
                "title": {
                  "type": "string"
                },
                "description": {
                  "type": "string"
                },
                "tags": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "status": {
                  "type": "string"
                },
                "properties": {
                  "type": "object",
                  "description": "Merged with existing properties"
                }
              },
              "required": [
                "id"
              ]
            }
          }
        },
        "required": [
          "product_id",
          "nodes"
        ]
      },
      "throws": [
        "textError when `nodes` is missing / non-array / empty / >50, or any\nitem is missing `id`, or any `id` does not resolve."
      ],
      "examples": [],
      "warnings": [
        "Properties merge with `||`: top-level keys overwrite while nested\nkeys stay shallow (deep-merge stays out of scope). To clear a property,\npass it as `null`. Items with no setClauses (every field undefined) are\nsilently skipped."
      ],
      "see": [
        "update_node",
        "batch_create_nodes",
        "migrate_type"
      ],
      "source": "src/tools/batch.ts:216",
      "symbol": "batchUpdateNodes",
      "returns": "JSON: `{ updated: [id], count }`.",
      "return_shape": "{ updated: [id], count }",
      "atomicity": "atomic-with-rollback (BEGIN / COMMIT / ROLLBACK)."
    },
    {
      "name": "validate_graph",
      "description": "Validate a product graph for schema drift. Detects entity type drift (unknown types), edge type drift (unknown edge types), and property drift (missing expected properties, sampled over 500 nodes). Dangling edge checks are enforced by Postgres FK constraints and not re-reported here.",
      "domain": "validation",
      "inputSchema": {
        "type": "object",
        "properties": {
          "product_id": {
            "type": "string",
            "description": "Product ID"
          },
          "dry_run": {
            "type": "boolean",
            "description": "Always true for validate_graph (validation is read-only)."
          }
        },
        "required": [
          "product_id"
        ]
      },
      "throws": [
        "textError when `product_id` is missing or the product\nis not visible to the caller."
      ],
      "examples": [],
      "warnings": [
        "*Property drift is sampled** (first 500 nodes by id order);\nfor products beyond 500 nodes the drift list is incomplete. Each\nreported type carries one example node id; run again or query\n`list_nodes` for full coverage."
      ],
      "see": [
        "migrate_type",
        "migrate_cross_edges",
        "rename_edge_type",
        "list_anti_patterns",
        "list_type_migrations",
        "list_edge_migrations",
        "inspect"
      ],
      "source": "src/tools/validation.ts:83",
      "symbol": "validateGraph",
      "returns": "JSON: `{ valid, product_id, summary, entity_type_drift,\nedge_type_drift, property_drift, notes }`.",
      "return_shape": "{ valid, product_id, summary, entity_type_drift, edge_type_drift, property_drift, notes }",
      "atomicity": "atomic (read-only)"
    },
    {
      "name": "migrate_cross_edges",
      "description": "Find edges in upg.edges that carry a cross-product edge type and move them to upg.cross_product_edges. Cross-product edge types belong in the cross-product table; this tool corrects data from before the tightening. Defaults to dry_run=true.",
      "domain": "migrations",
      "inputSchema": {
        "type": "object",
        "properties": {
          "product_id": {
            "type": "string",
            "description": "Product ID"
          },
          "dry_run": {
            "type": "boolean",
            "description": "Default true: report what would move without moving."
          }
        },
        "required": [
          "product_id"
        ]
      },
      "throws": [
        "textError when `product_id` is missing."
      ],
      "examples": [],
      "warnings": [
        "Default is `dry_run: true`; pass `dry_run: false` to commit.\nIdempotent on retry: a second `dry_run: false` finds zero matching\nintra-product rows and reports `count: 0`. Migrated rows get a fresh\n`ce_*` id while the original edge id falls away; the audit log retains\nthe trail."
      ],
      "see": [
        "list_cross_edge_types",
        "list_portfolio_cross_edges",
        "validate_graph",
        "migrate_type"
      ],
      "source": "src/tools/migrations.ts:194",
      "symbol": "migrateCrossEdges",
      "returns": "JSON: `{ product_id, migrated, count, dry_run }`.",
      "return_shape": "{ product_id, migrated, count, dry_run }",
      "atomicity": "atomic-with-rollback (false only)"
    },
    {
      "name": "migrate_type",
      "description": "Bulk-retype all nodes of one entity type to another within a product. Catalog-aware: after renaming, re-infers edge types for all edges connected to the migrated nodes. Defaults to dry_run=true; pass dry_run=false to apply.",
      "domain": "migrations",
      "inputSchema": {
        "type": "object",
        "properties": {
          "product_id": {
            "type": "string",
            "description": "Product ID"
          },
          "from_type": {
            "type": "string",
            "description": "Current entity type to migrate away from"
          },
          "to_type": {
            "type": "string",
            "description": "New entity type. Must be a valid UPG entity type."
          },
          "dry_run": {
            "type": "boolean",
            "description": "Default true: count affected nodes without changing anything."
          }
        },
        "required": [
          "product_id",
          "from_type",
          "to_type"
        ]
      },
      "throws": [
        "textError when `product_id`, `from_type`, or `to_type`\nis missing, or when `to_type` is not a known UPG entity type."
      ],
      "examples": [],
      "warnings": [
        "Default is `dry_run: true`; pass `dry_run: false` to commit.\nIdempotent on retry: a second `dry_run: false` finds zero `from_type`\nnodes and reports `affected_nodes: 0`. Edge re-inference uses\n`resolveContainmentEdge`, so already-canonical edges may change type\nwhen the new pair has a different canonical edge."
      ],
      "see": [
        "validate_graph",
        "migrate_cross_edges",
        "rename_edge_type",
        "list_type_migrations",
        "list_entity_types"
      ],
      "source": "src/tools/migrations.ts:83",
      "symbol": "migrateType",
      "returns": "JSON: `{ from_type, to_type, affected_nodes, retyped_edges, dry_run }`.",
      "return_shape": "{ from_type, to_type, affected_nodes, retyped_edges, dry_run }",
      "atomicity": "atomic-with-rollback (false only)"
    }
  ]
}
