{"version":3,"file":"boot-check.mjs","names":[],"sources":["../../../src/services/resolution/boot-check.ts"],"sourcesContent":["import type { BosConfigInput } from \"../../types\";\nimport { BosConfigSchema } from \"../../types\";\nimport {\n  CircularExtendsError,\n  ConfigExtendsError,\n  ConfigLoadError,\n  ConfigNotfoundError,\n  PUBLISHED_CONFIG_ENV,\n} from \"./load\";\nimport { resolveExtendsRef } from \"./merge\";\nimport { type ResolutionIo, walkExtendsChain } from \"./session\";\n\n/**\n * The publish boot-check: given a *hypothetical* leaf payload (the train's\n * canonicalized publish payload, or a rollback snapshot), decide whether a\n * consumer boot would accept it, and report why not.\n *\n * Boot truth is a property of the extends-merged shape, never the leaf: a\n * consumer fetches the stored leaf, walks the extends chain, merges\n * child-wins, and parses with BosConfigSchema (resolvePublishedConfig). The\n * check re-runs exactly that pipeline — the same walker, merge, and schema —\n * with the payload substituted for the leaf node (`seedConfig`), so parity\n * holds by construction rather than by imitation. It deliberately does NOT\n * parse the leaf as-is when the leaf carries an extends ref: that is the\n * leaf-vs-merged confusion behind the rc.23 gate regression, where every\n * extends child was rejected for an `app.host` slot it legitimately inherits.\n *\n * Invariants:\n * - `ok` is one-directional soundness: `ok: true` means a consumer boot of\n *   this payload — against the parent-chain state observed during the check —\n *   would parse. The parent can drift between check and boot; the\n *   post-publish smoke remains the authoritative whole-boot gate. This check\n *   covers the config plane only (walk + merge + schema); pin/SRI byte\n *   verification stays with `verifyRollbackSnapshot`.\n * - Root payloads (no resolvable extends ref) do zero I/O — the walk's seed\n *   short-circuit returns the payload untouched and the check degenerates to\n *   a plain `safeParse`.\n * - Never throws for data: unreachable parents, circular chains, network\n *   failures, and garbage payloads all become issues. Only a thrown non-Error\n *   (a programmer error) propagates.\n * - Never mutates the payload, never exposes the merged shape (defu's merge\n *   reuses input subtrees — handing callers the merged object would alias\n *   their payload).\n * - `chain` is diagnostic display only — callers never branch on it.\n */\n\nexport interface BootCheckIssue {\n  /** \"chain\" — the extends walk itself failed (nothing to parse);\n   *  \"schema\" — BosConfigSchema rejected the effective (merged) shape. */\n  code: \"schema\" | \"chain\";\n  /** Dotted config path for schema issues (\"app.host\"); the offending\n   *  extends ref for chain issues; \"\" for whole-config schema failures. */\n  path: string;\n  /** Whose bytes contributed the failure: the payload itself, or a parent\n   *  link (best-effort — a failing dotted path present in the leaf is\n   *  attributed to the leaf; anything else to the chain). */\n  origin: \"payload\" | \"parent\";\n  message: string;\n}\n\nexport interface BootCheckReport {\n  /** Strictly mirrors what a consumer boot would accept — no false greens,\n   *  no false reds relative to boot (against observed parent state). */\n  ok: boolean;\n  issues: readonly BootCheckIssue[];\n  /** The parent refs walked (leaf excluded), in walk order — nearest parent\n   *  first; empty for root payloads and failed walks (the issue message\n   *  carries the tail). */\n  chain: readonly string[];\n}\n\nexport interface BootCheckOptions {\n  /** Registry override, threaded to parent fetches exactly as\n   *  resolvePublishedConfig threads it. */\n  registry?: string;\n  /** The one substitutable seam — the io `openResolution` itself takes.\n   *  Tests supply in-memory fetchers; production never passes it. */\n  io?: ResolutionIo;\n}\n\nexport interface ResolvedBootShape {\n  /** The parent refs walked (leaf excluded), in walk order — nearest parent\n   *  first; empty for root payloads. */\n  chain: readonly string[];\n  /** The extends-merged (child-wins) shape — a structured clone, safe to\n   *  hand to callers that may mutate it (the defu merge reuses input\n   *  subtrees). Root payloads clone the payload itself. */\n  config: BosConfigInput;\n}\n\nfunction extendsRefOf(payload: unknown): string | undefined {\n  return payload !== null && typeof payload === \"object\" && (payload as BosConfigInput).extends\n    ? resolveExtendsRef((payload as BosConfigInput).extends, PUBLISHED_CONFIG_ENV)\n    : undefined;\n}\n\n/**\n * The boot walk itself, exposed so consumers beside `verifyBootPayload` can\n * gate on the merged shape (the deploy train's composition gate) instead of\n * re-walking under their own walker. Throws on chain failure; the caller\n * decides how to report it.\n */\nexport async function resolveBootShape(\n  payload: unknown,\n  slot: { account: string; gateway: string },\n  opts: BootCheckOptions = {},\n): Promise<ResolvedBootShape> {\n  if (payload === null || typeof payload !== \"object\") {\n    throw new Error(\"payload is not a config object\");\n  }\n  const input = payload as BosConfigInput;\n  if (!extendsRefOf(input)) {\n    return { chain: [], config: structuredClone(input) };\n  }\n  const walked = await walkExtendsChain(`bos://${slot.account}/${slot.gateway}`, {\n    env: PUBLISHED_CONFIG_ENV,\n    ...(opts.registry ? { registry: opts.registry } : {}),\n    ...(opts.io ? { io: opts.io } : {}),\n    seedConfig: structuredClone(input),\n    visit: async () => {},\n  });\n  return { chain: walked.chain.slice(1), config: structuredClone(walked.config) };\n}\n\n/** Verify a payload against a shape the caller already walked with\n * `resolveBootShape` for this exact payload — the pure half of\n * `verifyBootPayload`, so a shared walk cannot drift from its verification. */\nexport function verifyResolvedBootShape(\n  payload: unknown,\n  shape: ResolvedBootShape,\n): BootCheckReport {\n  const extendsRef = extendsRefOf(payload);\n  if (!extendsRef) {\n    return { chain: [], ...parseShape(payload as object, payload, false) };\n  }\n  return { chain: shape.chain, ...parseShape(payload as object, shape.config, true) };\n}\n\nexport async function verifyBootPayload(\n  payload: unknown,\n  slot: { account: string; gateway: string },\n  opts: BootCheckOptions = {},\n): Promise<BootCheckReport> {\n  if (payload === null || typeof payload !== \"object\") {\n    return {\n      ok: false,\n      issues: [\n        {\n          code: \"schema\",\n          path: \"\",\n          origin: \"payload\",\n          message: \"payload is not a config object\",\n        },\n      ],\n      chain: [],\n    };\n  }\n  try {\n    return verifyResolvedBootShape(payload, await resolveBootShape(payload, slot, opts));\n  } catch (error) {\n    if (!(error instanceof Error)) throw error;\n    return {\n      ok: false,\n      chain: [],\n      issues: [\n        {\n          code: \"chain\",\n          path: extendsRefOf(payload) ?? \"\",\n          origin: \"parent\",\n          message: chainFailureMessage(error),\n        },\n      ],\n    };\n  }\n}\n\n/** Parse a shape with BosConfigSchema and map the outcome onto a report —\n *  the merged shape in the child path, the payload itself in the root path.\n *  Origin attribution (child path only): a failing dotted path that exists\n *  in the leaf payload is the leaf's; anything else came from the chain. */\nfunction parseShape(\n  payload: object,\n  shape: unknown,\n  attributed: boolean,\n): Pick<BootCheckReport, \"ok\" | \"issues\"> {\n  const parsed = BosConfigSchema.safeParse(shape);\n  if (parsed.success) return { ok: true, issues: [] };\n  return {\n    ok: false,\n    issues: parsed.error.issues.map((issue) => ({\n      code: \"schema\" as const,\n      path: issue.path.join(\".\"),\n      origin:\n        !attributed || pathExistsIn(payload, issue.path)\n          ? (\"payload\" as const)\n          : (\"parent\" as const),\n      message: issue.message,\n    })),\n  };\n}\n\nfunction pathExistsIn(payload: object, path: PropertyKey[]): boolean {\n  let cursor: unknown = payload;\n  for (const key of path) {\n    if (cursor === null || typeof cursor !== \"object\" || !(key in (cursor as object))) {\n      return false;\n    }\n    cursor = (cursor as Record<PropertyKey, unknown>)[key];\n  }\n  return true;\n}\n\nfunction chainFailureMessage(error: Error): string {\n  if (error instanceof CircularExtendsError) return `circular extends: ${error.message}`;\n  if (error instanceof ConfigNotfoundError)\n    return `a parent config could not be found: ${error.message}`;\n  if (error instanceof ConfigLoadError)\n    return `a parent config could not be loaded: ${error.message}`;\n  if (error instanceof ConfigExtendsError)\n    return `the extends chain could not be resolved: ${error.message}`;\n  return `the extends chain could not be resolved: ${error.message}`;\n}\n"],"mappings":";;;;;;AA0FA,SAAS,aAAa,SAAsC;CAC1D,OAAO,YAAY,QAAQ,OAAO,YAAY,YAAa,QAA2B,UAClF,kBAAmB,QAA2B,SAAS,oBAAoB,IAC3E;AACN;;;;;;;AAQA,eAAsB,iBACpB,SACA,MACA,OAAyB,CAAC,GACE;CAC5B,IAAI,YAAY,QAAQ,OAAO,YAAY,UACzC,MAAM,IAAI,MAAM,gCAAgC;CAElD,MAAM,QAAQ;CACd,IAAI,CAAC,aAAa,KAAK,GACrB,OAAO;EAAE,OAAO,CAAC;EAAG,QAAQ,gBAAgB,KAAK;CAAE;CAErD,MAAM,SAAS,MAAM,iBAAiB,SAAS,KAAK,QAAQ,GAAG,KAAK,WAAW;EAC7E,KAAK;EACL,GAAI,KAAK,WAAW,EAAE,UAAU,KAAK,SAAS,IAAI,CAAC;EACnD,GAAI,KAAK,KAAK,EAAE,IAAI,KAAK,GAAG,IAAI,CAAC;EACjC,YAAY,gBAAgB,KAAK;EACjC,OAAO,YAAY,CAAC;CACtB,CAAC;CACD,OAAO;EAAE,OAAO,OAAO,MAAM,MAAM,CAAC;EAAG,QAAQ,gBAAgB,OAAO,MAAM;CAAE;AAChF;;;;AAKA,SAAgB,wBACd,SACA,OACiB;CAEjB,IAAI,CADe,aAAa,OAClB,GACZ,OAAO;EAAE,OAAO,CAAC;EAAG,GAAG,WAAW,SAAmB,SAAS,KAAK;CAAE;CAEvE,OAAO;EAAE,OAAO,MAAM;EAAO,GAAG,WAAW,SAAmB,MAAM,QAAQ,IAAI;CAAE;AACpF;AAEA,eAAsB,kBACpB,SACA,MACA,OAAyB,CAAC,GACA;CAC1B,IAAI,YAAY,QAAQ,OAAO,YAAY,UACzC,OAAO;EACL,IAAI;EACJ,QAAQ,CACN;GACE,MAAM;GACN,MAAM;GACN,QAAQ;GACR,SAAS;EACX,CACF;EACA,OAAO,CAAC;CACV;CAEF,IAAI;EACF,OAAO,wBAAwB,SAAS,MAAM,iBAAiB,SAAS,MAAM,IAAI,CAAC;CACrF,SAAS,OAAO;EACd,IAAI,EAAE,iBAAiB,QAAQ,MAAM;EACrC,OAAO;GACL,IAAI;GACJ,OAAO,CAAC;GACR,QAAQ,CACN;IACE,MAAM;IACN,MAAM,aAAa,OAAO,KAAK;IAC/B,QAAQ;IACR,SAAS,oBAAoB,KAAK;GACpC,CACF;EACF;CACF;AACF;;;;;AAMA,SAAS,WACP,SACA,OACA,YACwC;CACxC,MAAM,SAAS,gBAAgB,UAAU,KAAK;CAC9C,IAAI,OAAO,SAAS,OAAO;EAAE,IAAI;EAAM,QAAQ,CAAC;CAAE;CAClD,OAAO;EACL,IAAI;EACJ,QAAQ,OAAO,MAAM,OAAO,KAAK,WAAW;GAC1C,MAAM;GACN,MAAM,MAAM,KAAK,KAAK,GAAG;GACzB,QACE,CAAC,cAAc,aAAa,SAAS,MAAM,IAAI,IAC1C,YACA;GACP,SAAS,MAAM;EACjB,EAAE;CACJ;AACF;AAEA,SAAS,aAAa,SAAiB,MAA8B;CACnE,IAAI,SAAkB;CACtB,KAAK,MAAM,OAAO,MAAM;EACtB,IAAI,WAAW,QAAQ,OAAO,WAAW,YAAY,EAAE,OAAQ,SAC7D,OAAO;EAET,SAAU,OAAwC;CACpD;CACA,OAAO;AACT;AAEA,SAAS,oBAAoB,OAAsB;CACjD,IAAI,iBAAiB,sBAAsB,OAAO,qBAAqB,MAAM;CAC7E,IAAI,iBAAiB,qBACnB,OAAO,uCAAuC,MAAM;CACtD,IAAI,iBAAiB,iBACnB,OAAO,wCAAwC,MAAM;CACvD,IAAI,iBAAiB,oBACnB,OAAO,4CAA4C,MAAM;CAC3D,OAAO,4CAA4C,MAAM;AAC3D"}