{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://latwoodtech.com/schemas/features.schema.json",
  "title": "features.yml — Feature Truth intent registry (schema v2)",
  "description": "Hand-authored feature intent for one app repo. Status is never declared here; the feature-truth engine computes it. See docs/standards/feature-truth.md.",
  "type": "object",
  "required": ["version", "app", "scan", "features"],
  "additionalProperties": false,
  "properties": {
    "version": { "const": 2 },
    "app": {
      "type": "string",
      "description": "Matches the `id` field in Factory docs/service-registry.yml",
      "pattern": "^[a-z0-9][a-z0-9-]*$"
    },
    "scan": {
      "type": "object",
      "required": ["worker_src", "web_src"],
      "additionalProperties": false,
      "properties": {
        "worker_src": { "$ref": "#/$defs/pathList" },
        "web_src": { "$ref": "#/$defs/pathList" },
        "nav_files": { "$ref": "#/$defs/pathList" },
        "test_dirs": { "$ref": "#/$defs/pathList" }
      }
    },
    "route_files": {
      "$ref": "#/$defs/pathList",
      "description": "Directories whose files must each be claimed by a feature's `sources` (orphan gate)"
    },
    "exempt_route_files": {
      "$ref": "#/$defs/pathList",
      "description": "Route files exempt from feature ownership (health, admin telemetry, infra)"
    },
    "features": {
      "type": "array",
      "minItems": 1,
      "items": { "$ref": "#/$defs/feature" }
    }
  },
  "$defs": {
    "pathList": {
      "type": "array",
      "items": { "type": "string", "minLength": 1 }
    },
    "feature": {
      "type": "object",
      "required": ["id", "title", "actor", "story", "expect"],
      "additionalProperties": false,
      "properties": {
        "id": {
          "type": "string",
          "pattern": "^F-[A-Z0-9]+-[0-9]{2,}$",
          "description": "Stable feature ID, F-<DOMAIN>-NN"
        },
        "title": { "type": "string", "minLength": 3 },
        "actor": {
          "type": "string",
          "description": "Who this is for: user, creator, admin, operator, partner, guest, …"
        },
        "story": {
          "type": "string",
          "minLength": 10,
          "description": "As a <actor>, I can <capability> so that <outcome>."
        },
        "expect": {
          "enum": ["planned", "built", "wired", "reachable", "shipped", "alive"],
          "description": "The human claim. The engine hard-fails when the computed rung is lower."
        },
        "surface": {
          "enum": ["web", "partner", "internal", "ops"],
          "description": "Who calls this feature. web (default): first-party web caller expected. partner: external caller by design (webhooks, OAuth IdP, crawlers). internal/ops: no caller expected (crons, pipelines, admin tooling) — wired/reachable inherit built."
        },
        "routes": {
          "type": "array",
          "items": {
            "type": "string",
            "pattern": "^(GET|POST|PUT|PATCH|DELETE|ALL) /\\S*$"
          },
          "description": "METHOD /path — drives built + wired signals"
        },
        "pages": {
          "type": "array",
          "items": { "type": "string", "pattern": "^/" },
          "description": "Web page paths — drive wired + reachable signals"
        },
        "sources": {
          "$ref": "#/$defs/pathList",
          "description": "Files this feature claims (orphan gate ownership + built fallback)"
        },
        "analytics_events": {
          "type": "array",
          "items": { "type": "string", "minLength": 1 },
          "description": "PostHog event names — drive the alive rung"
        },
        "tests": {
          "$ref": "#/$defs/pathList",
          "description": "Optional explicit test files; @feature tags in test code also bind"
        },
        "owner": { "type": "string" },
        "notes": { "type": "string" }
      }
    }
  }
}
