{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "urn:cede:schema:job:v0",
  "title": "CEDE job resource, v0",
  "description": "The one uniform job resource SPEC.md section 3 requires of every verb: POST creates a job, GET /jobs/{id} polls it, and the result embeds or links the produced objects. One shape for all seven verbs, so a client that can poll Ingest can poll Price without learning a second protocol. A job describes work the platform did on data; it asserts nothing about a transaction, and there is no terminal state that means anything was executed (SPEC.md section 4).",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "schema_version",
    "id",
    "verb",
    "status",
    "created_at",
    "updated_at",
    "links"
  ],
  "properties": {
    "schema_version": {
      "type": "string",
      "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$",
      "description": "Version of this job schema the payload conforms to."
    },
    "id": {
      "$ref": "#/$defs/uuid",
      "description": "Server-assigned identity of the job."
    },
    "verb": {
      "type": "string",
      "enum": ["ingest", "analyse", "price", "backtest", "package", "model_run"],
      "description": "What this job runs: one of the seven verbs (SPEC.md section 3), or model_run, which runs a registry model composed in the builder (SPEC.md section 2A). The list grows by schema version bump as the remaining verbs land — package joined it at 0.2.0 with SPEC.md section 3.7, analyse at 0.3.0 with SPEC.md section 3.2 — so a job can never name work the published schema has not documented."
    },
    "status": {
      "type": "string",
      "enum": ["queued", "running", "succeeded", "failed"],
      "description": "Lifecycle of the job. queued and running are non-terminal; succeeded and failed are terminal and never change afterwards."
    },
    "created_at": { "$ref": "#/$defs/timestamp" },
    "updated_at": { "$ref": "#/$defs/timestamp" },
    "links": {
      "type": "object",
      "additionalProperties": false,
      "required": ["self"],
      "properties": {
        "self": {
          "type": "string",
          "minLength": 1,
          "description": "Path at which this job is polled."
        }
      }
    },
    "subject": {
      "type": "object",
      "additionalProperties": false,
      "description": "What the job was asked to work on: the submitted file for Ingest, the object and the named analyses for Analyse, the object for Price, the structure and any narrowed window for Backtest. Present from creation, so a client that has only the job id can still say what it submitted.",
      "properties": {
        "object_id": { "$ref": "#/$defs/uuid" },
        "structure_id": { "$ref": "#/$defs/uuid" },
        "analyses": {
          "type": "array",
          "minItems": 1,
          "description": "The named analyses an analyse job was asked to run over the object (SPEC.md section 3.2), as the caller named them. Echoed from creation so a client holding only the job id can say what it asked for; an unknown name never reaches a job, because the route refuses it with the valid set in the message.",
          "items": { "type": "string", "minLength": 1 }
        },
        "model_id": {
          "type": "string",
          "minLength": 1,
          "description": "The model a model_run job runs (SPEC.md section 2A)."
        },
        "window": {
          "type": "object",
          "additionalProperties": false,
          "description": "The historical window a Backtest job was asked for, as the caller gave it (SPEC.md section 3.5). Absent when the caller asked for the archive's full period of record, which is the default.",
          "properties": {
            "from_year": { "type": "integer" },
            "to_year": { "type": "integer" }
          }
        },
        "source": { "$ref": "#/$defs/source_file" },
        "declared_currency": {
          "type": "string",
          "pattern": "^[A-Z]{3}$",
          "description": "The ISO 4217 code the ingest request DECLARED for a file that states no currency of its own (SPEC.md section 2, exposure.currency). A statement by the submitter, never a reading of the file: Ingest infers no currency from an address or a country column, so this is the only way an untagged schedule's amounts acquire one. It sits on the subject rather than inside source because source is what is known about the submitted bytes and this is what the caller said about them. Absent when the caller declared nothing, which is every ingest that does not need it. A code the file itself contradicts never reaches a job that succeeds: the job fails with 422 currency_conflict. Added at 0.4.0 with CEDE-229."
        }
      }
    },
    "result": { "$ref": "#/$defs/result" },
    "error": { "$ref": "#/$defs/error" }
  },
  "allOf": [
    {
      "description": "A succeeded job has a result and no error.",
      "if": { "properties": { "status": { "const": "succeeded" } }, "required": ["status"] },
      "then": { "required": ["result"], "not": { "required": ["error"] } }
    },
    {
      "description": "A failed job has an error and no result. The error is the same shape as the error response body, so a failure reads the same whether it arrived synchronously or by polling.",
      "if": { "properties": { "status": { "const": "failed" } }, "required": ["status"] },
      "then": { "required": ["error"], "not": { "required": ["result"] } }
    },
    {
      "description": "A job that has not finished has neither. An absent result must never be mistaken for an empty one.",
      "if": {
        "properties": { "status": { "enum": ["queued", "running"] } },
        "required": ["status"]
      },
      "then": {
        "allOf": [
          { "not": { "required": ["result"] } },
          { "not": { "required": ["error"] } }
        ]
      }
    }
  ],
  "$defs": {
    "uuid": {
      "type": "string",
      "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
      "description": "A lowercase UUID, as assigned by the server."
    },
    "timestamp": {
      "type": "string",
      "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}([.][0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$",
      "description": "RFC 3339 date-time with an explicit UTC offset, as on the risk object."
    },
    "source_file": {
      "type": "object",
      "additionalProperties": false,
      "required": ["name", "sha256", "bytes"],
      "description": "The submitted file, identified by content. The digest is what makes an ingest replayable: the same bytes under any name produce the same canonical object.",
      "properties": {
        "name": { "type": "string", "minLength": 1 },
        "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
        "bytes": { "type": "integer", "minimum": 0 },
        "media_type": { "type": "string", "minLength": 1, "description": "The media type the submission DECLARED, checked against what Ingest reads before the job was created — never a finding about the bytes. Absent when the caller declared nothing (no part header) or declared 'application/octet-stream', which states that the type is unknown rather than stating a type. What the reader actually read the file as is on the produced object, at provenance.source_files[].media_type." }
      }
    },
    "object_link": {
      "type": "object",
      "additionalProperties": false,
      "required": ["id", "links"],
      "description": "A produced canonical risk object, linked rather than embedded: SPEC.md section 3 allows either, and a link keeps a job that produced two hundred objects from being a two hundred object response.",
      "properties": {
        "id": { "$ref": "#/$defs/uuid" },
        "links": {
          "type": "object",
          "additionalProperties": false,
          "required": ["self"],
          "properties": {
            "self": { "type": "string", "minLength": 1 }
          }
        }
      }
    },
    "result": {
      "type": "object",
      "additionalProperties": false,
      "required": ["objects"],
      "description": "What the job produced. objects is always present and always a list, empty included, so a client never has to distinguish 'no objects' from 'field missing'. price is present exactly when the verb is price, and embeds the full price response document (urn:cede:schema:price-response:v0) rather than linking it, because a technical price is meaningless apart from the assumption set travelling with it (SPEC.md section 3.3). backtest is present exactly when the verb is backtest and embeds the artifact (urn:cede:schema:backtest-response:v0) for the same reason: forty years of payouts read apart from their data vintages and coverage disclosure are forty numbers nobody can check. model_run is present exactly when the verb is model_run and embeds the full model run result (urn:cede:schema:model-run:v0) for the same reason again, and analysis is present exactly when the verb is analyse and embeds the run's artifacts (urn:cede:schema:analysis:v0), each with the assumptions and data vintages SPEC.md section 3.2 requires on every artifact.",
      "properties": {
        "objects": {
          "type": "array",
          "items": { "$ref": "#/$defs/object_link" }
        },
        "price": {
          "type": "object",
          "description": "A price response document. Validated in full against urn:cede:schema:price-response:v0 by the contract oracle; restated loosely here so that the two schemas cannot drift into disagreeing about the same bytes."
        },
        "analysis": {
          "type": "object",
          "description": "An analyse run: the named analyses that ran over the object and the artifacts they produced. Present exactly when the verb is analyse, and embedded rather than linked for the same reason a price is — an artifact read apart from its assumptions and data vintages is a number nobody can check. Validated in full against urn:cede:schema:analysis:v0 by the contract oracle; restated loosely here so the two schemas cannot drift."
        },
        "backtest": {
          "type": "object",
          "description": "A backtest artifact. Validated in full against urn:cede:schema:backtest-response:v0 by the contract oracle; restated loosely here for the same reason as price."
        },
        "package": {
          "type": "object",
          "description": "A submission pack. Present exactly when the verb is package, and embedded rather than linked for the same reason a price is: the pack carries the watermark, the limitations and the disclosure that say what its exhibits are, and a client that read the identifier without them would hold the numbers and none of the labels. Validated in full against urn:cede:schema:package:v0 by the contract oracle; restated loosely here so the two schemas cannot drift."
        },
        "model_run": {
          "type": "object",
          "description": "A model run result. Present exactly when the verb is model_run, and embedded rather than linked for the same reason a price is: an index level and a technical price are meaningless apart from the assumption set travelling with them. Validated in full against urn:cede:schema:model-run:v0 by the contract oracle; restated loosely here so the two schemas cannot drift."
        },
        "diagnostics": {
          "type": "object",
          "additionalProperties": false,
          "description": "Per-run diagnostics. Ingest's honest record of what it could not confidently normalise lives on each produced object, in exposure.source_fidelity; this is the run-level summary that points at it.",
          "properties": {
            "objects_produced": { "type": "integer", "minimum": 0 },
            "ingest_run_id": { "type": "string", "minLength": 1 },
            "notes": {
              "type": "array",
              "items": { "type": "string", "minLength": 1 }
            }
          }
        }
      }
    },
    "error": {
      "type": "object",
      "additionalProperties": false,
      "required": ["status", "code", "message"],
      "properties": {
        "status": { "type": "integer", "minimum": 400, "maximum": 599 },
        "code": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" },
        "message": { "type": "string", "minLength": 1 },
        "details": {
          "type": "array",
          "items": { "type": "string", "minLength": 1 }
        }
      }
    }
  }
}
