{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://josh.dev/spec/_schema.json",
  "title": "Josh Foundation spec item",
  "description": "Source-of-truth schema for a single spec item under docs/spec/data/<id>.yaml. Renderer (bin/build-spec.py) and in-browser editor (docs/_assets/spec.js) both consume this. Agents should also consume this — it's the contract for what a 'spec' means in Josh.",
  "type": "object",
  "additionalProperties": false,
  "required": ["id", "title", "category", "status", "why", "acceptance_criteria", "success_determiner"],
  "definitions": {
    "compareOp": {
      "type": "object",
      "additionalProperties": false,
      "required": ["op", "value"],
      "description": "A single assertion on a query output column. `op` selects the comparison; `value` is the threshold (numeric ops), substring (contains), regex (matches), or peer column index (eq_col).",
      "properties": {
        "op": {
          "type": "string",
          "enum": ["eq", "ne", "lt", "lte", "gt", "gte", "contains", "matches", "eq_col"],
          "description": "eq/ne/lt/lte/gt/gte = numeric or string comparison | contains = substring | matches = regex search | eq_col = equality against another column (value is that column's index, 0-based)"
        },
        "value": {
          "description": "Threshold value. For numeric ops: number. For contains/matches: string. For eq_col: integer column index."
        }
      }
    }
  },
  "properties": {
    "id": {
      "type": "string",
      "pattern": "^[a-z][a-z0-9-]*[a-z0-9]$",
      "description": "kebab-case, must equal the filename stem"
    },
    "title": {
      "type": "string",
      "minLength": 3,
      "maxLength": 120,
      "description": "Human-friendly, sentence-case (no trailing period)"
    },
    "category": {
      "type": "string",
      "enum": ["substrate", "source", "surface", "launch"],
      "description": "substrate = cross-cutting machinery; source = per-data-source ingester; surface = REST/MCP/CLI/UI; launch = OSS launch artifacts"
    },
    "status": {
      "type": "string",
      "enum": ["draft", "planned", "in_progress", "blocked", "verified", "shipped"],
      "description": "draft = idea, not committed | planned = scoped, not started | in_progress = active | blocked = waiting on a dependency | verified = success_determiner ran green | shipped = live (only after verified)"
    },
    "priority": {
      "type": "string",
      "enum": ["p0", "p1", "p2"],
      "description": "p0 = launch-blocking | p1 = launch-desired | p2 = post-launch"
    },
    "owner": {
      "type": ["string", "null"],
      "description": "Optional. Free-text handle of the person on the hook."
    },
    "updated_at": {
      "type": "string",
      "format": "date-time",
      "description": "ISO-8601 timestamp — auto-bumped on every save"
    },
    "why": {
      "type": "string",
      "minLength": 10,
      "description": "1–3 sentences. The deliverable + intent. Anthropic: 'ambitious deliverables, not steps.'"
    },
    "user_stories": {
      "type": "array",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["as", "i_want", "so_that"],
        "properties": {
          "as":      { "type": "string", "minLength": 2, "description": "actor, e.g. 'a policy analyst using my own AI agent'" },
          "i_want":  { "type": "string", "minLength": 2, "description": "capability, e.g. 'to query the latest Federal Register documents'" },
          "so_that": { "type": "string", "minLength": 2, "description": "outcome, e.g. 'I get fresh data without running my own scraper'" }
        }
      }
    },
    "acceptance_criteria": {
      "type": "array",
      "minItems": 1,
      "description": "EARS-form sentences. Each item should start with one of: 'When ...', 'While ...', 'Where ...', 'If ... then ...', or be ubiquitous ('The system shall ...').",
      "items": {
        "type": "string",
        "minLength": 8
      }
    },
    "success_determiner": {
      "type": "object",
      "description": "TAGGED UNION — the load-bearing field. Never free prose. An agent runs this to decide if the spec is met.",
      "oneOf": [
        {
          "additionalProperties": false,
          "required": ["kind", "command", "expect"],
          "properties": {
            "kind": { "const": "bash" },
            "command": { "type": "string", "minLength": 2, "description": "Shell command(s). Multi-line OK. Should exit 0 on success. This is the human/CI smoke (e.g. remote ingest + assert)." },
            "expect":  { "type": "string", "description": "What the command should produce — substring, count, or '>= N' style assertion." },
            "probe": {
              "type": "array",
              "minItems": 1,
              "description": "Target-agnostic machine-checkable contract: the SQL assertions the `command` smoke-tests, lifted out so they can run against a local snapshot (CI) or production (remote) identically. `bin/test-determiners.py` mutation-tests these; a routine trusts their combined green/red/CNV. Each query MUST return its assertion on the FIRST output line — boolean-ize multi-line schema text with `SELECT (sql LIKE '%x%' AND sql LIKE '%y%') FROM sqlite_master WHERE name='t'` (LIKE treats `[` literally; use exact `name=` to isolate a table from its shadow tables).",
              "items": {
                "type": "object",
                "additionalProperties": false,
                "required": ["query", "compare"],
                "properties": {
                  "query":   { "type": "string", "minLength": 5 },
                  "compare": {
                    "description": "Assertion(s) against the first row of this probe's query. Single object compares against column 0; array compares positionally column-by-column. Same shape as a sql-kind determiner's `compare`.",
                    "oneOf": [
                      { "$ref": "#/definitions/compareOp" },
                      { "type": "array", "minItems": 1, "items": { "$ref": "#/definitions/compareOp" } }
                    ]
                  },
                  "label":   { "type": "string", "description": "Short human label for the assertion — shown in dry-run / mutation-test output." }
                }
              }
            },
            "notes":   { "type": "string" }
          }
        },
        {
          "additionalProperties": false,
          "required": ["kind", "db", "query", "expect"],
          "properties": {
            "kind":  { "const": "sql" },
            "db":    { "type": "string", "description": "Target db identifier — typically 'substrate' (the SQLite file)." },
            "query": { "type": "string", "minLength": 5 },
            "expect":{ "type": "string", "description": "Human-readable description of what should hold. Use `compare:` for the machine-checked assertion." },
            "compare": {
              "description": "Machine-checked assertion against the first row of query output. Single object compares against column 0; array compares positionally column-by-column. Without `compare:`, the harness runs the query but cannot verify the result — gameable. Required for routine-eligible specs.",
              "oneOf": [
                { "$ref": "#/definitions/compareOp" },
                { "type": "array", "minItems": 1, "items": { "$ref": "#/definitions/compareOp" } }
              ]
            },
            "notes": { "type": "string" }
          }
        },
        {
          "additionalProperties": false,
          "required": ["kind", "path", "runner"],
          "properties": {
            "kind":   { "const": "test_file" },
            "path":   { "type": "string", "description": "Path to test file relative to repo root, e.g. 'josh-ingester/tests/test_federal_register.py'." },
            "runner": { "type": "string", "description": "How to run, e.g. 'pytest', 'kamal app exec --reuse \"pytest tests/...\"'." },
            "notes":  { "type": "string" }
          }
        },
        {
          "additionalProperties": false,
          "required": ["kind", "source", "expect"],
          "properties": {
            "kind":   { "const": "expected_output" },
            "source": { "type": "string", "description": "Where the output comes from — URL, file path, command stdout, etc." },
            "expect": { "type": "string", "description": "Substring or full expected output." },
            "notes":  { "type": "string" }
          }
        },
        {
          "additionalProperties": false,
          "required": ["kind", "checklist"],
          "properties": {
            "kind":      { "const": "manual" },
            "checklist": {
              "type": "array",
              "minItems": 1,
              "items": { "type": "string", "minLength": 4 },
              "description": "Items a human (or agent with computer-use) must visually confirm. Use sparingly — prefer mechanical kinds."
            },
            "notes":     { "type": "string" }
          }
        }
      ]
    },
    "routine_eligible": {
      "type": "boolean",
      "description": "If true, a scheduled self-prompting routine is trusted to act on this determiner's green/red/CNV signal without a human in the loop. Gated (see substrate-determiner-verification): the determiner must be mechanically verifiable (sql with `compare`, or bash with a `probe:`) AND have a mutation suite at docs/spec/mutations/<id>.yaml that `poe verify-determiner-suite` proves drives it red on each expect:fail mutation. Absent → false: a human reads the result. `spec-lint --strict` enforces the structural precondition; the suite enforces the behavioral one."
    },
    "clarifications_needed": {
      "type": "array",
      "description": "Open questions. Agent should surface these instead of guessing. Pattern from GitHub Spec Kit.",
      "items": { "type": "string", "minLength": 4 }
    },
    "out_of_scope": {
      "type": "array",
      "description": "What this spec explicitly does NOT cover — prevents agent from inventing scope.",
      "items": { "type": "string", "minLength": 4 }
    },
    "dependencies": {
      "type": "array",
      "description": "Other spec ids this depends on, OR file paths (migrations, modules) that must exist first.",
      "items": { "type": "string", "minLength": 2 }
    },
    "plan": {
      "type": "string",
      "description": "Architecture decisions, in Markdown. Free-form, but agent-readable. Cite source files."
    },
    "tasks": {
      "type": "array",
      "description": "Internal checklist. Each task can flip independently. Agents update these as they make progress.",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["id", "done", "text"],
        "properties": {
          "id":   { "type": "string", "pattern": "^t[0-9]+$" },
          "done": { "type": "boolean" },
          "text": { "type": "string", "minLength": 3 }
        }
      }
    },
    "changelog": {
      "type": "array",
      "description": "Append-only history of status transitions. Auto-prepended on save when status changes.",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["at", "from", "to"],
        "properties": {
          "at":     { "type": "string", "format": "date-time" },
          "by":     { "type": "string", "description": "Free-text actor handle." },
          "from":   { "type": "string", "enum": ["draft", "planned", "in_progress", "blocked", "verified", "shipped", "(new)"] },
          "to":     { "type": "string", "enum": ["draft", "planned", "in_progress", "blocked", "verified", "shipped"] },
          "note":   { "type": "string", "description": "Why this transition. Free-text Markdown. Optional but encouraged." },
          "commit": { "type": "string", "description": "Optional git short hash." }
        }
      }
    }
  }
}
