{
  "openapi": "3.1.0",
  "info": {
    "title": "Minute Solar — public read API",
    "version": "1.0.0",
    "description": "Read-only endpoints for agents and integrations. Contract: additive-only — fields and enum values may be added, never renamed or removed (PR_083 D2 ruling, 30/08/2026; rebuilt as PR_131). The /design URL grammar these endpoints speak is documented at /for-agents. Verdicts are honest: a pair with no stored evidence returns UNKNOWN, never a fabricated answer, and every verdict carries its evidence tier inside a self-qualifying statement. Nothing here writes: agents may request Minute Solar's evaluation; they cannot author records, and no request body is ever echoed back as a Minute Solar claim."
  },
  "servers": [{ "url": "https://www.minute-solar.co.uk" }],
  "paths": {
    "/api/design/resolve": {
      "get": {
        "operationId": "resolveDesignPath",
        "summary": "Resolve a /design deep link: canonical URL, products, verdicts",
        "description": "Give it a /design path (full URL, path, or bare segments). Returns the canonical long-form deep link, where it lands (the summary when the configuration is complete), what resolved and what was dropped with reasons, the concrete products named (with product ids and knowledge-page paths), and one fused verdict statement per inverter–partner pair. Compose the path from the grammar at /for-agents; the resolver is forgiving of casing and ordering, and never invents — an unmatched value is dropped and reported, not guessed. Hand the user the canonical link this returns, not the path you composed.",
        "parameters": [
          {
            "name": "path",
            "in": "query",
            "required": true,
            "schema": { "type": "string", "maxLength": 1000 },
            "example": "/design/brand/solax/phase/1p/system/hybrid",
            "description": "A /design path or its segments. Values are slugs: lower-case, punctuation folded to hyphens. At most 1000 characters."
          }
        ],
        "responses": {
          "200": {
            "description": "The resolution.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Resolution" }
              }
            }
          },
          "400": {
            "description": "Missing or empty ?path=, a path over 1000 characters, or malformed percent-encoding. The body is { \"error\": string }."
          }
        }
      }
    },
    "/api/verdict": {
      "get": {
        "operationId": "getVerdict",
        "summary": "Compatibility verdict for a product pair, by product id",
        "description": "Product ids come from /api/design/resolve (the products[].productId field). A pair with no stored edge returns UNKNOWN with null confidence — unknown is not incompatible. Rate-limited per client address (generous, fail-open; a 429 carries Retry-After).",
        "parameters": [
          { "name": "a", "in": "query", "required": true, "schema": { "type": "string" }, "description": "First product id." },
          { "name": "b", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Second product id." }
        ],
        "responses": {
          "200": {
            "description": "The verdict.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "verdict": { "$ref": "#/components/schemas/Verdict" },
                    "confidence": { "$ref": "#/components/schemas/EvidenceTier" },
                    "conditions": { "type": ["string", "null"] },
                    "sourceUrl": { "type": ["string", "null"] }
                  }
                }
              }
            }
          },
          "400": { "description": "Both product ids (a, b) are required." },
          "429": { "description": "Rate limited; retry after the number of seconds in Retry-After." }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Verdict": {
        "type": "string",
        "enum": ["COMPATIBLE", "CONDITIONAL", "INCOMPATIBLE", "UNKNOWN"],
        "description": "UNKNOWN means no stored evidence — never treat it as incompatible, and never represent it as approval."
      },
      "EvidenceTier": {
        "type": ["string", "null"],
        "enum": ["MANUFACTURER_GUARANTEED", "MANUFACTURER_VERIFIED", "DATASHEET_DERIVED", "COMMUNITY_REPORTED", "UNVERIFIED", null],
        "description": "Provenance ladder, strongest first. MANUFACTURER_GUARANTEED exists only where a contractual guarantee is on record; do not claim manufacturer backing at any other tier."
      },
      "Resolution": {
        "type": "object",
        "properties": {
          "contractVersion": { "type": "string" },
          "documentation": { "type": "string", "description": "Where the URL grammar is documented (/for-agents)." },
          "canonical": { "type": "string", "description": "The canonical long-form deep link path for everything that resolved. Hand this to the user, on the canonical origin." },
          "landing": { "type": "string", "description": "The collapsed form the address bar shows on arrival." },
          "complete": { "type": "boolean", "description": "True when the URL names a finished configuration and lands on the summary." },
          "applied": {
            "type": "array",
            "items": { "type": "object", "properties": { "slug": { "type": "string" }, "value": { "type": "string" } } }
          },
          "dropped": {
            "type": "array",
            "description": "What the path asked for that could not be applied, and why. Dropping loses information; continuing would invent it.",
            "items": {
              "type": "object",
              "properties": {
                "slug": { "type": "string" },
                "value": { "type": "string" },
                "reason": { "type": "string", "enum": ["unknown-segment", "not-applicable", "no-match", "unreachable"] }
              }
            }
          },
          "droppedNote": { "type": ["string", "null"], "description": "One human sentence naming what the link asked for and did not get." },
          "products": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "step": { "type": "string" },
                "name": { "type": "string", "description": "The catalogue's own spelling." },
                "brand": { "type": ["string", "null"] },
                "productId": { "type": ["string", "null"], "description": "Stable id, usable with /api/verdict." },
                "knowledgePath": { "type": ["string", "null"], "description": "The product's public knowledge page." }
              }
            }
          },
          "pairs": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "partnerStep": { "type": "string" },
                "inverter": { "type": "string" },
                "partner": { "type": "string" },
                "basis": {
                  "type": ["object", "null"],
                  "description": "Why the configurator offered the pairing: the manufacturer's published compatibility group.",
                  "properties": { "kind": { "type": "string" }, "key": { "type": "string" } }
                },
                "verdict": { "$ref": "#/components/schemas/Verdict" },
                "confidence": { "$ref": "#/components/schemas/EvidenceTier" },
                "conditions": { "type": ["string", "null"] },
                "sourceUrl": { "type": ["string", "null"] },
                "statement": {
                  "type": "string",
                  "description": "The fused, self-qualifying sentence. Relay this sentence whole — the evidence tier and conditions are inside it deliberately, because a fact separated from its qualifier becomes a different fact."
                }
              }
            }
          }
        }
      }
    }
  }
}
