{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "visual-style",
  "description": "CE Brand Pipeline — visual direction schema. Load-bearing: all pipeline tools read from or write to this file.",
  "type": "object",
  "required": ["meta", "core", "visual", "extended", "ce_resonance", "origin"],
  "properties": {

    "meta": {
      "type": "object",
      "required": ["version", "brand", "category", "created_at", "updated_at", "locked"],
      "properties": {
        "version":    { "type": "string", "pattern": "^\\d+\\.\\d+(\\.\\d+)?$", "description": "Semver. Increment minor on field changes, major on structural changes." },
        "brand":      { "type": "string", "pattern": "^[a-z][a-z0-9_]*$", "description": "Snake_case slug. Must match brand field in taste board records." },
        "category":   { "type": "string", "enum": ["food_photography", "fintech", "lifestyle", "sports", "beauty", "hospitality", "tech", "fashion", "health", "automotive", "real_estate", "entertainment", "education", "events"] },
        "created_at": { "type": "string", "format": "date" },
        "updated_at": { "type": "string", "format": "date", "description": "Updated on every write. Tooling must set this automatically." },
        "locked": {
          "type": "boolean",
          "description": "When true: no pipeline tool may write to this file. Enforcement: all tools check locked flag before writing and throw a LockError if true. Unlocking requires manual CLI command: brand-pipeline unlock <brand>. Locked files may still be read."
        },
        "locked_at":       { "type": ["string", "null"], "format": "date", "description": "Date the direction was locked." },
        "locked_by":       { "type": ["string", "null"], "description": "Who triggered the lock (curator name)." },
        "locked_on_image": { "type": ["string", "null"], "description": "content_hash of the generation image that triggered the lock decision (Gate 2). Links A to B's evidentiary record." }
      }
    },

    "core": {
      "type": "object",
      "required": ["direction_name", "palette", "mood", "what_this_isnt", "prompt_framework"],
      "properties": {

        "direction_name": { "type": "string", "description": "The named visual territory. Human-readable. E.g. 'Liquid Gold Minimalism'." },

        "palette": {
          "type": "array",
          "minItems": 2,
          "maxItems": 8,
          "items": {
            "type": "object",
            "required": ["name", "hex", "role"],
            "properties": {
              "name": { "type": "string" },
              "hex":  { "type": "string", "pattern": "^#[0-9A-Fa-f]{6}$", "description": "Must be 6-digit hex with # prefix. No shorthand, no rgb(), no color names." },
              "role": { "type": "string", "enum": ["primary", "secondary", "accent", "background", "shadow", "neutral"], "description": "primary = dominant brand color | secondary = supporting | accent = highlight/CTA | background = field/canvas | shadow = dark zones | neutral = mid-tone bridge" }
            }
          }
        },

        "mood": {
          "type": "array",
          "minItems": 1,
          "maxItems": 4,
          "description": "Shared vocabulary with taste board schema. Use only values from this enum — freeform strings will fail cross-brand queries.",
          "items": {
            "type": "string",
            "enum": ["warm_luxury", "quiet_confidence", "raw_energy", "clinical_precise", "playful_bold", "premium_restrained", "organic_natural", "editorial_sharp", "digital_clean", "melancholic", "aspirational", "intimate", "cerebral", "kinetic", "sensory"]
          }
        },

        "what_this_isnt": {
          "type": "array",
          "minItems": 3,
          "items": { "type": "string" },
          "description": "Brand-level anti-patterns. Freeform — specificity matters more than consistency here. These reject wrong brand territory. Contextually scoped NOT lists (lighting, texture, color) live in visual.* and apply only within their prompt section. This list applies to brand-level prompt framing."
        },

        "prompt_framework": {
          "type": "object",
          "required": ["master", "suffix"],
          "properties": {
            "master": { "type": "string", "description": "Universal prefix applied BEFORE all shot-specific content in every generated prompt. Sets the brand's visual world." },
            "suffix": { "type": "string", "description": "Universal closer appended AFTER all shot-specific content in every generated prompt. Typically contains the consolidated NOT list for prompt use. Derived from what_this_isnt + visual.*.NOT fields." },
            "variants": {
              "type": "object",
              "description": "Format-specific overrides. If a variant exists, it replaces master+suffix for that format. If absent, master+suffix are used.",
              "properties": {
                "16:9": { "type": "string" },
                "3:4":  { "type": "string" },
                "1:1":  { "type": "string" }
              },
              "additionalProperties": false
            }
          }
        }
      }
    },

    "visual": {
      "type": "object",
      "required": ["lighting", "texture", "color_behavior"],
      "description": "Universal brand constants — apply across ALL shot types. Per-shot overrides go in extended.shot_library[x].lighting_note only. NOT arrays here are contextually scoped: lighting.NOT applies only to lighting prompt sections, texture.NOT to texture sections, color_behavior.NOT to color sections.",
      "properties": {

        "lighting": {
          "type": "object",
          "required": ["quality", "shadows", "fill", "NOT"],
          "properties": {
            "quality":  { "type": "string", "description": "Character of light. Reference vocabulary: warm / cool / neutral / hard / soft / directional / ambient / diffused / dramatic / flat" },
            "shadows":  { "type": "string", "description": "Shadow behavior and color." },
            "fill":     { "type": "string", "description": "Overall luminosity character. Reference vocabulary: high / low / airy / dense / lifted / deep" },
            "NOT":      { "type": "array", "items": { "type": "string" }, "description": "Lighting-specific anti-patterns ONLY. Injected into the lighting section of generated prompts. Do not duplicate brand-level what_this_isnt here." }
          }
        },

        "texture": {
          "type": "object",
          "required": ["character", "grain", "NOT"],
          "properties": {
            "character": { "type": "string", "description": "Surface quality. Reference vocabulary: tactile / smooth / rough / digital / organic / polished / raw / matte / glossy" },
            "grain":     { "type": "string", "description": "Film grain treatment. Reference vocabulary: none / subtle / medium / heavy" },
            "NOT":       { "type": "array", "items": { "type": "string" }, "description": "Texture/surface-specific anti-patterns ONLY. Injected into texture sections of generated prompts." }
          }
        },

        "color_behavior": {
          "type": "object",
          "required": ["cast", "temperature", "saturation", "NOT"],
          "properties": {
            "cast":        { "type": "string", "enum": ["monochromatic_warm", "monochromatic_cool", "split_tone", "neutral_desaturated", "high_contrast", "pastel_light", "deep_rich", "chromatic_pop"] },
            "temperature": { "type": "string", "description": "Warm/cool/neutral plus feel description. E.g. 'Very warm — 3500-4500K feel, golden hour warmth'" },
            "saturation":  { "type": "string", "enum": ["muted", "medium", "rich", "selective"], "description": "muted = desaturated throughout | medium = balanced | rich = saturated throughout | selective = saturated in specific zones only" },
            "NOT":         { "type": "array", "items": { "type": "string" }, "description": "Color/palette-specific anti-patterns ONLY. Injected into color sections of generated prompts." }
          }
        }
      }
    },

    "extended": {
      "type": "object",
      "required": ["shot_library"],
      "properties": {

        "shot_library": {
          "type": "object",
          "minProperties": 1,
          "propertyNames": { "pattern": "^[a-z][a-z0-9_]*$", "description": "Snake_case only. Tool 04 queries by exact key name." },
          "additionalProperties": {
            "type": "object",
            "required": ["archetype", "style", "description", "crop", "dof", "when", "prompt_variant"],
            "properties": {
              "archetype":     { "type": "string", "enum": ["product_hero", "lifestyle", "sports", "process_craft", "device_in_use", "portrait", "texture_macro", "environmental", "editorial", "data_visual"] },
              "style":         { "type": "string", "description": "Must be a valid style for the given archetype. See taste-board-vocabulary.json for per-archetype style lists." },
              "description":   { "type": "string" },
              "crop":          { "type": "string" },
              "dof":           { "type": "string" },
              "lighting_note": { "type": "string", "description": "Override or confirm visual.lighting for THIS shot type only. Leave empty to inherit." },
              "when":          { "type": "string", "description": "When to use this shot type in a deliverable set." },
              "prompt_variant":{ "type": "string", "description": "Slotted into prompt_framework.master + this + prompt_framework.suffix. May reference [SUBJECT] and [ANGLE] as placeholders." },
              "subject_refs":  { "type": "array", "items": { "type": "string" }, "description": "Keys from extended.subjects this shot type uses. Enables machine join: which shots use which subjects." }
            }
          }
        },

        "subjects": {
          "type": "object",
          "propertyNames": { "pattern": "^[a-z][a-z0-9_]*$" },
          "additionalProperties": {
            "type": "object",
            "required": ["what", "appearance"],
            "properties": {
              "what":          { "type": "string" },
              "appearance":    { "type": "string" },
              "realism_rules": { "type": "array", "items": { "type": "string" }, "description": "Specific fidelity requirements. E.g. 'Croissant: irregular honeycomb, uneven pockets, ragged torn edges — NOT uniform spirals'" },
              "NOT":           { "type": "array", "items": { "type": "string" } }
            }
          }
        },

        "usage_ratio": {
          "type": "object",
          "description": "Percentage allocation per shot type. Values are numbers 0-100. Sum of all values should equal 100. Keys must match shot_library keys.",
          "additionalProperties": { "type": "number", "minimum": 0, "maximum": 100 }
        }
      }
    },

    "ce_resonance": {
      "type": "object",
      "required": ["score"],
      "properties": {
        "score":      { "type": ["integer", "null"], "minimum": 1, "maximum": 10, "description": "Set by Assaf only. null = pending scoring. Null-scored files are valid working drafts but excluded from locked pipeline runs and Tool 04 brand queries." },
        "curator":    { "type": "string" },
        "scored_at":  { "type": "string", "format": "date" },
        "notes":      { "type": "string" }
      }
    },

    "origin": {
      "type": "object",
      "required": ["positioning_territory"],
      "description": "Records why this direction exists — the strategic and research inputs that produced it. Gate 1's output lives here. Brief document (D) is the authoritative source; this section captures the key outputs for schema-level traceability.",
      "properties": {
        "positioning_territory": {
          "type": "string",
          "description": "The strategic bet made at Gate 1. One sentence: what market position this direction is designed to occupy. E.g. 'Luxury premium end of B2B ingredient suppliers — territory no competitor occupies.'"
        },
        "competitive_gap": {
          "type": "string",
          "description": "The gap in the competitive field that this direction exploits. E.g. 'Competitors split between clinical biotech and consumer-friendly; luxury premium is unoccupied.'"
        },
        "analog_references": {
          "type": "array",
          "items": { "type": "string" },
          "description": "Analog industries or brands used to seed the direction. E.g. ['Aesop skincare', 'Le Labo', 'Holiland Butter Christmas']. Not the specific images — those are in seed_images."
        },
        "seed_images": {
          "type": "array",
          "items": { "type": "string" },
          "description": "content_hashes or URLs of the reference images from Stage 3 that seeded direction generation. Links A to the specific B records that drove the direction."
        },
        "rejected_alternatives": {
          "type": "array",
          "description": "Other directions generated and not chosen. Preserved for future use.",
          "items": {
            "type": "object",
            "required": ["direction_name", "reason_rejected"],
            "properties": {
              "direction_name":   { "type": "string" },
              "summary":          { "type": "string", "description": "One-line description of the direction." },
              "stars":            { "type": "integer", "minimum": 1, "maximum": 5, "description": "Relative score at time of selection (1-5). A 4-star rejected direction may be exactly right for a future client." },
              "reason_rejected":  { "type": "string", "description": "Why this direction was not chosen over the selected one." }
            }
          }
        },
        "brief_ref": {
          "type": "string",
          "description": "Reference to the Brief document (Deliverable D) that contains the full strategic rationale for this direction."
        }
      }
    },

    "validation_config": {
      "type": "object",
      "description": "Travels with the schema. Updated when tests change so schema and criteria stay in sync.",
      "properties": {
        "test_1_prompt_fidelity": { "type": "number", "minimum": 0, "maximum": 1, "description": "Minimum pass rate. Default 0.75 (3/4 images pass Assaf brand check)." },
        "test_3_negative_space":  { "type": "number", "minimum": 0, "maximum": 1, "description": "Minimum rejection rate. Default 0.90 (9/10 anti-pattern images correctly rejected)." },
        "last_validated":         { "type": "string", "format": "date" },
        "validated_by":           { "type": "string" }
      }
    }

  }
}
