{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://skill-map.ai/spec/v1/execution-record.schema.json",
  "title": "ExecutionRecord",
  "description": "A single row in the execution history (`state_executions`). One record per extension execution (Action or finder Analyzer), whether an external agent processed the job (agent) or the extension ran in-process.",
  "type": "object",
  "required": ["id", "kind", "extensionId", "extensionVersion", "status", "startedAt", "finishedAt"],
  "additionalProperties": false,
  "properties": {
    "id": {
      "type": "string",
      "description": "Unique execution id. Format: `e-YYYYMMDD-HHMMSS-XXXX`."
    },
    "kind": {
      "type": "string",
      "enum": ["action"],
      "description": "Which extension kind was executed. Single-valued in v1: every recorded execution is stamped `action`, including the run of a probabilistic finder Analyzer processed through the queue (the queue records the EXECUTION channel, not the extension kind; `state_jobs.extension_kind` is where the kind travels). Matches the `state_executions.kind` CHECK constraint in `db-schema.md`. The audit kind was removed pre-1.0; widening this enum later is an additive change."
    },
    "extensionId": {
      "type": "string",
      "description": "Id of the extension that ran (e.g. `ai-summarizer-action`)."
    },
    "extensionVersion": {
      "type": "string",
      "description": "Semver of the extension implementation at execution time."
    },
    "nodeIds": {
      "type": "array",
      "description": "Target `node.path` values. Empty for whole-graph actions.",
      "items": { "type": "string" }
    },
    "contentHash": {
      "type": ["string", "null"],
      "pattern": "^[a-f0-9]{64}$",
      "description": "Duplicate-prevention hash used at submit time: sha256 over the NUL-joined (0x00) tuple (extensionId, extensionVersion, node.path, bodyHash, frontmatterHash, promptTemplateHash). May be null for in-process actions that don't memoize."
    },
    "status": {
      "type": "string",
      "enum": ["completed", "failed", "cancelled"],
      "description": "Terminal status. History never stores `queued` or `running`, those live in `state_jobs` until they reach a terminal state."
    },
    "failureReason": {
      "type": ["string", "null"],
      "enum": ["runner-error", "report-invalid", "timeout", "abandoned", "job-file-missing", "user-failed", null],
      "description": "Normalized reason when `status = failed`. Null for `completed` and `cancelled` (both self-explanatory), mirroring `Job.failureReason`. `user-failed` records an operator-forced failure (`sm jobs fail`); a `cancelled` status is the operator's `sm jobs cancel` and carries no reason. `job-file-missing` mirrors the same enum value on `Job.failureReason` (legacy name preserved across the disk-to-DB shift)."
    },
    "exitCode": {
      "type": ["integer", "null"],
      "description": "Exit code the recording agent reported for its execution, when it reports one. Null for in-process executions or when the agent reports none."
    },
    "runner": {
      "type": ["string", "null"],
      "enum": ["agent", "in-process", null],
      "description": "Which execution path produced this record: an external agent via claim/record, or in-process deterministic code. Null when the action runs synchronously without dispatching."
    },
    "startedAt": {
      "type": "integer",
      "description": "Unix milliseconds."
    },
    "finishedAt": {
      "type": "integer",
      "description": "Unix milliseconds."
    },
    "durationMs": {
      "type": ["integer", "null"],
      "minimum": 0,
      "description": "Finished - started. Stored denormalized for query speed."
    },
    "tokensIn": {
      "type": ["integer", "null"],
      "minimum": 0,
      "description": "Input tokens reported by the recording agent. Null if not applicable or not reported."
    },
    "tokensOut": {
      "type": ["integer", "null"],
      "minimum": 0,
      "description": "Output tokens reported by the runner."
    },
    "model": {
      "type": ["string", "null"],
      "description": "Executing model's name as SELF-REPORTED by the recording agent (`sm record --model <name>`). Unverifiable by design, like the token counts. Null when the agent does not declare one, and for in-process deterministic executions. Denormalized onto `state_findings.model` / `state_summaries.model` at record time."
    },
    "reportPath": {
      "type": ["string", "null"],
      "description": "Legacy field preserved across the disk-to-DB shift. Under B2 (DB-only job artifacts) this is always null because reports live inline in `state_executions.report_json`. Phase A of Step 10 will rename to `report` (object/null) carrying the parsed payload; until then the field remains as documented for backward compat."
    },
    "jobId": {
      "type": ["string", "null"],
      "description": "Originating job id when applicable. Null for in-process actions."
    }
  }
}
