{
  "openapi": "3.1.1",
  "info": {
    "title": "Opsdeck Normalization Checker (keys, minimal cover, 2NF/3NF/BCNF, decomposition) API",
    "version": "1.0.0",
    "summary": "Candidate keys, minimal cover, 2NF/3NF/BCNF and lossless decompositions from FDs.",
    "description": "Every endpoint performs the same calculation as the human page and returns inputs, result, units, assumptions, sources, warnings and computedAt. MCP endpoint: /mcp."
  },
  "servers": [
    {
      "url": "https://opsdecknormalizationchecker.usefulweb.app"
    }
  ],
  "paths": {
    "/api/v1/calculate": {
      "post": {
        "operationId": "calculate",
        "summary": "Candidate keys, minimal cover, 2NF/3NF/BCNF and lossless decompositions from FDs.",
        "description": "Enter a relation and its functional dependencies (or paste sample rows and let the tool propose dependencies as hypotheses with support counts). You get attribute closures, all candidate keys, prime attributes, a minimal cover, the highest normal form with every violating dependency explained, a 3NF synthesis (Bernstein: lossless and dependency preserving) and a BCNF decomposition, each with the chase test for lossless joins and the dependency-preservation test. Any decomposition of your own can be tested too.\n\nMethodology:\n- Attribute closure X+ is computed by applying the dependencies until nothing changes. Attributes that never appear on a right side are in every key; the remaining candidates are searched by increasing size, skipping supersets of keys already found.\n- Minimal (canonical) cover: right sides split into single attributes, extraneous left-side attributes removed, redundant dependencies removed (Silberschatz et al., section 7.4.3).\n- Normal forms (first normal form is assumed): 2NF fails when a non-prime attribute depends on part of a candidate key; 3NF fails when X → A has X not a superkey and A non-prime; BCNF fails when X → A has X not a superkey. Checking the given dependencies suffices for 3NF and BCNF of the relation itself; parts of a decomposition use the dependencies projected onto them.\n- 3NF synthesis (Bernstein): one relation per left side of the minimal cover, plus a candidate key if no relation contains one; relations contained in others are removed. BCNF decomposition splits on a violating X → Y into X+ and R − (X+ − X) until every part is in BCNF.\n- Lossless join: the chase (tableau) test. Dependency preservation: each dependency of the cover is checked by growing the closure of its left side through the parts only (Ullman’s algorithm, no projection needed).\n- Sample rows: every left side of up to three attributes is checked against every other attribute; only minimal dependencies that hold in all rows are proposed, with the number of row groups that share the left side as support. Unique attribute sets are listed as key hypotheses.",
        "tags": [
          "apps"
        ],
        "x-mcp-tool": "opsdeck_normalization_checker",
        "x-owner-app": "opsdeck",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OpsdeckNormalizationCheckerInput"
              },
              "examples": {
                "example1": {
                  "summary": "EMP_PROJ: partial dependencies (not 2NF) (Elmasri & Navathe)",
                  "value": {
                    "relation": "EMP_PROJ(Ssn, Pnumber, Hours, Ename, Pname, Plocation)",
                    "fds": "Ssn, Pnumber -> Hours\nSsn -> Ename\nPnumber -> Pname, Plocation"
                  }
                },
                "example2": {
                  "summary": "EMP_DEPT: transitive dependency (2NF, not 3NF) (Elmasri & Navathe)",
                  "value": {
                    "relation": "EMP_DEPT(Ename, Ssn, Bdate, Address, Dnumber, Dname, Dmgr_ssn)",
                    "fds": "Ssn -> Ename, Bdate, Address, Dnumber\nDnumber -> Dname, Dmgr_ssn"
                  }
                },
                "example3": {
                  "summary": "EMP_PROJ split into EMP_LOCS + EMP_PROJ1: a lossy decomposition (Elmasri & Navathe)",
                  "value": {
                    "relation": "EMP_PROJ(Ssn, Pnumber, Hours, Ename, Pname, Plocation)",
                    "fds": "Ssn -> Ename\nPnumber -> Pname, Plocation\nSsn, Pnumber -> Hours",
                    "decomposition": "EMP_LOCS(Ename, Plocation)\nEMP_PROJ1(Ssn, Pnumber, Hours, Pname, Plocation)"
                  }
                },
                "example4": {
                  "summary": "R(A, B, C, G, H, I): key AG (Silberschatz)",
                  "value": {
                    "relation": "R(A, B, C, G, H, I)",
                    "fds": "A -> B\nA -> C\nCG -> H\nCG -> I\nB -> H"
                  }
                },
                "example5": {
                  "summary": "Canonical cover of A → BC, B → C, A → B, AB → C (Silberschatz)",
                  "value": {
                    "relation": "R(A, B, C)",
                    "fds": "A -> BC\nB -> C\nA -> B\nAB -> C"
                  }
                },
                "example6": {
                  "summary": "Dependencies mined from sample order lines (hypotheses)",
                  "value": {
                    "relation": "",
                    "fds": "",
                    "sampleCsv": "order_id,customer,city,product,price\n1,Ann,Berlin,Pen,2\n1,Ann,Berlin,Ink,5\n2,Bob,Paris,Pen,2\n3,Ann,Berlin,Pad,3\n3,Ann,Berlin,Pen,2",
                    "maxLhs": 2,
                    "useMinedFds": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculation result envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OpsdeckNormalizationCheckerResult"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid input (see errors[]).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (see Retry-After).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tool": {
      "get": {
        "operationId": "getToolDefinition",
        "summary": "Inputs, output schema and examples for this tool",
        "responses": {
          "200": {
            "description": "Tool definition.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem details. 422 responses include per-field `errors`.",
        "required": [
          "type",
          "title",
          "status",
          "detail"
        ],
        "properties": {
          "type": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "integer"
          },
          "detail": {
            "type": "string"
          },
          "code": {
            "type": "string"
          },
          "errors": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "field",
                "message"
              ],
              "properties": {
                "field": {
                  "type": "string"
                },
                "message": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "OpsdeckNormalizationCheckerInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "relation": {
            "title": "Relation",
            "description": "Name and attributes. Leave empty to take the attributes from the header of the sample rows.",
            "type": "string",
            "maxLength": 2000,
            "default": ""
          },
          "fds": {
            "title": "Functional dependencies (one per line)",
            "description": "Write A, B -> C, D. With one-letter attributes AB -> C works too.",
            "type": "string",
            "maxLength": 100000,
            "default": ""
          },
          "decomposition": {
            "title": "Decomposition to test (optional)",
            "description": "One relation per line; the tool runs the chase test and the dependency-preservation test on it.",
            "type": "string",
            "maxLength": 100000,
            "default": ""
          },
          "sampleCsv": {
            "title": "Sample rows (CSV with header, optional)",
            "description": "Dependencies that hold in every row are proposed as hypotheses with support counts. A sample can only refute a dependency, never prove it.",
            "type": "string",
            "maxLength": 1000000,
            "default": ""
          },
          "maxLhs": {
            "title": "Largest left side to search in the sample",
            "type": "integer",
            "minimum": 1,
            "maximum": 3,
            "default": 2
          },
          "useMinedFds": {
            "title": "Use the dependencies found in the sample for the analysis",
            "type": "boolean",
            "default": true
          }
        },
        "required": []
      },
      "OpsdeckNormalizationCheckerResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schemaVersion",
          "tool",
          "computedAt",
          "inputs",
          "summary",
          "result",
          "units",
          "assumptions",
          "sources",
          "warnings",
          "presentation",
          "links"
        ],
        "properties": {
          "schemaVersion": {
            "type": "string",
            "const": "1.0"
          },
          "tool": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id",
              "name",
              "version"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            }
          },
          "computedAt": {
            "type": "string",
            "format": "date-time"
          },
          "inputs": {
            "type": "object",
            "description": "Normalised inputs actually used, including applied defaults."
          },
          "summary": {
            "type": "string"
          },
          "result": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "relation",
              "fdSource",
              "fds",
              "closures",
              "candidateKeys",
              "keySearchTruncated",
              "primeAttributes",
              "nonPrimeAttributes",
              "minimalCover",
              "normalForm",
              "violations",
              "synthesis3nf",
              "bcnf",
              "checked",
              "mined"
            ],
            "properties": {
              "relation": {
                "type": "object"
              },
              "fdSource": {
                "enum": [
                  "given",
                  "mined",
                  "given+mined"
                ]
              },
              "fds": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "closures": {
                "type": "array",
                "items": {
                  "type": "object"
                }
              },
              "candidateKeys": {
                "type": "array",
                "items": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              },
              "keySearchTruncated": {
                "type": "boolean"
              },
              "primeAttributes": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "nonPrimeAttributes": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "minimalCover": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "normalForm": {
                "enum": [
                  "1NF",
                  "2NF",
                  "3NF",
                  "BCNF"
                ]
              },
              "violations": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "form",
                    "dependency",
                    "reason"
                  ],
                  "properties": {
                    "form": {
                      "enum": [
                        "2NF",
                        "3NF",
                        "BCNF"
                      ]
                    },
                    "dependency": {
                      "type": "string"
                    },
                    "reason": {
                      "type": "string"
                    }
                  }
                }
              },
              "synthesis3nf": {
                "type": "object",
                "required": [
                  "relations",
                  "lossless",
                  "dependencyPreserving",
                  "lostDependencies",
                  "chase"
                ],
                "properties": {
                  "relations": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    }
                  },
                  "lossless": {
                    "type": "boolean"
                  },
                  "dependencyPreserving": {
                    "type": "boolean"
                  },
                  "lostDependencies": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "chase": {
                    "type": "object"
                  }
                },
                "description": "3NF synthesis from the minimal cover (lossless, dependency preserving)."
              },
              "bcnf": {
                "type": "object",
                "required": [
                  "relations",
                  "lossless",
                  "dependencyPreserving",
                  "lostDependencies",
                  "chase"
                ],
                "properties": {
                  "relations": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    }
                  },
                  "lossless": {
                    "type": "boolean"
                  },
                  "dependencyPreserving": {
                    "type": "boolean"
                  },
                  "lostDependencies": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "chase": {
                    "type": "object"
                  }
                },
                "description": "BCNF decomposition (lossless; may lose dependencies)."
              },
              "checked": {
                "type": [
                  "object",
                  "null"
                ],
                "required": [
                  "relations",
                  "lossless",
                  "dependencyPreserving",
                  "lostDependencies",
                  "chase"
                ],
                "properties": {
                  "relations": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    }
                  },
                  "lossless": {
                    "type": "boolean"
                  },
                  "dependencyPreserving": {
                    "type": "boolean"
                  },
                  "lostDependencies": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "chase": {
                    "type": "object"
                  }
                },
                "description": "Your decomposition, tested."
              },
              "mined": {
                "type": [
                  "object",
                  "null"
                ],
                "description": "Dependency hypotheses from the sample rows with their support (groups of rows that share the left side)."
              }
            }
          },
          "units": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "assumptions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "title"
              ],
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "publisher": {
                  "type": "string"
                },
                "retrievedAt": {
                  "type": "string"
                },
                "note": {
                  "type": "string"
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "presentation": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Display blocks used by the human page (same data)."
          },
          "links": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "human",
              "api",
              "openapi",
              "methodology"
            ],
            "properties": {
              "human": {
                "type": "string",
                "format": "uri"
              },
              "api": {
                "type": "string",
                "format": "uri"
              },
              "openapi": {
                "type": "string",
                "format": "uri"
              },
              "methodology": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      }
    }
  }
}