{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://becivic.be/schemas/skill.schema.json",
  "title": "Be Civic — Skill frontmatter (v4)",
  "description": "Validates the YAML frontmatter of every skills/<id>/canonical.md file (parsed as a JSON-equivalent object). The schema does not validate the markdown body. v4 introduced by the 2026-05-08 status-collapse cutover: the v3 trio of `status` (active/deprecated/retracted), `version_status` (alpha/beta/stable/rolled_back/quarantined), and `corpus_status` (in-flight/candidate-*/omitted) collapses into a single `status` enum {draft, alpha, beta, stable, quarantined, deprecated} (6 values; `proposal` was removed per spec §6.1 footnote A2G1 — new skills land at status: alpha). Render-gate: alpha/beta/stable render publicly; the other three do not. Render metadata fields `description` and `og:image` are now declared explicitly (previously tolerated as schema-vs-corpus drift). See spec §6.1.",
  "type": "object",

  "required": [
    "id",
    "title",
    "schema_version",
    "status",
    "category"
  ],

  "allOf": [
    {
      "$comment": "Render-gated statuses (alpha/beta/stable) are real published content and require a version + submission_contract_version. Founder-WIP / terminal statuses (draft, quarantined, deprecated) do not — they may be skeletons or audit-preserved files. The runtime validator (validate-cross-refs.ts) additionally enforces the body-non-empty constraint for render-gated statuses.",
      "if": {
        "properties": { "status": { "enum": ["alpha", "beta", "stable"] } },
        "required": ["status"]
      },
      "then": {
        "required": ["version", "submission_contract_version"]
      }
    }
  ],

  "additionalProperties": false,

  "properties": {
    "id": {
      "type": "string",
      "pattern": "^[a-z0-9][a-z0-9-]*[a-z0-9]$",
      "minLength": 2,
      "maxLength": 100,
      "description": "Kebab-case skill ID matching the parent folder name. Flat namespace (no '/'). Cross-ref to folder name is a Worker / PR-CI responsibility."
    },

    "title": {
      "type": "string",
      "minLength": 1,
      "maxLength": 200,
      "description": "Human-readable skill title."
    },

    "description": {
      "type": "string",
      "maxLength": 500,
      "description": "Page-metadata description rendered into <meta name=\"description\"> by the renderer. Optional but present on every skill in practice."
    },

    "og:image": {
      "type": "string",
      "format": "uri",
      "description": "Page-metadata Open Graph image URL rendered into <meta property=\"og:image\"> by the renderer. Defaults to the project-wide og-default.png when set explicitly via the regenerate-manifest workflow."
    },

    "noindex": {
      "type": "boolean",
      "description": "Renderer directive: when true, exclude this skill from /skills index, llms.txt, llms-full.txt, and sitemap. Intended for decoy / test skills (e.g., test-lifecycle-smoke) that need to be cross-ref-resolvable but invisible to discovery surfaces."
    },

    "schema_version": {
      "type": "integer",
      "const": 4,
      "description": "Skill-frontmatter schema version. v4 — introduced by the 2026-05-08 status-collapse cutover: the v3 trio of `status` / `version_status` / `corpus_status` collapses into a single `status` enum. v3 corpora must be migrated via tools/scripts/migrate-frontmatter-v3-to-v4.ts. Earlier schema_version values are not accepted by this schema."
    },

    "version": {
      "type": "string",
      "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(-[0-9A-Za-z.-]+)?(\\+[0-9A-Za-z.-]+)?$",
      "description": "Semver of the skill content. Required when status is alpha/beta/stable (rendered content). Optional for draft/quarantined/deprecated (typically 0.0.0 for drafts)."
    },

    "proposal_id": {
      "type": "string",
      "pattern": "^prop_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
      "description": "prop_ prefix + UUIDv7 lowercase hex. Used for in-flight alpha/beta state-machine bookkeeping (links a canonical.md back to the originating skill_draft submission). Retained for audit on promotion to stable. `proposal` is not a valid status value — new skills land at status: alpha directly (per spec §6.2.3)."
    },

    "status": {
      "type": "string",
      "enum": ["draft", "alpha", "beta", "stable", "quarantined", "deprecated"],
      "description": "v4 unified status enum (6 values per spec §6.1). Render-gate: alpha/beta/stable render publicly; the other three do not. draft = founder-internal WIP (skeleton or pre-alpha content, covers all pre-alpha work); alpha = drafted real content awaiting validations; beta = state-machine-promoted with validations; stable = state-machine-promoted, validation thresholds met; quarantined = rejected by validation OR prompt-injection flag (terminal, not rendered, pulled for review); deprecated = superseded by a newer skill (terminal but soft, audit-preserved, superseded_by pointer required)."
    },

    "superseded_by": {
      "type": "string",
      "pattern": "^[a-z0-9][a-z0-9-]*[a-z0-9]$",
      "maxLength": 100,
      "description": "Optional. Skill ID that supersedes this one. Only meaningful when status != active."
    },

    "category": {
      "type": "string",
      "pattern": "^[a-z][a-z0-9-]+(-[a-z][a-z0-9-]+)*$",
      "maxLength": 100,
      "description": "Open enum from data/categories.json (per G.3). Format gate: ^[a-z][a-z0-9-]+(-[a-z][a-z0-9-]+)*$. Examples: belgium-federal, belgium-flemish-region, belgium-walloon-region, belgium-brussels-region, belgium-commune, origin-us-federal, origin-ng, meta. Levenshtein-≤2 typo defence and full-membership cross-ref are Worker / PR-CI responsibilities."
    },

    "applies_to": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "residency_status": {
          "type": "array",
          "items": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9_]*$",
            "maxLength": 60
          },
          "uniqueItems": true,
          "description": "Snake_case residency-status tokens (e.g., registered_resident, family_reunification)."
        },
        "visa_categories": {
          "type": "array",
          "items": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9_]*$",
            "maxLength": 60
          },
          "uniqueItems": true,
          "description": "Snake_case visa-category tokens."
        },
        "civil_status": {
          "type": "array",
          "items": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9_]*$",
            "maxLength": 60
          },
          "uniqueItems": true,
          "description": "Snake_case civil-status tokens (e.g., single, married, cohabiting_legal, divorced, widowed). Open enum; canonical list cross-ref enforced by Worker / PR-CI. Added in v2 per §6.2.2c (referenced as an example field path for amendments)."
        },
        "origin_countries": {
          "type": "array",
          "items": {
            "type": "string",
            "pattern": "^[a-z]{2}$"
          },
          "uniqueItems": true,
          "description": "ISO-3166-1 alpha-2, lowercase. Only meaningful for origin-* skills."
        },
        "communes": {
          "type": "array",
          "items": {
            "type": "string",
            "pattern": "^[0-9]{5}$"
          },
          "uniqueItems": true,
          "description": "NIS5 codes. Only meaningful for belgium-commune skills."
        }
      },
      "description": "Filter dimensions describing which users this skill applies to. All sub-fields optional; absent applies_to means 'no filter'."
    },

    "requires": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["id"],
        "additionalProperties": false,
        "properties": {
          "id": {
            "oneOf": [
              {
                "type": "string",
                "pattern": "^[a-z0-9][a-z0-9-]*[a-z0-9]$",
                "maxLength": 100,
                "description": "Stable skill ID — kebab-case, must resolve to an existing skill (cross-ref Worker / PR-CI)."
              },
              {
                "type": "string",
                "pattern": "^prop_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
                "description": "In-flight proposal_id — used when a draft skill depends on a sub-skill that itself was drafted recently and has not yet promoted to stable (per §6.6)."
              }
            ]
          },
          "selects_on": {
            "type": "object",
            "additionalProperties": false,
            "minProperties": 1,
            "properties": {
              "region": {
                "type": "array",
                "items": {"type": "string", "minLength": 1, "maxLength": 60},
                "uniqueItems": true,
                "minItems": 1
              },
              "origin_country": {
                "type": "array",
                "items": {"type": "string", "minLength": 1, "maxLength": 60},
                "uniqueItems": true,
                "minItems": 1
              },
              "sponsor_type": {
                "type": "array",
                "items": {"type": "string", "minLength": 1, "maxLength": 60},
                "uniqueItems": true,
                "minItems": 1
              },
              "entry_type": {
                "type": "array",
                "items": {"type": "string", "minLength": 1, "maxLength": 60},
                "uniqueItems": true,
                "minItems": 1
              },
              "card_outcome": {
                "type": "array",
                "items": {"type": "string", "minLength": 1, "maxLength": 60},
                "uniqueItems": true,
                "minItems": 1
              }
            },
            "description": "Optional dispatch hints — map of context-key → array of values for which the consuming agent should invoke this dependency. Keys are a closed set (region, origin_country, sponsor_type, entry_type, card_outcome); the values themselves are validated against schemas/types.json by the cross-ref validator."
          }
        }
      },
      "description": "Skill DAG dependencies (per §6.6). Entries may resolve to stable skills or to in-flight proposal_ids."
    },

    "regional_variation": {
      "type": "boolean",
      "description": "Optional. True when the procedure varies by region (Flemish, Walloon, Brussels) — captured during walks; informs applies_to.regions expansion and downstream dispatch via selects_on.region on dependents."
    },

    "recurring": {
      "type": "boolean",
      "description": "Optional. True for procedures that repeat on a cadence (e.g., annual tax declaration, ID renewal, mutuelle re-confirmation). Drives roadmap and reminder surfaces."
    },

    "walked_at": {
      "type": "string",
      "format": "date",
      "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$",
      "description": "Optional. ISO 8601 date the body was last researched in a graph walk. Drives staleness detection independently of last_verified (which tracks citation freshness)."
    },

    "authority_id": {
      "type": "string",
      "pattern": "^[a-z0-9][a-z0-9-]*[a-z0-9]$",
      "minLength": 2,
      "maxLength": 100,
      "description": "Optional. Kebab-case id referencing an entry in data/authorities.json. Replaces the free-text authority field that previously lived in canonical bodies. Cross-ref resolution enforced by validate-cross-refs.ts."
    },

    "volatile_value_ids": {
      "type": "array",
      "items": {
        "type": "string",
        "pattern": "^[a-z0-9][a-z0-9-]*[a-z0-9]$",
        "minLength": 2,
        "maxLength": 100
      },
      "uniqueItems": true,
      "description": "Optional. Array of kebab-case ids referencing entries in data/volatile-values.json. Preferred over inline volatile_values for values shared across skills (DVZ fees, RIS thresholds, indexation rates). Inline volatile_values overrides remain permitted for skill-specific scalars. Cross-ref resolution enforced by validate-cross-refs.ts."
    },

    "inputs": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["name", "type"],
        "additionalProperties": false,
        "properties": {
          "name": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9_]*$",
            "maxLength": 60
          },
          "type": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9_]*$",
            "maxLength": 60,
            "description": "Type token resolved against schemas/types.json. Initial set per §6.1: country_code, commune, document_artefact, string, number, date, bool."
          }
        }
      }
    },

    "outputs": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["name", "type"],
        "additionalProperties": false,
        "properties": {
          "name": {"type": "string", "pattern": "^[a-z][a-z0-9_]*$", "maxLength": 60},
          "type": {"type": "string", "pattern": "^[a-z][a-z0-9_]*$", "maxLength": 60},
          "description": {"type": "string", "minLength": 1, "maxLength": 300}
        }
      }
    },

    "requires_capabilities": {
      "type": "array",
      "items": {
        "type": "string",
        "enum": [
          "file_read",
          "structured_output",
          "multi_turn",
          "tool_execution",
          "pdf_generation",
          "web_fetch",
          "vision",
          "local_filesystem"
        ]
      },
      "uniqueItems": true,
      "description": "Closed enum per §6.7. Capability floor for any consumer."
    },

    "citations": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["ref", "url", "grade"],
        "additionalProperties": false,
        "properties": {
          "ref": {"type": "string", "minLength": 1, "maxLength": 500},
          "article": {"type": "string", "minLength": 1, "maxLength": 200},
          "url": {"type": "string", "format": "uri"},
          "archived_url": {"type": "string", "format": "uri"},
          "grade": {
            "type": "string",
            "enum": ["primary", "secondary"]
          }
        }
      },
      "description": "Legacy citations field. v2 corpus content uses the 'references' block below (per G.13); citations[] is retained for v1 compatibility."
    },

    "references": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/reference_entry"
      },
      "description": "References block (per G.13) — multilingual titles + URLs per official language + metadata. Each [ref-id] token in the body resolves to one entry here."
    },

    "last_verified": {
      "type": "string",
      "format": "date",
      "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$",
      "description": "ISO 8601 date the skill was last verified."
    },

    "verification_notes": {
      "type": "string",
      "minLength": 1,
      "maxLength": 500
    },

    "volatile_values": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["name", "type"],
        "additionalProperties": false,
        "properties": {
          "name": {"type": "string", "pattern": "^[a-z][a-z0-9_]*$", "maxLength": 80},
          "type": {
            "type": "string",
            "enum": ["number", "integer", "string"]
          }
        }
      },
      "description": "Named scalars for drift tracking (§6.3). Unit baked into the key."
    },

    "user_context_needed": {
      "type": "array",
      "items": {
        "type": "string",
        "pattern": "^[a-z][a-z0-9_]*$",
        "maxLength": 60
      },
      "uniqueItems": true
    },

    "submission_contract_version": {
      "type": "string",
      "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(-[0-9A-Za-z.-]+)?(\\+[0-9A-Za-z.-]+)?$",
      "description": "Semver of docs/submission-contract-v<N>.md the consuming agent must follow."
    }
  },

  "$defs": {
    "reference_entry": {
      "type": "object",
      "required": ["id", "type", "titles", "last_verified"],
      "additionalProperties": false,
      "properties": {
        "id": {
          "type": "string",
          "pattern": "^[a-z][a-z0-9-]*[a-z0-9]$",
          "maxLength": 100,
          "description": "Kebab-case ref-id. The opaque token cited as [ref-id] in the body."
        },
        "type": {
          "type": "string",
          "enum": ["statute", "spf-page", "inami-rule", "commune-page", "commune-procedure", "secondary"],
          "description": "Reference type. 'secondary' triggers the rationale requirement (per data/source-classes.json)."
        },
        "titles": {
          "type": "object",
          "minProperties": 1,
          "additionalProperties": false,
          "properties": {
            "fr": {"type": "string", "minLength": 1, "maxLength": 500},
            "nl": {"type": "string", "minLength": 1, "maxLength": 500},
            "de": {"type": "string", "minLength": 1, "maxLength": 500},
            "en": {"type": "string", "minLength": 1, "maxLength": 500}
          },
          "description": "Title per language code. Keys: official Belgian languages (fr, nl, de) plus optional en. At least one entry required."
        },
        "urls": {
          "type": "object",
          "minProperties": 1,
          "additionalProperties": false,
          "properties": {
            "fr": {"type": "string", "format": "uri"},
            "nl": {"type": "string", "format": "uri"},
            "de": {"type": "string", "format": "uri"},
            "en": {"type": "string", "format": "uri"}
          },
          "description": "URL per language code. Required unless justel_numac is provided (Belgian federal-law shortcut)."
        },
        "justel_numac": {
          "type": "string",
          "pattern": "^[0-9]{10}$",
          "description": "10-digit Justel NUMAC for Belgian federal law. When present, agent constructs language-specific URLs at runtime by appending ?language=fr|nl|de — urls block may be absent."
        },
        "last_verified": {
          "type": "string",
          "format": "date",
          "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
        },
        "rationale": {
          "type": "string",
          "minLength": 1,
          "maxLength": 500,
          "description": "Required when type='secondary' (per source-classes.json secondary_with_rationale_required). Explains why the secondary source is being used."
        },
        "archived_url": {
          "type": "string",
          "format": "uri",
          "description": "Optional Wayback / web.archive URL for citation rot resilience (§11)."
        }
      },
      "allOf": [
        {
          "description": "Secondary references must carry a rationale.",
          "if": {"properties": {"type": {"const": "secondary"}}, "required": ["type"]},
          "then": {"required": ["rationale"]}
        },
        {
          "description": "Either urls (per-language) or justel_numac must be present.",
          "anyOf": [
            {"required": ["urls"]},
            {"required": ["justel_numac"]}
          ]
        }
      ]
    }
  }
}
