{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://motion.metatwinmaker.com/twins.schema.json",
  "title": "MotionMeta twin catalog",
  "description": "The digital twins on motion.metatwinmaker.com, for the metatwinmaker.com catalog. Each twin is a machine that runs live in the browser. Tags are ids from `vocabulary`, which gives each a label and a description. Units are SI.",
  "type": "object",
  "required": ["schemaVersion", "site", "vocabulary", "twins", "jev"],
  "properties": {
    "$schema": {
      "type": "string"
    },
    "schemaVersion": {
      "const": 1,
      "description": "Bumped when a field changes meaning or is removed."
    },
    "site": {
      "type": "object",
      "description": "The site the twins live on.",
      "required": ["id", "name", "url", "parent", "domain", "focus", "notice", "environment"],
      "properties": {
        "id": {
          "type": "string"
        },
        "name": {
          "type": "string"
        },
        "url": {
          "type": "string",
          "format": "uri"
        },
        "parent": {
          "type": "string",
          "format": "uri",
          "description": "The catalog site."
        },
        "domain": {
          "type": "string",
          "description": "The site's field."
        },
        "focus": {
          "type": "string"
        },
        "notice": {
          "type": "string",
          "description": "Shown with any of the site's content."
        },
        "units": {
          "const": "SI"
        },
        "environment": {
          "type": "object",
          "description": "The 3D space the twins stand in.",
          "properties": {
            "name": {
              "type": "string"
            },
            "description": {
              "type": "string"
            },
            "url": {
              "type": "string",
              "format": "uri"
            },
            "model": {
              "type": "string",
              "format": "uri"
            },
            "triangles": {
              "type": "integer"
            },
            "coordinates": {
              "type": "string"
            }
          }
        },
        "sections": {
          "type": "array",
          "description": "The site's other parts that are not twins.",
          "items": {
            "type": "object",
            "required": ["id", "name", "url", "description"],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "url": {
                "type": "string",
                "format": "uri"
              },
              "description": {
                "type": "string"
              }
            }
          }
        },
        "features": {
          "type": "array",
          "items": {
            "type": "string"
          }
        }
      }
    },
    "vocabulary": {
      "type": "object",
      "description": "Every id a twin's classification may use, with a label and a description.",
      "properties": {
        "categories": {
          "$ref": "#/$defs/terms"
        },
        "industries": {
          "$ref": "#/$defs/terms"
        },
        "concepts": {
          "$ref": "#/$defs/terms"
        },
        "safety": {
          "$ref": "#/$defs/terms"
        },
        "audiences": {
          "$ref": "#/$defs/terms"
        },
        "instructions": {
          "$ref": "#/$defs/terms",
          "description": "Logix motion instructions, by mnemonic."
        },
        "levels": {
          "$ref": "#/$defs/terms"
        }
      }
    },
    "twins": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/twin"
      }
    },
    "jev": {
      "type": "object",
      "description": "For Jev (TypeSafe AI, https://docs.typesafe.ai/api). Send `api.model`, a twin's `jev.state` as `state`, and `questions`. The category is a `choice` whose `criteria` map an option's id to its description (up to 255 options); the level is a `score` over ordered levels; each industry, concept, safety function and audience is a `noul` (yes or no), keyed `<facet>_<id>` with hyphens as underscores.",
      "required": ["api", "questions"],
      "properties": {
        "about": {
          "type": "string"
        },
        "questions": {
          "type": "object",
          "additionalProperties": {
            "type": "object",
            "required": ["type", "instructions"],
            "properties": {
              "type": {
                "enum": ["choice", "score", "noul"]
              },
              "instructions": {
                "type": "string"
              },
              "criteria": {
                "description": "For a choice, a map of option id to its description; for a score, an array of ordered level descriptions.",
                "type": ["object", "array"]
              }
            }
          }
        },
        "api": {
          "type": "object",
          "properties": {
            "endpoint": {
              "type": "string",
              "format": "uri"
            },
            "method": {
              "const": "POST"
            },
            "model": {
              "type": "string"
            },
            "docs": {
              "type": "string",
              "format": "uri"
            },
            "cloudflareAiGateway": {
              "type": "object",
              "properties": {
                "endpoint": {
                  "type": "string"
                },
                "providerBaseUrl": {
                  "type": "string",
                  "format": "uri"
                },
                "headers": {
                  "type": "object",
                  "additionalProperties": {
                    "type": "string"
                  }
                },
                "docs": {
                  "type": "string",
                  "format": "uri"
                }
              }
            }
          },
          "description": "Where to send the request: TypeSafe's own endpoint, or the same request through a Cloudflare AI Gateway with TypeSafe as a custom provider ({placeholders} are the gateway's)."
        }
      }
    }
  },
  "$defs": {
    "terms": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["id", "label", "description"],
        "properties": {
          "id": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "description": {
            "type": "string"
          }
        }
      }
    },
    "twin": {
      "type": "object",
      "required": [
        "id",
        "station",
        "name",
        "url",
        "summary",
        "description",
        "machine",
        "classification",
        "views",
        "scenarios",
        "trends",
        "model",
        "simulation",
        "media",
        "related",
        "jev"
      ],
      "properties": {
        "id": {
          "type": "string",
          "description": "Stable; used in addresses and `related`."
        },
        "station": {
          "type": ["integer", "null"],
          "description": "Its station number in the lab hall; null for a twin in a room of its own (see `room`)."
        },
        "name": {
          "type": "string"
        },
        "url": {
          "type": "string",
          "format": "uri",
          "description": "Its page."
        },
        "summary": {
          "type": "string",
          "description": "One sentence."
        },
        "description": {
          "type": "string",
          "description": "A paragraph."
        },
        "machine": {
          "type": "object",
          "required": ["class", "lookAlike", "workpiece", "control", "axes"],
          "properties": {
            "class": {
              "type": "string",
              "description": "The kind of machine, generically."
            },
            "lookAlike": {
              "const": true,
              "description": "A look-alike of a machine class; no maker's product is modelled."
            },
            "workpiece": {
              "type": "string",
              "description": "What goes in and what comes out."
            },
            "control": {
              "type": "string"
            },
            "axes": {
              "type": "array",
              "items": {
                "type": "object",
                "required": ["name", "kind", "actuator", "role"],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "kind": {
                    "enum": ["linear", "rotary"]
                  },
                  "actuator": {
                    "type": "string"
                  },
                  "role": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "classification": {
          "type": "object",
          "description": "Ids from `vocabulary`.",
          "required": [
            "category",
            "industries",
            "concepts",
            "instructions",
            "safety",
            "audiences",
            "level",
            "keywords"
          ],
          "properties": {
            "category": {
              "type": "string",
              "description": "One id from vocabulary.categories."
            },
            "industries": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "concepts": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "instructions": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "safety": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "audiences": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "level": {
              "enum": ["introductory", "intermediate", "advanced"]
            },
            "keywords": {
              "type": "array",
              "items": {
                "type": "string"
              },
              "description": "Free words a search should match."
            }
          }
        },
        "views": {
          "type": "array",
          "description": "Camera views, each with its own address; `key` is its keyboard shortcut on the page.",
          "items": {
            "type": "object",
            "required": ["id", "label", "url"],
            "properties": {
              "id": {
                "type": "string"
              },
              "label": {
                "type": "string"
              },
              "key": {
                "type": "string"
              },
              "url": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        },
        "scenarios": {
          "type": "array",
          "description": "Troubleshooting scenarios: the twin set up the way a real problem shows itself. The visitor runs a test, tries fixes and tests again; the symptom is there while its measure is above the threshold (`above`), or, for what a line makes, below it (`below`).",
          "items": {
            "type": "object",
            "required": [
              "id",
              "title",
              "url",
              "report",
              "lookFor",
              "test",
              "symptom",
              "fixesToTry",
              "cause"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "title": {
                "type": "string"
              },
              "url": {
                "type": "string",
                "format": "uri"
              },
              "report": {
                "type": "string",
                "description": "What the operator reports."
              },
              "lookFor": {
                "type": "string",
                "description": "Where the evidence shows."
              },
              "test": {
                "type": "string",
                "description": "What the scenario's test runs."
              },
              "symptom": {
                "type": "object",
                "required": ["measure"],
                "oneOf": [
                  {
                    "required": ["above"]
                  },
                  {
                    "required": ["below"]
                  }
                ],
                "properties": {
                  "measure": {
                    "type": "string"
                  },
                  "above": {
                    "type": "number"
                  },
                  "below": {
                    "type": "number"
                  }
                }
              },
              "fixesToTry": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Some help without curing it; at least one cures it."
              },
              "cause": {
                "type": "string",
                "description": "The root cause, explained."
              }
            }
          }
        },
        "trends": {
          "type": "array",
          "description": "Live trend panels, one measure per panel.",
          "items": {
            "type": "object",
            "required": ["title", "unit", "series"],
            "properties": {
              "group": {
                "type": "string"
              },
              "title": {
                "type": "string"
              },
              "unit": {
                "type": "string"
              },
              "series": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          }
        },
        "lessons": {
          "type": "array",
          "description": "Academy lessons run on this twin.",
          "items": {
            "type": "object"
          }
        },
        "faults": {
          "type": "array",
          "description": "Fault catalog entries this twin can reproduce or explain.",
          "items": {
            "type": "object"
          }
        },
        "model": {
          "type": "object",
          "required": ["url", "manifest", "format", "units", "triangles", "joints"],
          "properties": {
            "url": {
              "type": "string",
              "format": "uri"
            },
            "manifest": {
              "type": "string",
              "format": "uri"
            },
            "format": {
              "type": "string"
            },
            "units": {
              "const": "m"
            },
            "triangles": {
              "type": "integer"
            },
            "joints": {
              "type": "array",
              "description": "The model's moving nodes.",
              "items": {
                "type": "object",
                "properties": {
                  "node": {
                    "type": "string"
                  },
                  "kind": {
                    "enum": ["linear", "rotary"]
                  }
                }
              }
            }
          }
        },
        "hall": {
          "type": ["object", "null"],
          "description": "Where the twin's station is on the hall's floor plan, m.",
          "properties": {
            "x": {
              "type": "number"
            },
            "z": {
              "type": "number"
            },
            "walkUrl": {
              "type": "string",
              "format": "uri"
            }
          }
        },
        "buildGuide": {
          "type": "string",
          "format": "uri",
          "description": "A step-by-step guide to building the real machine the twin models, from generic parts matched to the twin's figures, if it has one."
        },
        "room": {
          "type": "object",
          "description": "For a twin in a room of its own: the room, its address, its model, and where its door is on the hall's floor plan, m.",
          "required": ["name", "url", "model", "door"],
          "properties": {
            "name": {
              "type": "string"
            },
            "url": {
              "type": "string",
              "format": "uri"
            },
            "model": {
              "type": "string",
              "format": "uri"
            },
            "door": {
              "type": ["object", "null"],
              "properties": {
                "x": {
                  "type": "number"
                },
                "z": {
                  "type": "number"
                }
              }
            }
          }
        },
        "simulation": {
          "type": "string",
          "description": "Where and how it runs."
        },
        "media": {
          "type": "object",
          "description": "Recordings of the twin, for marketing and the catalog.",
          "properties": {
            "videos": {
              "type": "array",
              "items": {
                "type": "object",
                "required": ["title", "url"],
                "properties": {
                  "title": {
                    "type": "string"
                  },
                  "url": {
                    "type": "string",
                    "format": "uri"
                  }
                }
              }
            }
          }
        },
        "related": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Twin ids."
        },
        "jev": {
          "type": "object",
          "required": ["state", "stateText", "expected"],
          "properties": {
            "state": {
              "type": "object",
              "description": "The structured state Jev decides from: what the twin is, does and shows (its machine, axes, control, troubleshooting problems and trends), without its tags."
            },
            "expected": {
              "type": "object",
              "description": "The answer the twin's own tags give to each question in the top-level `jev.questions`: an option id for a choice, the level's index for a score, true or false for a noul.",
              "additionalProperties": {
                "type": ["string", "number", "boolean"]
              }
            },
            "stateText": {
              "type": "string",
              "description": "The same state as one block of text."
            }
          }
        }
      }
    }
  }
}
