{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://dotbabel.dev/schemas/dotbabel.config.schema.json",
  "title": "dotbabel project config",
  "description": "Shape of a repo's `.dotbabel.json`, which drives `dotbabel project-sync` and `dotbabel-check-project-sync`. Every key is optional; omitted keys fall back to DEFAULT_PROJECT_CONFIG in plugins/dotbabel/src/project-sync.mjs.",
  "type": "object",
  "additionalProperties": true,
  "properties": {
    "$schema": {
      "type": "string",
      "description": "Editor hint pointing at this schema."
    },
    "rule_floor_source": {
      "type": "string",
      "description": "Markdown file holding the canonical rule floor that instruction files are generated from.",
      "default": "CLAUDE.md"
    },
    "commands_dir": {
      "type": "string",
      "description": "Directory of Claude command markdown files that fan out to the other CLIs.",
      "default": ".claude/commands"
    },
    "skills_dir": {
      "type": "string",
      "description": "Directory of Claude skill directories that fan out to the other CLIs.",
      "default": ".claude/skills"
    },
    "fan_out": {
      "type": "array",
      "description": "CLIs to fan commands and skills out to. An unknown name is rejected at config load rather than skipped.",
      "default": ["codex", "gemini", "antigravity", "opencode", "copilot"],
      "items": {
        "type": "string",
        "enum": ["codex", "gemini", "antigravity", "opencode", "copilot"]
      }
    },
    "fan_out_layout": {
      "type": "string",
      "description": "Whether Codex and Gemini get their own skills trees or share one. 'per-cli' writes .codex/skills/ and .gemini/skills/ as two identical trees. 'shared' writes one canonical tree at .cli/skills/ and points both CLIs at it with a directory symlink, halving the tracked entries. Copilot is unaffected. Switch back to 'per-cli' if a CLI will not follow the redirect.",
      "default": "per-cli",
      "enum": ["per-cli", "shared"]
    },
    "gate_on_cli_presence": {
      "type": "boolean",
      "description": "When true, skip a CLI's symlink fan-out if its binary is absent from PATH. `dotbabel-check-project-sync` honors the same setting, so it will not report the skipped CLI as drift. Pass --all to either binary to override for one run. Instruction files are always written, never gated.",
      "default": true
    },
    "cli_excluded": {
      "type": "object",
      "description": "Per-CLI exclusion list. Maps a CLI name to command basenames (without .md) and skill ids that must not fan out to it, for commands that describe a Claude-only flow. An excluded entry already on disk is removed on the next sync; `dotbabel-check-project-sync` reports it as drift until then. Under fan_out_layout 'shared' an exclusion for codex or gemini applies to both, since they read one tree.",
      "default": {},
      "propertyNames": {
        "enum": ["codex", "gemini", "antigravity", "opencode", "copilot"]
      },
      "additionalProperties": {
        "type": "array",
        "items": { "type": "string", "minLength": 1 }
      }
    },
    "cli_substitutions": {
      "type": "object",
      "description": "Per-CLI text substitutions applied while rendering instruction files.",
      "additionalProperties": true
    },
    "targets": {
      "type": "array",
      "description": "Instruction files to generate from the rule floor.",
      "items": {
        "type": "object",
        "additionalProperties": true,
        "required": ["relativeOutputPath"],
        "properties": {
          "relativeOutputPath": {
            "type": "string",
            "description": "Repo-relative path of the generated instruction file."
          },
          "cliSet": {
            "type": "array",
            "description": "CLIs this instruction file serves.",
            "items": { "type": "string" }
          },
          "substitutionKey": {
            "type": "string",
            "description": "Key into cli_substitutions used when rendering this target."
          }
        }
      }
    },
    "attestation": {
      "type": "object",
      "description": "When the merge gate may reuse a local-attest run instead of re-verifying. Read from the BASE ref, never the head, so a pull request cannot relax its own enforcement. Absent means no enforcement, which is how a repository adopts this without its own adopting change being blocked by it.",
      "additionalProperties": false,
      "properties": {
        "enforce": {
          "type": "boolean",
          "default": false,
          "description": "Require trusted, current attestation evidence before a pull request may merge."
        },
        "governance_files": {
          "type": "array",
          "items": { "type": "string", "minLength": 1 },
          "description": "Files whose committed bytes are hashed into every attestation. A pull request that edits one of these cannot attest its own change: the gate recomputes the hash from the base ref and reports ATTESTATION_CONFIG_CHANGED. Defaults to ['.local-attest.config.mjs', '.dotbabel.json']."
        },
        "required_legs": {
          "type": "array",
          "items": { "type": "string", "minLength": 1 },
          "description": "Matrix leg names that must be present and passing in the evidence payload. Name the legs that merge-time verification has stopped running itself."
        },
        "trusted_associations": {
          "type": "array",
          "minItems": 1,
          "items": { "enum": ["OWNER", "MEMBER", "COLLABORATOR"] },
          "description": "GitHub author associations whose attestation the gate believes. Defaults to ['OWNER']. Associations weaker than COLLABORATOR are refused: anyone who can open a pull request could then authorize its own merge by skipping the suite and the quality profile."
        }
      }
    },
    "quality": {
      "$ref": "#/$defs/quality"
    },
    "criteria": {
      "$ref": "#/$defs/criteria"
    },
    "model_intelligence": {
      "type": "object",
      "description": "Project-level Model Intelligence policy (spec model-intelligence, §5). Policy and configuration overrides only: never a store for discovered models, resolved recommendations, cache evidence, generated projections, or release snapshot data.",
      "additionalProperties": false,
      "properties": {
        "floor": {
          "type": "object",
          "description": "A minimum capability bound for this repository. The strongest applicable floor wins, and no layer may lower a more specific one.",
          "additionalProperties": false,
          "required": ["mode", "requirement"],
          "properties": {
            "mode": { "const": "floor" },
            "requirement": {
              "type": "string",
              "enum": ["mechanical", "routine", "deep", "frontier", "exceptional"]
            },
            "rationale": {
              "type": "string",
              "minLength": 1,
              "description": "Why this floor exists. Carried into the explanation so a later maintainer can tell a deliberate safety fence from an experiment."
            }
          }
        },
        "pin": {
          "type": "object",
          "description": "One exact runtime-native configuration for this repository. It is honoured only when it satisfies every effective floor.",
          "additionalProperties": false,
          "required": ["mode", "runtime", "config"],
          "properties": {
            "mode": { "const": "pin" },
            "runtime": {
              "type": "string",
              "enum": ["claude", "codex", "gemini", "antigravity", "opencode", "copilot"]
            },
            "config": {
              "type": "object",
              "minProperties": 1,
              "description": "Opaque runtime-native values; Dotbabel does not parse their spelling."
            },
            "rationale": {
              "type": "string",
              "minLength": 1,
              "description": "Why this pin exists, carried into the explanation."
            }
          }
        },
        "preferences": {
          "type": "object",
          "description": "Resolver preferences. They order candidates that already satisfy the requirement; they never lower a floor. Closed to unknown keys, so a misspelled preference fails here instead of becoming inert policy.",
          "additionalProperties": false,
          "properties": {
            "$comment": { "type": "string" },
            "representation": {
              "type": "string",
              "enum": ["stable-alias", "native-id", "opaque-selector"]
            },
            "prefer_locally_verified": { "type": "boolean" },
            "prefer_fresher_evidence": { "type": "boolean" },
            "allow_stale_evidence": { "type": "boolean" },
            "allow_expired_evidence": { "type": "boolean" }
          }
        }
      }
    }
  },
  "$defs": {
    "criteria": {
      "type": "object",
      "additionalProperties": false,
      "description": "Acceptance-criteria settings (KD-14). The merge gate reads this object from the base ref, so a pull request cannot relax the rules that judge it.",
      "properties": {
        "pass_env": {
          "type": "array",
          "description": "Environment variable names criterion commands receive.",
          "default": [],
          "items": { "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" }
        },
        "timeout_seconds": {
          "type": "integer",
          "description": "Per-criterion command timeout.",
          "default": 600,
          "minimum": 1,
          "maximum": 3600
        },
        "enforcement": {
          "description": "\"warn\" moves criteria findings out of the merge verdict; the documented rollback switch.",
          "default": "block",
          "enum": ["block", "warn"]
        },
        "trusted_associations": {
          "type": "array",
          "description": "GitHub author associations whose evidence comment the merge gate believes.",
          "default": ["OWNER"],
          "minItems": 1,
          "items": { "type": "string", "pattern": "\\S" }
        },
        "require_ci_check": {
          "type": "boolean",
          "description": "Also require a successful \"dotbabel criteria\" check run on the head commit.",
          "default": false
        }
      }
    },
    "repositoryPath": {
      "type": "string",
      "minLength": 1,
      "not": {
        "anyOf": [
          { "pattern": "^(?:[\\\\/]|[A-Za-z]:)" },
          { "pattern": "(?:^|[\\\\/])\\.\\.(?:[\\\\/]|$)" }
        ]
      }
    },
    "qualityRule": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "enabled": { "type": "boolean" },
        "level": { "enum": ["error", "warning", "info"] },
        "threshold": { "type": "number", "minimum": 0 },
        "scope": { "enum": ["changed", "component", "repository"] },
        "on_unavailable": { "enum": ["error", "warning", "info"] },
        "profiles": {
          "type": "array",
          "uniqueItems": true,
          "items": { "enum": ["fast", "pr", "deep"] }
        }
      }
    },
    "qualityReport": {
      "type": "object",
      "additionalProperties": false,
      "required": ["format"],
      "properties": {
        "format": {
          "enum": [
            "exit-code",
            "go-coverprofile",
            "coveragepy-json",
            "istanbul-json",
            "lcov",
            "sarif",
            "golangci-json",
            "eslint-json",
            "ruff-json",
            "jscpd-json",
            "stryker-json",
            "dotbabel-v1"
          ]
        },
        "path": { "$ref": "#/$defs/repositoryPath" }
      },
      "allOf": [
        {
          "if": { "properties": { "format": { "const": "exit-code" } }, "required": ["format"] },
          "else": { "required": ["path"] }
        }
      ]
    },
    "qualityTool": {
      "type": "object",
      "additionalProperties": false,
      "required": ["argv"],
      "properties": {
        "argv": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 } },
        "paths": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/repositoryPath" } },
        "timeout_seconds": { "type": "integer", "minimum": 1, "maximum": 3600 },
        "report": { "$ref": "#/$defs/qualityReport" }
      }
    },
    "qualityComponent": {
      "type": "object",
      "additionalProperties": false,
      "required": ["root", "languages"],
      "properties": {
        "root": { "$ref": "#/$defs/repositoryPath" },
        "languages": {
          "type": "array",
          "minItems": 1,
          "uniqueItems": true,
          "items": { "type": "string", "minLength": 1 }
        },
        "tools": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "format": { "$ref": "#/$defs/qualityTool" },
            "compile": { "$ref": "#/$defs/qualityTool" },
            "typecheck": { "$ref": "#/$defs/qualityTool" },
            "lint": { "$ref": "#/$defs/qualityTool" },
            "test": { "$ref": "#/$defs/qualityTool" },
            "coverage": { "$ref": "#/$defs/qualityTool" },
            "complexity": { "$ref": "#/$defs/qualityTool" },
            "mutation": { "$ref": "#/$defs/qualityTool" },
            "dead-code": { "$ref": "#/$defs/qualityTool" },
            "dependencies": { "$ref": "#/$defs/qualityTool" },
            "duplication": { "$ref": "#/$defs/qualityTool" },
            "security": { "$ref": "#/$defs/qualityTool" },
            "race": { "$ref": "#/$defs/qualityTool" },
            "regression": { "$ref": "#/$defs/qualityTool" }
          }
        }
      }
    },
    "qualityException": {
      "type": "object",
      "additionalProperties": false,
      "required": ["id", "rule", "fingerprint", "reason", "expires"],
      "properties": {
        "id": { "type": "string", "pattern": "^QEX-[0-9]+$" },
        "rule": { "$ref": "#/$defs/qualityRuleId" },
        "fingerprint": { "type": "string", "pattern": "^sha256:" },
        "reason": { "type": "string", "minLength": 1 },
        "expires": { "type": "string", "format": "date" },
        "tracking": { "type": "string", "format": "uri" }
      }
    },
    "qualityRuleId": {
      "enum": [
        "correctness.format",
        "correctness.compile",
        "correctness.types",
        "correctness.tests",
        "correctness.regression",
        "correctness.lint",
        "security.high_confidence",
        "coverage.no_regression",
        "complexity.cognitive",
        "complexity.cyclomatic",
        "coverage.changed_lines",
        "coverage.changed_branches",
        "mutation.changed_score",
        "duplication.percent",
        "size.function_loc",
        "size.file_loc",
        "maintainability.dead_code",
        "maintainability.unused_dependencies",
        "semantic.ignored_errors",
        "semantic.swallowed_errors",
        "semantic.dynamic_types",
        "semantic.unchecked_assertions",
        "semantic.unbounded_concurrency",
        "semantic.lifecycle",
        "architecture.speculative_abstraction",
        "policy.new_suppression"
      ]
    },
    "quality": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "enabled": { "type": "boolean", "default": true },
        "default_profile": { "enum": ["fast", "pr", "deep"], "default": "fast" },
        "base_ref": { "type": "string", "minLength": 1 },
        "baseline_file": { "$ref": "#/$defs/repositoryPath" },
        "exclude": { "type": "array", "items": { "type": "string", "minLength": 1 } },
        "critical_paths": { "type": "array", "items": { "type": "string", "minLength": 1 } },
        "rules": {
          "type": "object",
          "additionalProperties": false,
          "propertyNames": { "$ref": "#/$defs/qualityRuleId" },
          "properties": {
            "correctness.format": { "$ref": "#/$defs/qualityRule" },
            "correctness.compile": { "$ref": "#/$defs/qualityRule" },
            "correctness.types": { "$ref": "#/$defs/qualityRule" },
            "correctness.tests": { "$ref": "#/$defs/qualityRule" },
            "correctness.regression": { "$ref": "#/$defs/qualityRule" },
            "correctness.lint": { "$ref": "#/$defs/qualityRule" },
            "security.high_confidence": { "$ref": "#/$defs/qualityRule" },
            "coverage.no_regression": { "$ref": "#/$defs/qualityRule" },
            "complexity.cognitive": { "$ref": "#/$defs/qualityRule" },
            "complexity.cyclomatic": { "$ref": "#/$defs/qualityRule" },
            "coverage.changed_lines": { "$ref": "#/$defs/qualityRule" },
            "coverage.changed_branches": { "$ref": "#/$defs/qualityRule" },
            "mutation.changed_score": { "$ref": "#/$defs/qualityRule" },
            "duplication.percent": { "$ref": "#/$defs/qualityRule" },
            "size.function_loc": { "$ref": "#/$defs/qualityRule" },
            "size.file_loc": { "$ref": "#/$defs/qualityRule" },
            "maintainability.dead_code": { "$ref": "#/$defs/qualityRule" },
            "maintainability.unused_dependencies": { "$ref": "#/$defs/qualityRule" },
            "semantic.ignored_errors": { "$ref": "#/$defs/qualityRule" },
            "semantic.swallowed_errors": { "$ref": "#/$defs/qualityRule" },
            "semantic.dynamic_types": { "$ref": "#/$defs/qualityRule" },
            "semantic.unchecked_assertions": { "$ref": "#/$defs/qualityRule" },
            "semantic.unbounded_concurrency": { "$ref": "#/$defs/qualityRule" },
            "semantic.lifecycle": { "$ref": "#/$defs/qualityRule" },
            "architecture.speculative_abstraction": { "$ref": "#/$defs/qualityRule" },
            "policy.new_suppression": { "$ref": "#/$defs/qualityRule" }
          }
        },
        "components": { "type": "array", "items": { "$ref": "#/$defs/qualityComponent" } },
        "exceptions": { "type": "array", "items": { "$ref": "#/$defs/qualityException" } }
      }
    }
  }
}
