{"version":3,"file":"validate.mjs","names":[],"sources":["../src/validate.ts"],"sourcesContent":["import type {\n  ApplicationMarquette,\n  MARQUETTE_FORMAT_VERSION,\n  MarquetteEnvironment,\n  MarquetteRoute,\n  MarquetteTarget,\n  RenderingMode,\n} from \"./model.js\";\n\n/** Severity of an application-marquette diagnostic. */\nexport type MarquetteDiagnosticSeverity = \"error\" | \"warning\";\n\n/** Stable, source-addressable application-marquette diagnostic. */\nexport interface MarquetteDiagnostic {\n  /** Stable machine-readable diagnostic code. */\n  readonly code: `VIZE_MARQUETTE_${string}`;\n  /** Severity used by CLI, editor, test, and CI consumers. */\n  readonly severity: MarquetteDiagnosticSeverity;\n  /** JSON-style path into the authored marquette. */\n  readonly path: string;\n  /** Human-readable explanation and next action. */\n  readonly message: string;\n}\n\nconst IDENTIFIER = /^[a-z0-9][a-z0-9._-]*$/;\nconst SUPPORTED_FORMAT_VERSION: typeof MARQUETTE_FORMAT_VERSION = 1;\n\n/**\n * Validates a complete application marquette.\n *\n * Diagnostics cover structural, reference, capability, target, and dependency\n * invariants. Results are sorted by path, code, and message so identical input\n * produces stable editor, CLI, test, and CI output.\n *\n * This function does not mutate the authored object and does not throw.\n */\nexport function validateApplicationMarquette(\n  marquette: ApplicationMarquette,\n): MarquetteDiagnostic[] {\n  const diagnostics: MarquetteDiagnostic[] = [];\n  const environments = marquette.environments ?? [];\n  const backends = marquette.backends ?? [];\n  const protocols = marquette.protocols ?? [];\n  const routes = marquette.routes ?? [];\n  const targets = new Set(marquette.targets ?? []);\n  const capabilityIds = new Set(Object.keys(marquette.capabilities ?? {}));\n\n  if ((marquette.formatVersion ?? SUPPORTED_FORMAT_VERSION) !== SUPPORTED_FORMAT_VERSION) {\n    diagnostics.push(\n      error(\"VIZE_MARQUETTE_001\", \"formatVersion\", \"unsupported marquette format version\"),\n    );\n  }\n\n  validateIdentifier(marquette.application, \"application\", \"VIZE_MARQUETTE_002\", diagnostics);\n\n  for (const [key, capability] of Object.entries(marquette.capabilities ?? {})) {\n    const path = contractPath(\"capabilities\", key);\n    validateIdentifier(key, path, \"VIZE_MARQUETTE_003\", diagnostics);\n    if (key !== capability.id) {\n      diagnostics.push(\n        error(\"VIZE_MARQUETTE_004\", path, \"capability map key must equal capability.id\"),\n      );\n    }\n    const version = capability.version ?? 1;\n    if (!Number.isInteger(version) || version < 1) {\n      diagnostics.push(\n        error(\"VIZE_MARQUETTE_005\", path, \"capability version must be greater than zero\"),\n      );\n    }\n    if (capability.description.trim().length === 0) {\n      diagnostics.push(\n        error(\"VIZE_MARQUETTE_024\", path, \"capability description must not be empty\"),\n      );\n    }\n  }\n\n  const environmentIds = collectUniqueIds(\"environments\", environments, diagnostics);\n  const backendIds = collectUniqueIds(\"backends\", backends, diagnostics);\n  const protocolIds = collectUniqueIds(\"protocols\", protocols, diagnostics);\n  collectUniqueIds(\"routes\", routes, diagnostics);\n\n  for (const environment of environments) {\n    const path = contractPath(\"environments\", environment.id);\n    if (!targets.has(environment.target)) {\n      diagnostics.push(\n        error(\"VIZE_MARQUETTE_007\", path, \"environment target must be declared in targets\"),\n      );\n    }\n    for (const dependency of [...new Set(environment.dependsOn ?? [])].sort()) {\n      if (dependency === environment.id) {\n        diagnostics.push(error(\"VIZE_MARQUETTE_008\", path, \"environment cannot depend on itself\"));\n      } else if (!environmentIds.has(dependency)) {\n        diagnostics.push(\n          error(\"VIZE_MARQUETTE_009\", path, \"environment dependency does not exist\"),\n        );\n      }\n    }\n    validateCapabilities(path, environment.capabilities, capabilityIds, diagnostics);\n\n    if (\n      environment.consumer === \"client\" &&\n      (environment.runtime === \"rust\" ||\n        environment.runtime === \"go\" ||\n        environment.runtime === \"jvm\")\n    ) {\n      diagnostics.push(\n        warning(\n          \"VIZE_MARQUETTE_010\",\n          path,\n          \"client environment uses a server-oriented runtime; declare an adapter capability if this is intentional\",\n        ),\n      );\n    }\n  }\n\n  validateEnvironmentCycles(environments, diagnostics);\n\n  for (const backend of backends) {\n    const path = contractPath(\"backends\", backend.id);\n    if (backend.environment != null) {\n      const environment = environments.find((candidate) => candidate.id === backend.environment);\n      if (environment == null) {\n        diagnostics.push(error(\"VIZE_MARQUETTE_011\", path, \"backend environment does not exist\"));\n      } else if (environment.consumer !== \"server\") {\n        diagnostics.push(\n          error(\"VIZE_MARQUETTE_012\", path, \"backend environment must be a server consumer\"),\n        );\n      }\n    }\n    validateCapabilities(path, backend.capabilities, capabilityIds, diagnostics);\n  }\n\n  for (const protocol of protocols) {\n    const path = contractPath(\"protocols\", protocol.id);\n    if (!backendIds.has(protocol.backend)) {\n      diagnostics.push(error(\"VIZE_MARQUETTE_013\", path, \"protocol backend does not exist\"));\n    }\n    validateCapabilities(path, protocol.capabilities, capabilityIds, diagnostics);\n  }\n\n  const routePaths = new Map<string, string>();\n  for (const route of routes) {\n    const path = contractPath(\"routes\", route.id);\n    if (!route.path.startsWith(\"/\")) {\n      diagnostics.push(error(\"VIZE_MARQUETTE_014\", path, \"route path must start with /\"));\n    }\n    const environment = environments.find((candidate) => candidate.id === route.environment);\n    if (environment == null) {\n      diagnostics.push(error(\"VIZE_MARQUETTE_015\", path, \"route environment does not exist\"));\n    }\n    if (route.backend != null && !backendIds.has(route.backend)) {\n      diagnostics.push(error(\"VIZE_MARQUETTE_016\", path, \"route backend does not exist\"));\n    }\n    if (route.protocol != null) {\n      const protocol = protocols.find((candidate) => candidate.id === route.protocol);\n      if (!protocolIds.has(route.protocol) || protocol == null) {\n        diagnostics.push(error(\"VIZE_MARQUETTE_017\", path, \"route protocol does not exist\"));\n      } else if (route.backend != null && protocol.backend !== route.backend) {\n        diagnostics.push(\n          error(\n            \"VIZE_MARQUETTE_018\",\n            path,\n            \"route protocol and backend must refer to the same service\",\n          ),\n        );\n      }\n    }\n    if (environment != null && !renderingMatchesTarget(route, environment.target)) {\n      diagnostics.push(\n        error(\n          \"VIZE_MARQUETTE_023\",\n          path,\n          \"rendering mode is not compatible with the route environment target\",\n        ),\n      );\n    }\n    validateCapabilities(path, route.capabilities, capabilityIds, diagnostics);\n\n    const routeKey = `${route.environment}\\u0000${route.path}`;\n    const previous = routePaths.get(routeKey);\n    if (previous != null) {\n      diagnostics.push(\n        error(\"VIZE_MARQUETTE_019\", path, `route path is already used by route \"${previous}\"`),\n      );\n    }\n    routePaths.set(routeKey, route.id);\n  }\n\n  for (const target of targets) {\n    if (!environments.some((environment) => environment.target === target)) {\n      diagnostics.push(\n        warning(\"VIZE_MARQUETTE_020\", \"targets\", \"declared target has no environment\"),\n      );\n    }\n  }\n\n  return diagnostics.sort(compareDiagnostics);\n}\n\nfunction collectUniqueIds(\n  collection: string,\n  values: readonly { readonly id: string }[],\n  diagnostics: MarquetteDiagnostic[],\n): Set<string> {\n  const ids = new Set<string>();\n  for (const value of values) {\n    const path = contractPath(collection, value.id);\n    validateIdentifier(value.id, path, \"VIZE_MARQUETTE_006\", diagnostics);\n    if (ids.has(value.id)) {\n      diagnostics.push(\n        error(\"VIZE_MARQUETTE_006\", path, \"identifier must be unique within its collection\"),\n      );\n    }\n    ids.add(value.id);\n  }\n  return ids;\n}\n\nfunction validateIdentifier(\n  id: string,\n  path: string,\n  code: `VIZE_MARQUETTE_${string}`,\n  diagnostics: MarquetteDiagnostic[],\n): void {\n  if (!IDENTIFIER.test(id)) {\n    diagnostics.push(\n      error(\n        code,\n        path,\n        \"identifier must use lowercase ASCII letters, digits, dash, underscore, or dot\",\n      ),\n    );\n  }\n}\n\nfunction validateCapabilities(\n  path: string,\n  capabilities: readonly string[] | undefined,\n  declared: ReadonlySet<string>,\n  diagnostics: MarquetteDiagnostic[],\n): void {\n  for (const capability of [...new Set(capabilities ?? [])].sort()) {\n    if (!declared.has(capability)) {\n      diagnostics.push(error(\"VIZE_MARQUETTE_021\", path, \"referenced capability is not declared\"));\n    }\n  }\n}\n\nfunction validateEnvironmentCycles(\n  environments: readonly MarquetteEnvironment[],\n  diagnostics: MarquetteDiagnostic[],\n): void {\n  const graph = new Map(environments.map((value) => [value.id, value.dependsOn ?? []]));\n  const visiting = new Set<string>();\n  const visited = new Set<string>();\n\n  for (const id of [...graph.keys()].sort()) {\n    if (hasCycle(id, graph, visiting, visited)) {\n      diagnostics.push(\n        error(\n          \"VIZE_MARQUETTE_022\",\n          contractPath(\"environments\", id),\n          \"environment dependency graph must be acyclic\",\n        ),\n      );\n    }\n  }\n}\n\nfunction hasCycle(\n  id: string,\n  graph: ReadonlyMap<string, readonly string[]>,\n  visiting: Set<string>,\n  visited: Set<string>,\n): boolean {\n  if (visited.has(id)) return false;\n  if (visiting.has(id)) return true;\n  visiting.add(id);\n  const cyclic = [...(graph.get(id) ?? [])]\n    .sort()\n    .some((dependency) => graph.has(dependency) && hasCycle(dependency, graph, visiting, visited));\n  visiting.delete(id);\n  visited.add(id);\n  return cyclic;\n}\n\nfunction renderingMatchesTarget(route: MarquetteRoute, target: MarquetteTarget): boolean {\n  const targetRendering: Partial<Record<RenderingMode, MarquetteTarget>> = {\n    native: \"native\",\n    desktop: \"desktop\",\n    terminal: \"terminal\",\n  };\n  return (targetRendering[route.rendering] ?? \"web\") === target;\n}\n\nfunction compareDiagnostics(left: MarquetteDiagnostic, right: MarquetteDiagnostic): number {\n  return (\n    compareText(left.path, right.path) ||\n    compareText(left.code, right.code) ||\n    compareText(left.message, right.message)\n  );\n}\n\nfunction compareText(left: string, right: string): number {\n  return left < right ? -1 : left > right ? 1 : 0;\n}\n\nfunction contractPath(collection: string, id: string): string {\n  return `${collection}.${id}`;\n}\n\nfunction error(\n  code: `VIZE_MARQUETTE_${string}`,\n  path: string,\n  message: string,\n): MarquetteDiagnostic {\n  return { code, severity: \"error\", path, message };\n}\n\nfunction warning(\n  code: `VIZE_MARQUETTE_${string}`,\n  path: string,\n  message: string,\n): MarquetteDiagnostic {\n  return { code, severity: \"warning\", path, message };\n}\n"],"mappings":"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"}