schemas/v1/report.schema.json

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://raw.githubusercontent.com/Fortigi/PSMutant/main/schemas/v1/report.schema.json",
  "title": "PSMutant report",
  "description": "The report JSON written by Invoke-PSMutation. Together with the object the command returns, this file is the module's entire public contract. Two shapes share it: a full run, and the partial run produced by -RecheckFrom, which is identified by a 'mode' of 'Recheck' and deliberately carries NO score. Additional properties are permitted on purpose: schemaVersion changes when a field changes meaning or disappears, never when one is added, so a reader validating against this schema keeps working when a later version records more.",
  "type": "object",
  "required": [
    "generatedFrom",
    "schemaVersion",
    "producedBy",
    "generatedAt",
    "durations",
    "operators",
    "sourceHashes",
    "survivors",
    "mutants"
  ],
  "properties": {
    "generatedFrom": {
      "const": "PSMutant",
      "description": "Marks the file as this tool's output."
    },
    "schemaVersion": {
      "type": "integer",
      "minimum": 1,
      "description": "The format version. Branch on this rather than sniffing for keys. It changes when a field changes MEANING or disappears; adding a field does not change it."
    },
    "producedBy": {
      "type": "object",
      "required": ["module", "version"],
      "properties": {
        "module": { "type": "string" },
        "version": { "type": "string", "description": "The ModuleVersion that produced the report." }
      }
    },
    "generatedAt": {
      "type": "string",
      "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$",
      "description": "UTC, ISO-8601, second precision. A STRING in the file: PowerShell's ConvertFrom-Json recognises the format and hands a caller a [datetime] instead, so a PowerShell reader never sees this text. The file is the contract; other languages read a string."
    },
    "durations": {
      "type": "object",
      "required": ["baselineSeconds", "totalSeconds", "perMutantTimeoutSeconds"],
      "properties": {
        "baselineSeconds": { "type": "number", "description": "How long the unmutated suite took." },
        "totalSeconds": { "type": "number", "description": "The whole run, measured after the last mutant." },
        "perMutantTimeoutSeconds": {
          "type": ["number", "null"],
          "description": "The deadline each mutant was given, recorded beside the baseline it was derived from -- which is what makes comparing them mean anything. A timeout expiry is scored as a kill, so a suite drifting toward this bound inflates the score before anything visibly breaks."
        }
      }
    },
    "operators": {
      "type": "array",
      "items": { "type": "string" },
      "description": "The operator set, SORTED. Recorded because mutant ids are AST-walk positions: they only refer to the same code for an identical source and an identical operator set."
    },
    "sourceHashes": {
      "type": ["object", "null"],
      "additionalProperties": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
      "description": "SHA256 per mutated file, lowercase hex. With 'operators', this is what lets a later -RecheckFrom prove the ids in this report still mean what they meant."
    },
    "survivors": {
      "type": "array",
      "items": { "$ref": "#/definitions/mutant" },
      "description": "The mutants no test killed. A subset of 'mutants', repeated here because it is the working list."
    },
    "mutants": {
      "type": "array",
      "items": { "$ref": "#/definitions/mutant" },
      "description": "Every mutant evaluated in this run."
    },

    "mutationScore": {
      "type": "number",
      "description": "Full runs only. Percentage of killed over total, where total EXCLUDES declared equivalents."
    },
    "total": { "type": "integer", "description": "Full runs only. Mutants counted in the score, excluding declared equivalents." },
    "killed": { "type": "integer", "description": "Full runs only." },
    "survived": { "type": "integer", "description": "Full runs only." },
    "declaredEquivalent": {
      "type": "integer",
      "description": "Full runs only. How many mutants the config argued cannot change behaviour, and which are therefore absent from 'total'. A reader who cannot see this number cannot tell a real 100% from a declared one."
    },
    "staleEquivalents": {
      "type": "array",
      "items": { "type": "string" },
      "description": "Full runs only. Equivalence declarations that are no longer true or no longer identify exactly one mutant. Non-empty means the run failed on them."
    },
    "thresholds": {
      "type": ["object", "null"],
      "description": "Full runs only. The gate as configured; null when none was set, which means report-only.",
      "properties": {
        "high": { "type": ["number", "null"] },
        "low": { "type": ["number", "null"] },
        "break": { "type": ["number", "null"], "description": "Unset means report-only: the run does not fail on the score." }
      }
    },

    "mode": {
      "const": "Recheck",
      "description": "Present only on a partial run. Its absence is what identifies a full report."
    },
    "note": { "type": "string", "description": "Recheck only. The caveat, restated in the artifact so a partial run cannot be read as a measurement." },
    "recheckedFrom": { "type": ["string", "null"], "description": "Recheck only. The report this run took its survivors from." },
    "priorSurvivors": { "type": "integer", "description": "Recheck only. How many survivors the source report held." },
    "rechecked": { "type": "integer", "description": "Recheck only. How many were actually re-run: fewer than priorSurvivors, because declared equivalents are skipped." },
    "nowKilled": { "type": "integer", "description": "Recheck only. How many of them a test now kills." }
  },
  "allOf": [
    {
      "description": "A recheck carries counts and never a score. Forbidding 'mutationScore' is the point rather than a formality: a partial number quoted as a real one is the failure this tool exists to prevent, so the format makes it impossible to report one by accident. Written as a boolean-false property schema rather than as \"not\": PowerShell's Test-Json silently IGNORES the \"not\" keyword, so the obvious spelling of this rule is a clause that can never fail.",
      "if": { "required": ["mode"] },
      "then": {
        "required": ["mode", "note", "priorSurvivors", "rechecked", "nowKilled"],
        "properties": { "mutationScore": false }
      },
      "else": {
        "required": [
          "mutationScore",
          "total",
          "killed",
          "survived",
          "declaredEquivalent",
          "staleEquivalents",
          "thresholds"
        ]
      }
    }
  ],
  "definitions": {
    "mutant": {
      "type": "object",
      "description": "One evaluated mutant. Note that 'Id' is NOT contractual: it is a walk position, it has changed before, and its only consumer is this module's own -RecheckFrom.",
      "required": ["Id", "Function", "File", "Line", "Operator", "Description", "Status"],
      "properties": {
        "Id": { "type": "integer" },
        "Function": {
          "type": ["string", "null"],
          "description": "The enclosing function, or empty for code at file scope. Carried so an equivalence declaration can be keyed by function rather than by a line number, which moves whenever anything above it is edited."
        },
        "File": { "type": "string", "description": "Repo-relative path to the mutated source." },
        "Line": { "type": "integer" },
        "Operator": { "type": "string" },
        "Description": {
          "type": "string",
          "description": "<original> -> <mutated>, whitespace-collapsed and truncated. Derived from the source, never written by the operator."
        },
        "Status": {
          "type": "string",
          "enum": ["Killed", "Survived"],
          "description": "Killed means at least one covering test failed against the mutant. A mutant whose run timed out is counted Killed."
        }
      }
    }
  }
}