{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://news.openxel.com/schema/digest.v1.json",
  "title": "OPENXEL News daily digest, version 1",
  "description": "One day of the Startup Daily Digest. Summary bullets, then sections, each holding cards made of simple content blocks. Text fields are 'rich text': plain text plus **bold** and [label](https://url) only.",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "schema_version",
    "date",
    "title",
    "summary",
    "sections"
  ],
  "properties": {
    "schema_version": {
      "const": 1
    },
    "date": {
      "description": "The digest's day in Asia/Kolkata, YYYY-MM-DD. This is the storage key: posting the same date again replaces that day.",
      "type": "string",
      "pattern": "^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])$"
    },
    "title": {
      "type": "string",
      "minLength": 1,
      "maxLength": 120
    },
    "generated_at": {
      "description": "When the routine finished, ISO 8601 with offset, e.g. 2026-11-14T06:45:00+05:30.",
      "type": "string",
      "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}(:\\d{2}(\\.\\d+)?)?(Z|[+-]\\d{2}:\\d{2})$"
    },
    "summary": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "bullets"
      ],
      "properties": {
        "title": {
          "type": "string",
          "maxLength": 80,
          "default": "Today in 30 seconds"
        },
        "bullets": {
          "type": "array",
          "minItems": 1,
          "maxItems": 10,
          "items": {
            "$ref": "#/$defs/richText"
          }
        }
      }
    },
    "highlights": {
      "description": "Optional. 2–4 tiles shown in the page hero: the day's most interesting facts or a suggestion, each taken from a card in this digest. Example: {\"label\":\"Biggest raise\",\"value\":\"$1B\",\"detail\":\"Mirendil, Series A led by Kleiner Perkins\",\"card\":\"funding-news\"}.",
      "type": "array",
      "maxItems": 4,
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "label",
          "value"
        ],
        "properties": {
          "label": {
            "type": "string",
            "minLength": 1,
            "maxLength": 24,
            "description": "What the tile is, e.g. Biggest raise, Fastest grower, Next deadline, Idea to build."
          },
          "value": {
            "type": "string",
            "minLength": 1,
            "maxLength": 28,
            "description": "The headline figure or name, short: $1B, +325%, 15 Nov, Agent control panel."
          },
          "detail": {
            "type": "string",
            "minLength": 1,
            "maxLength": 90,
            "description": "One plain line of context: who, what, where."
          },
          "card": {
            "$ref": "#/$defs/slug",
            "description": "id of the card this comes from; must exist in this digest. The tile links to it."
          }
        }
      }
    },
    "sections": {
      "type": "array",
      "minItems": 1,
      "maxItems": 12,
      "items": {
        "$ref": "#/$defs/section"
      }
    },
    "footer": {
      "$ref": "#/$defs/richText"
    }
  },
  "$defs": {
    "richText": {
      "description": "Plain text. Allowed markup: **bold** and [label](https://url). No HTML.",
      "type": "string",
      "minLength": 1,
      "maxLength": 2000
    },
    "slug": {
      "type": "string",
      "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
      "maxLength": 40
    },
    "httpsUrl": {
      "type": "string",
      "pattern": "^https?://[^\\s]+$",
      "maxLength": 600
    },
    "tone": {
      "description": "Colour hint for a cell or item: good (green, e.g. growth or YES), warn (amber, e.g. KINDA), muted (grey, e.g. NOT REALLY), bad (red).",
      "enum": [
        "good",
        "warn",
        "muted",
        "bad"
      ]
    },
    "item": {
      "description": "A list item or table cell: either a rich-text string, or an object when it needs a tag chip or a tone.",
      "oneOf": [
        {
          "$ref": "#/$defs/richText"
        },
        {
          "type": "object",
          "additionalProperties": false,
          "required": [
            "text"
          ],
          "properties": {
            "text": {
              "$ref": "#/$defs/richText"
            },
            "tag": {
              "type": "string",
              "minLength": 1,
              "maxLength": 40
            },
            "tone": {
              "$ref": "#/$defs/tone"
            }
          }
        }
      ]
    },
    "section": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "id",
        "title",
        "cards"
      ],
      "properties": {
        "id": {
          "$ref": "#/$defs/slug"
        },
        "title": {
          "type": "string",
          "minLength": 1,
          "maxLength": 60
        },
        "cards": {
          "type": "array",
          "minItems": 1,
          "maxItems": 12,
          "items": {
            "$ref": "#/$defs/card"
          }
        }
      }
    },
    "card": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "id",
        "title",
        "status",
        "blocks",
        "sources"
      ],
      "properties": {
        "id": {
          "$ref": "#/$defs/slug"
        },
        "title": {
          "type": "string",
          "minLength": 1,
          "maxLength": 90
        },
        "url": {
          "$ref": "#/$defs/httpsUrl",
          "description": "The source's homepage, shown as 'open'."
        },
        "subtitle": {
          "$ref": "#/$defs/richText"
        },
        "status": {
          "description": "ok = read and has content. unavailable = the source could not be read today. nothing_new = read, but nothing changed since yesterday.",
          "enum": [
            "ok",
            "unavailable",
            "nothing_new"
          ]
        },
        "status_note": {
          "$ref": "#/$defs/richText",
          "description": "One line explaining an unavailable or nothing_new status."
        },
        "blocks": {
          "type": "array",
          "maxItems": 30,
          "items": {
            "$ref": "#/$defs/block"
          }
        },
        "sources": {
          "type": "array",
          "maxItems": 12,
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "label",
              "url"
            ],
            "properties": {
              "label": {
                "type": "string",
                "minLength": 1,
                "maxLength": 120
              },
              "url": {
                "$ref": "#/$defs/httpsUrl"
              }
            }
          }
        }
      }
    },
    "block": {
      "oneOf": [
        {
          "title": "paragraph",
          "type": "object",
          "additionalProperties": false,
          "required": [
            "type",
            "text"
          ],
          "properties": {
            "type": {
              "const": "paragraph"
            },
            "text": {
              "$ref": "#/$defs/richText"
            }
          }
        },
        {
          "title": "heading (a mini-heading inside a card)",
          "type": "object",
          "additionalProperties": false,
          "required": [
            "type",
            "text"
          ],
          "properties": {
            "type": {
              "const": "heading"
            },
            "text": {
              "type": "string",
              "minLength": 1,
              "maxLength": 90
            }
          }
        },
        {
          "title": "highlight (one large name, e.g. the idea of the day)",
          "type": "object",
          "additionalProperties": false,
          "required": [
            "type",
            "text"
          ],
          "properties": {
            "type": {
              "const": "highlight"
            },
            "text": {
              "type": "string",
              "minLength": 1,
              "maxLength": 120
            }
          }
        },
        {
          "title": "list / numbered_list",
          "type": "object",
          "additionalProperties": false,
          "required": [
            "type",
            "items"
          ],
          "properties": {
            "type": {
              "enum": [
                "list",
                "numbered_list"
              ]
            },
            "items": {
              "type": "array",
              "minItems": 1,
              "maxItems": 30,
              "items": {
                "$ref": "#/$defs/item"
              }
            }
          }
        },
        {
          "title": "table",
          "type": "object",
          "additionalProperties": false,
          "required": [
            "type",
            "columns",
            "rows"
          ],
          "properties": {
            "type": {
              "const": "table"
            },
            "columns": {
              "type": "array",
              "minItems": 1,
              "maxItems": 6,
              "items": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "label"
                ],
                "properties": {
                  "label": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 40
                  },
                  "align": {
                    "enum": [
                      "left",
                      "right"
                    ],
                    "default": "left"
                  }
                }
              }
            },
            "rows": {
              "description": "Each row has exactly as many cells as there are columns.",
              "type": "array",
              "minItems": 1,
              "maxItems": 40,
              "items": {
                "type": "array",
                "items": {
                  "$ref": "#/$defs/item"
                }
              }
            }
          }
        },
        {
          "title": "facts (label → value pairs, a table without a header)",
          "type": "object",
          "additionalProperties": false,
          "required": [
            "type",
            "items"
          ],
          "properties": {
            "type": {
              "const": "facts"
            },
            "items": {
              "type": "array",
              "minItems": 1,
              "maxItems": 12,
              "items": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "label",
                  "text"
                ],
                "properties": {
                  "label": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 40
                  },
                  "text": {
                    "$ref": "#/$defs/richText"
                  }
                }
              }
            }
          }
        },
        {
          "title": "note (an aside: the editor's take, a caveat, a coverage gap)",
          "type": "object",
          "additionalProperties": false,
          "required": [
            "type",
            "text"
          ],
          "properties": {
            "type": {
              "const": "note"
            },
            "text": {
              "$ref": "#/$defs/richText"
            }
          }
        }
      ]
    }
  }
}
