{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "manni:structure:1.0.0",
  "title": "manni structure vocabulary v1.0.0",
  "description": "What the page connects to: products, concepts, and other pages. Relationships between pages — which no generator owns — never position in a navigation tree, which every generator does; there is deliberately no ordering, no parent, no nav title here. The applicability and concept fields are the harvest fallbacks of their deeper `graph` twins (manni:graph:1.0.0): where both are present, the deeper declaration wins.",
  "type": "object",
  "additionalProperties": true,
  "properties": {
    "applies-to": {
      "$ref": "#/$defs/stringList",
      "description": "What the page applies to, as flat product/variant/version labels — `operator-1.4`, `kubernetes`. Deliberately the same shape as `graph.applies-to`, whose deeper declaration wins when both are present. The labels are flat, with no named dimensions such as product vs deployment; an org that needs axes can prefix labels (`deploy:kubernetes`) or overlay its own schema.",
      "x-manni-location": "page"
    },
    "not-applicable-to": {
      "$ref": "#/$defs/stringList",
      "description": "Product/variant labels this page explicitly does NOT apply to — the carve-out `applies-to` alone cannot state: `applies-to: operator-1.4` with `not-applicable-to: operator-1.4-fips`. Same flat shape as `applies-to`, and the harvest fallback of the deeper `graph.not-applicable-to`, whose declaration wins when both are present. Disjointness from `applies-to` is a graph-layer (SHACL) check, exactly as it is one level down: JSON Schema cannot compare two sibling lists, and a page naming the same label on both sides passes here.",
      "x-manni-location": "page"
    },
    "concepts": {
      "$ref": "#/$defs/stringList",
      "description": "Controlled-vocabulary terms from your glossary that this page is about — a curated list, where `keywords` (manni:core:1.0.0) is free words. The bridge into knowledge-graph tooling, and the harvest fallback of the deeper `graph.concepts`.",
      "x-manni-location": "page"
    },
    "prerequisites": {
      "$ref": "#/$defs/stringList",
      "description": "Pages, ids, or capabilities the reader needs first. A relationship between pages — which no generator owns — not a position in a navigation tree, which every generator does.",
      "x-manni-location": "page"
    },
    "next-steps": {
      "$ref": "#/$defs/stringList",
      "description": "Where the reader goes after this page: several sensible follow-ons, unordered. Relationship, not navigation — the generator's `next`/`prev` position keys are a different fact and stay with the generator. **A deeper or more advanced treatment of the same topic belongs here too.** There is deliberately no separate `advanced` or `deeper-dive` field, and no ranking among these: \"more advanced\" is a claim about the reader, not about the page, and it is derived — two pages sharing `concepts` while pointing at `personas` of different levels (manni:audience:1.0.0) — which is the same reasoning that cut reader `expertise` from this family. A docs set with no persona definitions cannot derive it, and that is the deliberate day-one cost.",
      "x-manni-location": "page"
    },
    "related-pages": {
      "$ref": "#/$defs/stringList",
      "description": "Pages, ids, or URLs associatively related to this one. The `-pages` suffix is deliberate: `graph.related-concepts` relates concepts, this relates pages, and each name says which. **This is the field a site renders under a \"See also\" heading** — the key stays semantic, the label stays editorial, and there is deliberately no `see-also` alias, because one fact with two keys is the second surface this family exists to avoid. URLs are accepted, so an off-site \"Learn more\" target needs no separate field.",
      "x-manni-location": "page"
    }
  },
  "$defs": {
    "stringList": {
      "if": {
        "type": "array"
      },
      "then": {
        "type": "array",
        "minItems": 1,
        "uniqueItems": true,
        "items": {
          "type": "string",
          "minLength": 1
        }
      },
      "else": {
        "type": "string",
        "minLength": 1
      },
      "description": "One non-empty string, or a non-empty list of unique ones — the same shape as the `labelList` of manni:graph:1.0.0, so the harvest fallback and the deeper twin accept identical values."
    }
  }
}
