{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://codafort.dev/schemas/finding-v1.schema.json",
  "title": "coda-finding/1",
  "description": "O Finding canônico do Codafort (PRD §4.3): o mesmo objeto atravessa os dois momentos (src|run) e todas as superfícies (CLI, MCP, agentes, plataforma). Invariante D1: o finding emitido pelo CLI gratuito é byte-idêntico ao que a plataforma paga governa — o paywall fica no workflow AO REDOR do finding, nunca no finding.",
  "type": "object",
  "additionalProperties": false,
  "required": ["schema", "id", "moment", "rule", "severity", "message", "location", "provenance"],
  "properties": {
    "schema": {
      "const": "coda-finding/1"
    },
    "id": {
      "type": "string",
      "minLength": 1,
      "description": "Identidade estável do finding. src: vuln_hash (determinístico entre re-runs — é o mesmo hash que ancora os IDs de sessão SF-n); run: crash_id (8 hex, fingerprint FNV-1a da falha)."
    },
    "moment": {
      "enum": ["src", "run"],
      "description": "src = análise de código-fonte (pré-execução); run = análise de artefato/execução (forense)."
    },
    "rule": {
      "type": "object",
      "additionalProperties": false,
      "required": ["id"],
      "properties": {
        "id": {
          "type": "string",
          "minLength": 1,
          "description": "src: rule_id do catálogo (ex.: SS-PY-EXEC-TAINT); run: classe da falha (ex.: SIGSEGV, EXCEPTION_ACCESS_VIOLATION)."
        },
        "kind": {
          "type": ["string", "null"],
          "description": "src: vulnerability | bug | code_smell | security_hotspot; run: crash | deadlock."
        }
      }
    },
    "severity": {
      "enum": ["critical", "high", "medium", "low", "info"],
      "description": "Escala canônica. src: bijeção com a escala interna (Blocker→critical, Critical→high, Major→medium, Minor→low, Info→info — a mesma de ImpactSeverity); run: derivada da explorabilidade (Fase E)."
    },
    "exploitability": {
      "enum": ["high", "medium", "low", "unknown", null],
      "description": "run: classificação estilo !exploitable (Fase E). src: null até existir exploitability baseada em reachability."
    },
    "confidence": {
      "enum": ["high", "medium", "low", null],
      "description": "src: confiança da regra/análise. run: null (a explorabilidade já carrega a incerteza)."
    },
    "confirmation": {
      "enum": ["candidate", "statically_supported", "runtime_observed", "dynamically_confirmed", "exploit_demonstrated", "disproven_under_tested_conditions", "inconclusive"],
      "description": "Degrau da escada de confirmação (PC-1.1 do plan-progressive-confirmation), separado da severidade: candidate (suspected), statically_supported (o estático sustenta), runtime_observed (o dado alcançou o sink em execução), dynamically_confirmed (teste dinâmico reproduziu), exploit_demonstrated (execução controlada e benigna demonstrada: marcador ecoado, aritmética, callback OOB com nonce), disproven_under_tested_conditions (o sink foi alcançado com o dado sanitizado no tráfego testado; não suprime nada) e inconclusive (testado, sem veredito)."
    },
    "observations": {
      "type": "array",
      "description": "Quem viu o quê: uma entrada por modalidade que contribuiu para o degrau (PC-1.1). Ausente sem observação de outra modalidade.",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["modality", "kind"],
        "properties": {
          "modality": { "type": "string", "description": "iast, dast, run ou src." },
          "kind": { "type": "string", "description": "O veredito ou a prova (confirmed-at-runtime, sanitized-at-runtime, o oráculo do DAST…)." },
          "ref": { "type": "string", "description": "Referência à prova na fonte: id da troca no audit do DAST, id do evento do IAST." }
        }
      }
    },
    "entrypoint": {
      "type": "object",
      "additionalProperties": false,
      "required": ["method", "path"],
      "description": "Rota HTTP do handler que o fluxo atravessa (PC-1.2 do plan-progressive-confirmation): o verbo e o caminho declarados no código, sem o contexto do deploy. Leva o finding ao teste dinâmico dirigido e à junção com o DAST. Ausente fora de handler roteado.",
      "properties": {
        "method": { "type": "string", "description": "GET, POST, PUT, DELETE, PATCH ou ANY (a rota não fixa o verbo)." },
        "path": { "type": "string", "description": "Caminho como declarado, com os parâmetros na sintaxe do framework ({id}, <uid>, :id)." }
      }
    },
    "score": {
      "type": "number",
      "minimum": 0,
      "maximum": 1,
      "description": "Precisão medida da regra nas réguas de terceiro que dirigem volta, (TP + 1) / (TP + FP + 2) — ordena a fila e dá curva PR ao placar (PC-0.1 do plan-progressive-confirmation). Ausente quando a regra não tem medida ou o finding é suspected: não medido não vira número. Não é severidade."
    },
    "tier": {
      "enum": ["confirmed", "suspected"],
      "description": "Camada do veredito. confirmed: fluxo provado (entra na precisão, assinável). suspected: candidato de baixa confiança recuperado sem tocar a precisão do confirmed (triado pelo agente)."
    },
    "evidence_level": {
      "enum": ["structural", "observed-at-runtime", "proven-by-oracle"],
      "description": "F2 — força de evidência DERIVADA deste finding, na escala do documento de arquitetura. `structural` (E1): o motor provou o caminho no código (taint-path, match de regra) — o piso de tudo que o codafort emite, já que E0 (asserção sem artefato) não tem caminho de existência. `observed-at-runtime` (E2): outra modalidade OBSERVOU este finding sendo alcançado em execução (`confirmed-at-runtime` nos `verdicts` de uma contribuição coda-evidence/1). AUSENTE quando nenhuma evidência foi fornecida — 'não medido' não vira rótulo. Positivo-só: apenas `confirmed-at-runtime` SOBE o nível; `unreached` é 'não medido' e `sanitized-at-runtime` vale para o tráfego que houve — nenhum dos dois rebaixa. `proven-by-oracle` (E3): o oráculo do codaprobe provou ESTE finding — o scan dirigido (`--plan-from`) leva o id do achado que o planejou, e o veredito `dynamically_confirmed`/`exploit_demonstrated` volta por ele (PC-1.3 do plan-progressive-confirmation)."
    },
    "cwe": {
      "type": "array",
      "items": { "type": "integer", "minimum": 1 },
      "description": "CWE IDs numéricos (sem o prefixo CWE-)."
    },
    "owasp": {
      "type": "array",
      "items": { "type": "string" },
      "description": "Categorias OWASP Top 10 (ex.: A03:2021)."
    },
    "message": {
      "type": "string",
      "minLength": 1,
      "description": "Explicação humana do finding — nenhum finding sem explicação (PRD §5.4)."
    },
    "location": {
      "type": "object",
      "additionalProperties": false,
      "required": ["file"],
      "properties": {
        "file": {
          "type": "string",
          "minLength": 1,
          "description": "src: caminho do arquivo no repo; run: módulo/binário onde a falha ocorreu (ex.: app.exe)."
        },
        "line": { "type": ["integer", "null"], "minimum": 1 },
        "column": { "type": ["integer", "null"], "minimum": 0 },
        "end_column": { "type": ["integer", "null"], "minimum": 0 },
        "symbol": {
          "type": ["string", "null"],
          "description": "run: símbolo+offset resolvido (ex.: app.exe+0x1020)."
        }
      }
    },
    "dataflow_path": {
      "type": ["array", "null"],
      "description": "src: passos source→propagação→sink (codeFlow); run: frames do stack walk, topo primeiro. null quando não há caminho.",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["message"],
        "properties": {
          "file": { "type": ["string", "null"] },
          "line": { "type": ["integer", "null"], "minimum": 1 },
          "message": { "type": "string" }
        }
      }
    },
    "evidence": {
      "type": ["string", "null"],
      "description": "src: trecho de código da linha do finding; run: instrução da falha + classificação do endereço (ex.: write @ ponteiro NULO)."
    },
    "fix": {
      "type": ["string", "null"],
      "description": "Patch determinístico do engine (texto de substituição para o intervalo [column, end_column)). null = sem fix — recusa honesta; nunca inventar patch fora do engine."
    },
    "provenance": {
      "type": "object",
      "additionalProperties": false,
      "required": ["tool"],
      "properties": {
        "tool": {
          "type": "object",
          "additionalProperties": false,
          "required": ["name", "version"],
          "properties": {
            "name": { "type": "string", "minLength": 1 },
            "version": { "type": "string", "minLength": 1 }
          }
        },
        "commit_sha": { "type": ["string", "null"] },
        "branch": { "type": ["string", "null"] },
        "dirty": { "type": ["boolean", "null"] },
        "config_hash": {
          "type": ["string", "null"],
          "description": "SHA-256 dos configs .codafort-*.yaml presentes (formato sha256:<hex>)."
        },
        "artifact": {
          "type": ["string", "null"],
          "description": "run: o dump/binário analisado (ex.: crash-1234.dmp)."
        }
      }
    }
  }
}
