{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://aburi.kage1020.com/schema/aburi.config.v1.json",
  "title": "Aburi Project Config",
  "description": "aburi.json / aburi.jsonc project configuration. All fields optional; empty config is valid (autodetect handles everything).",
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "$schema": {
      "const": "https://aburi.kage1020.com/schema/aburi.config.v1.json"
    },
    "ignore": {
      "type": "array",
      "items": { "type": "string", "minLength": 1 },
      "uniqueItems": true,
      "description": "Glob patterns added to drop-list category A (file-level skip). POSIX, workspace-root relative."
    },
    "respectGitignore": {
      "type": "boolean",
      "default": true,
      "description": "Whether to merge .gitignore patterns into drop-list category A."
    },
    "languages": {
      "type": "array",
      "items": { "$ref": "#/$defs/PluginRef" },
      "uniqueItems": true,
      "description": "Enabled language plugins (manifest names or relative paths)."
    },
    "frameworks": {
      "type": "array",
      "items": { "$ref": "#/$defs/PluginRef" },
      "uniqueItems": true,
      "description": "Enabled framework plugins. Order has no semantic effect."
    },
    "effects": {
      "type": "array",
      "items": { "$ref": "#/$defs/PluginRef" },
      "uniqueItems": true,
      "description": "Enabled effect plugins. Order determines first-match-wins priority."
    },
    "pluginOptions": {
      "type": "object",
      "additionalProperties": true,
      "propertyNames": { "$ref": "#/$defs/PluginManifestName" },
      "description": "Per-plugin opaque options keyed by manifest name."
    },
    "components": {
      "type": "array",
      "items": { "$ref": "#/$defs/ComponentOverride" }
    },
    "suppress": {
      "type": "array",
      "items": { "type": "string", "minLength": 1 },
      "uniqueItems": true,
      "description": "Identifier prefixes added to drop-list category C call drop."
    },
    "keep": {
      "type": "array",
      "items": { "type": "string", "minLength": 1 },
      "uniqueItems": true,
      "description": "Identifier prefixes or decorator names that escape drop. Wins over suppress and plugin/core drop rules."
    },
    "frameworkHints": {
      "type": "array",
      "items": { "$ref": "#/$defs/FrameworkHint" }
    },
    "output": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "dir": {
          "type": "string",
          "minLength": 1,
          "default": "out",
          "description": "Where scan and diff write their artifacts, and where explain reads the IR back from. Resolved against the working directory, as --output-dir is; --output-dir wins when both are given. May be absolute, which is why this is not a RelativePath."
        }
      }
    },
    "strict": {
      "type": "boolean",
      "default": true,
      "description": "If true, undeclared vocab emitted by plugins aborts extraction. If false, warnings are written to out/aburi-vocab-discovered.json."
    },
    "maxFileSizeBytes": {
      "type": "integer",
      "minimum": 1024,
      "default": 2097152,
      "description": "Files exceeding this size are skipped during scan to protect WASM heap. Default 2 MB."
    },
    "minParsedFileRatio": {
      "type": "number",
      "exclusiveMinimum": 0,
      "maximum": 1,
      "description": "Smallest share of discovered files a scan may parse and still exit 0. Absent by default: a scan that parsed at least one file passes whatever it lost. Set it and `aburi scan` exits 3 when parsedFiles / totalFiles falls below it, counting every skip reason. A workspace that discovers nothing, or parses nothing, exits 3 with or without this key."
    },
    "parseTimeoutMs": {
      "type": "integer",
      "minimum": 100,
      "default": 5000,
      "description": "Per-file parse timeout (parse + extract + walk combined). Files exceeding this are aborted and recorded as skipped. Default 5000 ms."
    },
    "classifyTimeoutMs": {
      "type": "integer",
      "minimum": 10,
      "maximum": 5000,
      "default": 50,
      "description": "Per-call timeout for effect plugin classify(). Exceeding calls fall through to next plugin and are recorded in stats.effectClassifyTimeouts[]. Default 50 ms; raise to 200-500 for SQL/regex-heavy effect plugins."
    },
    "lsp": {
      "type": "object",
      "additionalProperties": false,
      "description": "Optional LSP enrichment (docs/design/lsp-enrichment.md). Opt-in per-language; default off. Refines SourceRange columns, this./super./interface call resolution, Signature.inferredThrows, and CallEdge.confidence.",
      "properties": {
        "enabled": {
          "type": "boolean",
          "default": false,
          "description": "Master switch. When false the enrichment pass is a total no-op regardless of servers."
        },
        "servers": {
          "type": "object",
          "additionalProperties": { "$ref": "#/$defs/LspServerConfig" },
          "propertyNames": { "$ref": "#/$defs/LspLanguageKey" },
          "description": "Per-language server config. Keys are language short-form ids (typescript, python, go, …)."
        }
      }
    }
  },

  "$defs": {
    "PluginRef": {
      "type": "string",
      "minLength": 1,
      "description": "Manifest name (e.g., 'effects-prisma'), npm package id ('@scope/pkg'), or relative path ('./aburi-plugins/x.mjs')."
    },

    "PluginManifestName": {
      "type": "string",
      "pattern": "^[a-z][a-z0-9-]*$"
    },

    "ComponentOverride": {
      "type": "object",
      "additionalProperties": false,
      "required": ["id", "roots"],
      "properties": {
        "id": {
          "type": "string",
          "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$"
        },
        "name": { "type": "string", "minLength": 1 },
        "roots": {
          "type": "array",
          "items": { "$ref": "#/$defs/RelativePath" },
          "minItems": 1
        },
        "publicApi": {
          "type": "array",
          "items": { "type": "string", "minLength": 1 }
        },
        "languages": {
          "type": "array",
          "items": { "type": "string", "pattern": "^[a-z][a-z0-9]*$" }
        },
        "frameworks": {
          "type": "array",
          "items": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" }
        },
        "description": { "type": ["string", "null"] }
      }
    },

    "FrameworkHint": {
      "type": "object",
      "additionalProperties": false,
      "required": ["name"],
      "properties": {
        "name": {
          "type": "string",
          "pattern": "^[a-z][a-z0-9-]*$",
          "description": "Identifier used as the auto-registered ad-hoc plugin name and as Component.frameworks[] value."
        },
        "decorators": {
          "type": "object",
          "additionalProperties": { "$ref": "#/$defs/HintRule" },
          "description": "Keyed by decorator name (e.g., 'AcmeController')."
        },
        "classNamePatterns": {
          "type": "object",
          "additionalProperties": { "$ref": "#/$defs/HintRule" },
          "description": "Keyed by class name glob pattern (e.g., '*Handler')."
        }
      }
    },

    "HintRule": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "boundary": {
          "type": "boolean",
          "description": "Override Decorator.boundary for matching decorators. Ignored for classNamePatterns."
        },
        "extKind": {
          "type": "string",
          "pattern": "^framework(:[a-z][a-z0-9.-]*){2,}$",
          "description": "Set Symbol.extKind for matching symbols. Must start with 'framework:' and have at least three segments ('framework:<vendor>:<kind>'); the loader injects 'hint' as the second segment so the resulting ownership prefix is unique per hint name. Writing 'framework:hint:*' directly is rejected at load time."
        },
        "derivedBy": {
          "type": "string",
          "minLength": 1,
          "description": "Append to Symbol.derivedBy[]."
        },
        "drop": {
          "type": "boolean",
          "description": "If true, drop matching symbol as Category B with dropReason set from name."
        }
      }
    },

    "RelativePath": {
      "type": "string",
      "minLength": 1,
      "not": { "pattern": "\\\\" }
    },

    "LspLanguageKey": {
      "type": "string",
      "pattern": "^[a-z][a-z0-9]*$",
      "description": "Language short-form id (typescript, python, go, …). Matches ir-schema LanguageId."
    },

    "LspServerConfig": {
      "type": "object",
      "additionalProperties": false,
      "required": ["command"],
      "properties": {
        "command": {
          "type": "string",
          "minLength": 1,
          "description": "Server binary. PATH-resolvable or absolute path. Missing binaries trigger per-language fallback (lsp-enrichment.md §6.1)."
        },
        "args": {
          "type": "array",
          "items": { "type": "string" },
          "default": [],
          "description": "Arguments passed to the server binary (e.g., ['--stdio'])."
        },
        "initializeTimeoutMs": {
          "type": "integer",
          "minimum": 100,
          "default": 10000,
          "description": "Handshake timeout (lsp-enrichment.md §4.4). Default 10 s absorbs cold-disk starts."
        },
        "requestTimeoutMs": {
          "type": "integer",
          "minimum": 50,
          "default": 500,
          "description": "Per-request timeout. Exceeding requests fall back per-request (lsp-enrichment.md §6.1). Default 500 ms."
        },
        "fileBudgetMs": {
          "type": "integer",
          "minimum": 100,
          "default": 2000,
          "description": "Per-file budget across all requests for that file. Exceeding triggers per-file fallback. Default 2000 ms."
        },
        "concurrency": {
          "type": "integer",
          "minimum": 1,
          "default": 8,
          "description": "Max in-flight requests per file (lsp-enrichment.md §4.3)."
        },
        "initializationOptions": {
          "type": "object",
          "additionalProperties": true,
          "default": {},
          "description": "Opaque object forwarded verbatim to the server's initialize.initializationOptions. Server-specific; outside Aburi's compatibility scope."
        }
      }
    }
  }
}
