{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://hushspec.org/schemas/hushspec-merge-vector.v1.schema.json",
  "title": "HushSpec Merge Vector v1",
  "description": "The shape of a merge vector directory under fixtures/ (core spec Section 4, Merge Semantics; Section 8 Level 2). Merge vectors are a directory convention rather than a single file, so this schema describes the *descriptor* a conformance runner builds for one directory -- which is what the four SDK runners already reconstruct from the filenames -- and, under $defs/FixtureManifest, the optional fixture.yaml that sits in the directory.\n\nDiscovery. A merge vector directory is any directory under fixtures/ that holds a base.yaml beside at least one child-<name>.yaml. Runners walk fixtures/<module>/merge/ and its subdirectories; a vector that needs its own base -- a digest pin names one exact document, so a pin-match and a pin-mismatch case cannot share one -- gets a subdirectory instead of colliding with the shared base.\n\nFiles. base.yaml is the parent document. child-<name>.yaml is the overlay; its merge_strategy selects the strategy under test. expected-<name>.yaml is the document the merge MUST produce, matched to its child by replacing the leading 'child-' with 'expected-'. Any other *.yaml in the directory (an intermediate hop of a multi-hop chain, say) is inert: runners only iterate the child-*.yaml files.\n\nComposition. A child whose extends carries a '#sha256:' pin is resolved (core Section 2.3) with a loader scoped to the vector directory, which also accepts the bare references 'base' and 'base.yaml'; the pin is then actually checked. Every other child is composed with a direct merge(base, child), which is what the vectors are testing.\n\nRefusal. A vector that must be refused rather than merged carries no expected-<name>.yaml and is marked instead. Only two markings are honoured by all four SDK runners, so only these two are normative: an 'expect-reject' file in the directory, or 'reject: true' in the directory's fixture.yaml. Both are directory-wide; a refusal case therefore lives in its own subdirectory with its own base.yaml. The per-child spellings some runners additionally accept (a '<stem>.expect-reject' marker, a 'reject' name list, per-child entries under 'cases') are tolerated aliases, not portable.",
  "type": "object",
  "required": [
    "directory",
    "base",
    "children"
  ],
  "additionalProperties": false,
  "properties": {
    "directory": {
      "type": "string",
      "pattern": "^fixtures/[A-Za-z0-9._-]+(?:/[A-Za-z0-9._-]+)*$",
      "description": "Repository-relative path of the vector directory, POSIX separators."
    },
    "base": {
      "const": "base.yaml",
      "description": "The parent document. Always named base.yaml: discovery in all four runners keys on that exact name."
    },
    "children": {
      "type": "array",
      "minItems": 1,
      "uniqueItems": true,
      "items": {
        "$ref": "#/$defs/Child"
      },
      "description": "Every child-<name>.yaml in the directory, sorted by file name."
    },
    "manifest": {
      "$ref": "#/$defs/FixtureManifest",
      "description": "The parsed fixture.yaml, when the directory has one."
    },
    "reject_marker": {
      "type": "string",
      "enum": [
        "expect-reject",
        "fixture.yaml"
      ],
      "description": "How the directory declares that its vectors are refusals. Present only for a refusal directory; absent for a merging one."
    }
  },
  "$defs": {
    "Child": {
      "type": "object",
      "required": [
        "file",
        "expected",
        "pinned"
      ],
      "additionalProperties": false,
      "description": "One overlay document and what the runner must do with it.",
      "properties": {
        "file": {
          "type": "string",
          "pattern": "^child-[A-Za-z0-9._-]+\\.ya?ml$",
          "description": "File name of the overlay, which MUST start with 'child-'."
        },
        "expected": {
          "type": [
            "string",
            "null"
          ],
          "pattern": "^expected-[A-Za-z0-9._-]+\\.ya?ml$",
          "description": "File name of the expected merged document: the child's name with 'child-' replaced by 'expected-'. Null exactly when the directory is marked as a refusal, since a refused vector produces no document to compare."
        },
        "pinned": {
          "type": "boolean",
          "description": "Whether the child's extends carries a '#sha256:' digest pin, which sends the vector through the resolver instead of a direct merge."
        },
        "merge_strategy": {
          "enum": [
            "deep_merge",
            "merge",
            "replace"
          ],
          "description": "The strategy the child declares (core Section 4). Absent when the child relies on the default, deep_merge."
        }
      }
    },
    "FixtureManifest": {
      "type": "object",
      "additionalProperties": false,
      "description": "fixture.yaml: optional per-directory metadata. Its only portable key is the directory-wide 'reject'. 'description' is informative and ignored by runners.",
      "properties": {
        "description": {
          "type": "string",
          "minLength": 1,
          "description": "What the directory's vectors exercise, and the specification section they come from."
        },
        "reject": {
          "const": true,
          "description": "Every child in this directory MUST be refused rather than merged. Only the literal true is portable across the four runners; a list of child names is an alias one runner accepts and the others ignore, so it MUST NOT be used."
        },
        "cases": {
          "type": "object",
          "description": "Per-child overrides. Honoured by the Python and Go runners only, so a directory that relies on this is not portable; kept in the schema because the corpus may still carry one.",
          "additionalProperties": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "reject": {
                "type": "boolean",
                "description": "Whether this one child is refused."
              }
            }
          }
        }
      }
    }
  }
}
