{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://lexlint.io/schema/lexlint.schema.json",
  "title": "lexlint.yml",
  "description": "The committed LexLint manifest. You declare what your app does and where it operates; the lint writes its findings underneath. Manifest-first, not detection-first.",
  "type": "object",
  "required": ["version", "app", "profile"],
  "additionalProperties": false,
  "properties": {
    "version": { "type": "integer", "enum": [1] },
    "app": {
      "type": "object",
      "required": ["name"],
      "additionalProperties": false,
      "properties": {
        "name": { "type": "string", "minLength": 1 },
        "repo": { "type": "string", "format": "uri" },
        "scope": {
          "type": "object",
          "description": "Which files the lint reads. Absent means the whole repository. Scope narrows the read, never the declaration: a scoped subtree still operates in every jurisdiction profile.jurisdictions names, and draws the same findings. A subtree that is really a different app is a second manifest with its own app.name, not a scope.",
          "additionalProperties": false,
          "minProperties": 1,
          "properties": {
            "include": {
              "type": "array",
              "minItems": 1,
              "uniqueItems": true,
              "description": "Read only these paths. A trailing \"/\" is a directory and everything under it; anything else is a glob. An include that matches no file is an error to stop on, never a silent fallback to the whole repository, which would read everything while reporting that it read one directory.",
              "items": { "$ref": "#/$defs/scopePath" }
            },
            "exclude": {
              "type": "array",
              "minItems": 1,
              "uniqueItems": true,
              "description": "Subtract these paths, with or without an include. Same path rules.",
              "items": { "$ref": "#/$defs/scopePath" }
            }
          }
        }
      }
    },
    "profile": {
      "type": "object",
      "description": "Your declaration. LexLint never infers this from your code.",
      "required": ["activities", "jurisdictions"],
      "additionalProperties": false,
      "properties": {
        "activities": {
          "type": "array",
          "minItems": 1,
          "uniqueItems": true,
          "items": {
            "type": "string",
            "enum": ["crawls_web", "trains_models", "generates_content",
                     "deploys_chatbot", "processes_voice", "processes_biometrics",
                     "automated_outreach", "high_risk_decisions",
                     "publishes_adult_content", "operates_social_platform",
                     "serves_minors", "operates_app_store", "ships_mobile_app",
                     "aggregates_content", "distributes_software_product",
                     "handles_health_records", "provides_financial_services",
                     "operates_essential_service", "is_listed_company",
                     "provides_telecom_services", "statutory_requests",
                     "records_conversations", "tracks_devices"]
          }
        },
        "jurisdictions": {
          "type": "array",
          "minItems": 1,
          "uniqueItems": true,
          "description": "Jurisdictions, each a slug (lowercase, \"/\"-separated), an ISO 3166 code (\"US-CA\", \"FR\") or an Atlas ID (\"urn:ungovr:us/ca\"). The profile the server writes always holds the slug. Declare every jurisdiction you operate in: an undeclared one is simply unlinted, and unlinted is not clean.",
          "items": { "type": "string", "pattern": "^(?:[a-z0-9]+(?:/[a-z0-9-]+)*|[A-Za-z]{2}(?:-[A-Za-z0-9]{1,3})?|urn:ungovr:[a-z0-9-]+(?:/[a-z0-9-]+)*)$", "description": "A jurisdiction: a slug (lowercase, \"/\"-separated), an ISO 3166 code (\"US-CA\", \"FR\") or an Atlas ID. The written profile always holds the slug. ALWAYS QUOTE IT: YAML reads a bare no as boolean false, so an unquoted list silently drops Norway. A jurisdiction left off is not passed, it is unlinted, and unlinted is not clean." }
        },
        "domains": {
          "type": "object",
          "description": "Domain to jurisdiction slug, as resolved by resolve_domain_jurisdiction. A null value means unresolved: check the operator's terms page and declare what you find.",
          "additionalProperties": { "type": ["string", "null"] }
        },
        "public_sector": {
          "type": "array",
          "minItems": 1,
          "uniqueItems": true,
          "items": { "type": "string", "enum": ["body", "supplier", "none"] },
          "not": { "allOf": [{ "contains": { "const": "none" } }, { "minItems": 2 }] },
          "description": "Whether a public body runs this software (body), it is sold to or run for public bodies (supplier), both, or neither (none). none is only ever alone. The developer's answer, never read from the code. Absent means never asked."
        },
        "regulated_sector": {
          "type": "object",
          "minProperties": 1,
          "additionalProperties": false,
          "properties": {
            "financial": {
              "type": "string",
              "enum": ["body", "supplier", "none"],
              "description": "Whether a regulated financial entity such as a bank, insurer or payment institution runs this software (body), it is supplied to such entities (supplier), or neither (none). The developer's answer, never read from the code."
            }
          },
          "description": "Whose duty a bound-party role's law is, keyed by sector. Only financial exists, and it is asked only when provides_financial_services is declared. It moves duties between you and your customer and never adds or removes a match. Absent means never asked."
        }
      }
    },
    "lint": {
      "type": "object",
      "description": "Written by the lint, except work_items, which triage writes. Yours to edit: work_items, and state, where, note and handled_by on a finding. A re-run carries all of those across and refreshes the rest.",
      "additionalProperties": false,
      "properties": {
        "run_at": { "type": "string", "format": "date" },
        "tool": { "type": "string" },
        "summary": { "type": "string" },
        "findings": { "type": "array", "items": { "$ref": "#/$defs/finding" } },
        "work_items": {
          "type": "array",
          "description": "Triage output: one entry per thing to actually do, each carrying the findings it answers.",
          "items": { "$ref": "#/$defs/work_item" }
        },
        "vanished": { "type": "array", "description": "Sorted by id. Each entry keeps the four triage fields it had as a finding, because an id that comes back brings its acknowledgment with it: a citation edited upstream and edited back is the same duty, and making the developer acknowledge it twice is the silent deletion this block exists to prevent.", "items": { "$ref": "#/$defs/vanished" } }
      }
    }
  },
  "$defs": {
    "scopePath": {
      "type": "string",
      "minLength": 1,
      "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$)).+$",
      "description": "A path relative to this manifest's own directory. Never absolute, never climbing out with \"..\": a scope that escapes the repository is a manifest error, not a wider lint."
    },
    "finding": {
      "type": "object",
      "description": "One finding as the run returned it, plus the four fields triage writes. Closed on purpose: keeping this property map exhaustive is what makes a field LexLint emits and never documented a build failure rather than a surprise in your manifest. The consequence for an installed bundle is deliberate rather than overlooked. A bundle's schema cannot know a field added to the law library after it shipped, so the lint writes only the keys named here and reports which ones it left out; updating LexLint picks them up on the next run.",
      "required": ["id", "severity", "state"],
      "additionalProperties": false,
      "properties": {
        "id": { "type": "string", "minLength": 1 },
        "severity": {
          "type": "string",
          "enum": ["warn", "info"],
          "description": "warn: a live obligation applies (obligation), a posture finding needs attention (posture), or LexLint lacks current data for a declared jurisdiction (coverage). info: context rather than a live duty, and kind says which it is: an instrument LexLint cannot say is in force today (pending), a jurisdiction-wide statement of how the local law treats crawling as a whole, binding nobody on its own (posture), a note about what was not reported (coverage), a duty your public-sector customer owes (customer_duty), or an instrument whose owed is not yours (obligation). LexLint never reports error: a lint cannot be sure something is prohibited, so it will not say so."
        },
        "kind": {
          "type": "string",
          "enum": ["obligation", "coverage", "posture", "pending", "customer_duty"],
          "description": "obligation: a specific instrument binds the declared profile now. coverage: a note about what was not reported, never a pass. LexLint could not read something, holds no data for a declared jurisdiction, cannot map what it holds to a declared activity, or holds an instrument that no longer binds. posture: a jurisdiction-wide crawl-law attribute, such as whether browsewrap binds or what weight robots.txt carries; cites nothing and binds nobody on its own, and appears only when crawls_web is declared. pending: an instrument LexLint cannot say is in force today, whether proposed, not yet in force (from a later day, or from a date not yet set), blocked by a court, or carrying a status LexLint has no policy for; a duty to watch rather than one to act on. customer_duty: with public_sector or regulated_sector declared, a duty your customer owes, a public body or a financial institution, and meets through your software."
        },
        "jurisdiction": { "type": ["string", "null"] },
        "jurisdiction_name": {
          "type": "string",
          "description": "How the jurisdiction reads to a person, from the engine since bundle 1.20.0: Ireland, European Union, US-California (\"California (US)\" before 1.44.0). Absent when the law library did not name the slug, or when the slug answered from a parent."
        },
        "jurisdiction_flag": {
          "type": "string",
          "description": "The jurisdiction's flag as an emoji, printed before jurisdiction_name, from the engine since bundle 1.43.0: 🇪🇺, 🇨🇦. Present only with jurisdiction_name, and only on a country, the EU or the UN: a state or a city has none."
        },
        "summary": { "type": "string" },
        "detail": {
          "type": "string",
          "description": "The whole research summary the one sentence in summary was cut from, from the engine since bundle 1.20.0. Present on instrument findings that have one."
        },
        "why": {
          "type": "string",
          "description": "Why this instrument was pulled in, in basic language, from the engine since bundle 1.21.0: which declared activity reached it and what the law library tags it for. Shown above the finding wherever it appears."
        },
        "matched_by": {
          "type": "object",
          "description": "The match why was written from, from the engine since bundle 1.21.0.",
          "additionalProperties": false,
          "properties": {
            "activities": { "type": "array", "items": { "type": "string" } },
            "flags": { "type": "array", "items": { "type": "string" } },
            "categories": { "type": "array", "items": { "type": "string" } }
          }
        },
        "posture": {
          "type": "object",
          "description": "Only on kind: posture. The jurisdiction-wide crawl-law attributes this finding summarises, carried verbatim from the law library so the manifest records what was read rather than only what was concluded. crawl_policy and access_legality are open string maps: a new axis added to the law library must land in a manifest without a schema change, because the alternative is a lint that refuses to write its own result.",
          "properties": {
            "crawl_policy": { "type": "object", "additionalProperties": { "type": ["string", "null"] } },
            "access_legality": { "type": "object", "additionalProperties": { "type": ["string", "null"] } }
          },
          "additionalProperties": true
        },
        "basis": {
          "type": ["string", "null"],
          "description": "On kind: posture, the law library value this finding rests on, as field = value, or the researcher's note for an access-matrix verdict. Not a citation: it names the attribute read, not an instrument that binds. On kind: customer_duty, why the duty is your customer's: it binds your public-sector customer and is discharged through the software you supply."
        },
        "citation": { "type": ["string", "null"] },
        "url": { "type": ["string", "null"] },
        "note_url": {
          "type": ["string", "null"],
          "description": "LexLint's own page on this instrument, when it has one: what the citation should be shown as a link to. Distinct from url, which is the official source. Copied from the run and never constructed: a jurisdiction researched before the note pages existed has none, and a guessed URL is a dead link beside a statute."
        },
        "status": { "type": ["string", "null"] },
        "requires": {
          "type": "array",
          "items": { "type": "string" },
          "description": "What the instrument requires of an operator it binds, where the law library states it. Omitted rather than emitted empty: an empty list beside a real obligation reads as \"nothing required\", and the law library states exposure, never clearance."
        },
        "applies_to": {
          "type": ["string", "null"],
          "description": "Whom the instrument binds, as the law library records it. Reported, not filtered on. Whether a government-only duty reaches this caller is the question public_sector answers: with it declared, owed carries the answer, and without it nothing here decides it."
        },
        "owed": {
          "type": "string",
          "enum": ["yours", "customer", "elsewhere"],
          "description": "With public_sector or regulated_sector declared: whose duty this is for this caller. Absent otherwise."
        },
        "requires_elsewhere": {
          "type": "array",
          "items": { "type": "string" },
          "description": "With public_sector or regulated_sector declared: this instrument's lines owed by a party the caller is not. Context, never work."
        },
        "customer_duty_id": {
          "type": "string",
          "description": "The customer_duty finding split from this instrument."
        },
        "instrument_id": {
          "type": "string",
          "description": "On a customer_duty finding: the instrument finding it was split from."
        },
        "effective_date": { "type": ["string", "null"] },
        "lifecycle": {
          "type": "object",
          "description": "What the instrument status and dates mean, read as of the call. Derived, not stored, so its values move without a law library change. For display, use in_force.words; this object's label is kept for existing clients.",
          "properties": {
            "band": { "type": "string", "enum": ["new", "recent", "in_force", "imminent", "proposed", "blocked", "historical", "unknown"] },
            "label": { "type": "string" },
            "tone": { "type": "string", "enum": ["force", "live", "pending", "spent"] },
            "detail": { "type": ["string", "null"] },
            "commencement_established": { "type": "boolean" }
          },
          "required": ["band", "label", "tone", "detail", "commencement_established"],
          "additionalProperties": false
        },
        "in_force": {
          "type": "object",
          "description": "When this law binds, as LexLint's pages say it. Print `words` wherever a law's state is shown. Derived at the call, like lifecycle. Supersedes lifecycle.label for display; lifecycle stays for existing clients.",
          "properties": {
            "state": { "type": "string", "enum": ["in_force", "not_yet_in_force", "blocked", "proposed", "no_longer_in_force", "never_in_force", "unknown"] },
            "dated": { "type": "boolean" },
            "from": { "type": ["string", "null"], "description": "ISO date the law started or starts to bind." },
            "until": { "type": ["string", "null"], "description": "ISO date an ended law stopped binding, where the law library dates it." },
            "reason": { "type": ["string", "null"], "enum": ["repealed", "replaced", "struck down", "failed", null] },
            "days": { "type": ["integer", "null"] },
            "countdown": { "type": ["string", "null"] },
            "stage": { "type": ["string", "null"] },
            "words": { "type": "string" },
            "as_of": { "type": "string", "description": "The day the countdown was computed. A client that caches the answer recomputes from `from` rather than printing a stale countdown." }
          },
          "required": ["state", "dated", "from", "until", "reason", "days", "countdown", "stage", "words", "as_of"],
          "additionalProperties": false
        },
        "certainty": {
          "type": "object",
          "description": "Which resultset this instrument belongs in, and for forward-looking law how much of it is pinned to primary text rather than predicted. Derived at the call, not stored. A second axis over `kind`, never a replacement: an instrument blocked by a court stays kind \"pending\" and is resultset \"present\", because a court pausing enforcement is a present fact about a live risk. NOTHING IN THE \"forward\" RESULTSET MAY GATE A BUILD.",
          "properties": {
            "resultset": { "type": ["string", "null"], "enum": ["present", "forward", "historical", null], "description": "present: law as it stands. forward: law that may be coming, informational and never gating. historical: spent, and neither. null: LexLint has no rule for this status, which is a coverage gap and never a default placement." },
            "rung": { "type": ["integer", "null"], "enum": [1, 2, 3, 4, 5, null], "description": "Orders the forward resultset by how much is PINNED, never by a probability; 1 is the most certain. null means the law library holds no stage to rank by, which is NOT the lowest rung." },
            "label": { "type": ["string", "null"], "description": "The rung's state in words, or null where there is no rung." },
            "prediction": { "type": ["string", "null"], "description": "How much of this rung is predicted rather than pinned. Rung 1 reports \"none\"." },
            "detail": { "type": ["string", "null"], "description": "Why this placement was reached, in words." }
          },
          "required": ["resultset", "rung", "label", "prediction", "detail"],
          "additionalProperties": false
        },
        "settledness": {
          "type": "object",
          "description": "How much interpretation this duty needs, and the field the counsel lane routes on. Derived in the law library, not at the call. Present only where the law library has a verdict; ABSENT is not \"settled\", it means no rule fires and the lane is a judgment call as before.",
          "properties": {
            "band": { "type": "string" },
            "guidance_url": { "type": "string" },
            "guidance_body": { "type": "string" },
            "case_citation": { "type": "string" },
            "case_url": { "type": "string" },
            "under_challenge": { "type": "boolean" },
            "open_questions": { "type": "array", "items": { "type": "string" } },
            "as_of": { "type": "string", "description": "When the guidance and docket landscape was read. Distinct from the finding's own as_of_date." }
          },
          "required": ["band"],
          "additionalProperties": false
        },
        "court_action": {
          "type": "object",
          "description": "The rulings that changed this law's force, and whether a suit against it is pending, as the run returned them. Present only where a court has done either; absent means no court has, never that none will. Written as the run sent it, and its inner objects are left open on purpose, so a key the law library adds to a ruling later cannot make this file invalid.",
          "properties": {
            "rulings": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "name": { "type": "string" },
                  "name_en": { "type": ["string", "null"] },
                  "court": { "type": ["string", "null"] },
                  "decided": { "type": ["string", "null"] },
                  "role": { "type": "string" },
                  "role_word": { "type": ["string", "null"] },
                  "provisions": { "type": ["array", "null"], "items": { "type": "string" } },
                  "stage": { "type": ["object", "null"] },
                  "url": { "type": "string" }
                },
                "required": ["name", "role", "url"]
              }
            },
            "under_challenge": { "type": "boolean" }
          },
          "required": ["rulings", "under_challenge"]
        },
        "as_of_date": { "type": ["string", "null"] },
        "stale": { "type": ["boolean", "null"] },
        "confidence": { "type": ["string", "null"] },
        "resolved_from": { "type": ["string", "array", "null"] },
        "state": {
          "type": "string",
          "enum": ["new", "acknowledged"],
          "description": "Exactly two values. There is no resolved, fixed, waived, or ignored: an obligation applies whether or not you have met it, so a finding never goes away. acknowledged means you have seen it and recorded where you handled it."
        },
        "where": { "type": "string", "description": "Where in your code you handled it." },
        "note": { "type": "string" },
        "handled_by": {
          "type": "string",
          "minLength": 1,
          "description": "Slug of the work item that answers this finding. Free text today; a mitigation-catalogue key later. Not a claim that the obligation is discharged."
        }
      }
    },
    "vanished": {
      "type": "object",
      "description": "An acknowledged finding that stopped appearing. Kept, and warned about, until a human removes it: the instrument may have been repealed, or its citation may have been edited upstream so the derived id moved. Those have opposite implications and the lint cannot tell them apart.",
      "required": ["id", "last_seen"],
      "additionalProperties": true,
      "properties": {
        "id": { "type": "string", "minLength": 1 },
        "last_seen": {
          "type": ["string", "null"],
          "format": "date",
          "description": "The date this finding was last confirmed present. Null means the prior manifest recorded no run_at to report: the lint declines to invent a sighting date that did not happen."
        },
        "state": { "type": "string", "enum": ["new", "acknowledged"] },
        "where": { "type": "string" },
        "note": { "type": "string" },
        "handled_by": { "type": "string", "minLength": 1 }
      }
    },
    "work_item": {
      "type": "object",
      "required": ["id", "lane", "title", "findings"],
      "additionalProperties": false,
      "properties": {
        "id": { "type": "string", "minLength": 1 },
        "lane": {
          "type": "string",
          "enum": ["code", "doc", "counsel", "product"],
          "description": "code ships a diff, doc drafts an artifact into the repo, counsel is routed to a human and acted on by nobody here. product ships what a public-sector customer needs to discharge a duty of their own."
        },
        "title": { "type": "string", "minLength": 1 },
        "findings": {
          "type": "array",
          "minItems": 1,
          "items": { "type": "string", "minLength": 1 }
        },
        "jurisdictions": {
          "type": "array",
          "items": { "type": "string", "minLength": 1 }
        }
      }
    }
  }
}
