{"version":3,"file":"compose-seam.cjs","names":["Effect","fileExists","readText","readFile","readFileSync","Finding"],"sources":["../../../../../src/services/scaffold/upgrade-audit/checks/compose-seam.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport { Effect } from \"effect\";\nimport { Finding } from \"../domain\";\nimport { fileExists, readText } from \"../fs\";\n\n/**\n * Compose-seam guard — replays the host's SSR composition collision locally:\n * our core ui manifest (which claims /login) composed with the deployed\n * auth-ui manifest (which ships _public/login plus a device-login subtree).\n *\n * every-plugin's constructTree handles the collision by design (\"core ui\n * wins; shadowed route skipped\") but pre-fix releases mark the shadowed\n * children without splicing them out of `pending`, so the loop exits\n * non-empty and throws a false \"unresolvable parentage\" — the prod boot\n * failure this guard watches for. Run with: pnpm compose-seam [--live]\n */\n\nexport const UI_MANIFEST_PATH = \"ui/src/manifest.gen.json\";\nexport const AUTH_FIXTURE_PATH = \"fixtures/compose-seam/auth-ui.manifest.gen.json\";\nexport const AUTH_MANIFEST_URL =\n  \"https://cdn.everything.dev/bundles/dev.everything.near/everything.dev/auth-ui/manifest.gen.json\";\nexport const AUTH_PLUGIN_KEY = \"auth\";\nexport const CORE_UI_KEY = \"ui\";\nexport const SHADOW_LOG_MARKER = \"core ui wins; shadowed route skipped\";\n\nexport interface ManifestRoute {\n  readonly id: string;\n  readonly type: \"index\" | \"layout\" | \"route\";\n  readonly path?: string;\n  readonly parentId?: string;\n  readonly mount?: string;\n}\n\nexport interface PluginManifestData {\n  readonly name: string;\n  readonly manifestVersion: number;\n  readonly routes: readonly ManifestRoute[];\n}\n\nexport type ComposeSeam =\n  | \"ok\"\n  | \"cascade-bug\"\n  | \"path-collision\"\n  | \"compose-failed\"\n  | \"absent-ui-manifest\";\n\nexport interface ComposeProbe {\n  readonly seam: ComposeSeam;\n  /** `[Compose]` lines emitted by constructTree during the probe */\n  readonly composeLog: readonly string[];\n  readonly error?: string;\n  readonly mountCounts?: Record<string, number>;\n  readonly uiClaimsLogin: boolean;\n}\n\nexport type ConstructTreeFn = (input: {\n  name: string;\n  plugins: ReadonlyArray<{ key: string; mfName: string }>;\n  resolve: (ref: { key: string; mfName: string }) => Promise<{\n    key: string;\n    manifest: PluginManifestData;\n    routeConfig: { routeConfigLoaders: Record<string, () => Promise<unknown>> };\n  }>;\n  rootOptions?: unknown;\n}) => Promise<{ mountCounts: Record<string, number> }>;\n\n/** Pure — \"unresolvable parentage\" after a shadow log is the known cascade bug. */\nexport const classifyComposeError = (\n  message: string,\n  shadowedARoute: boolean,\n): Exclude<ComposeSeam, \"ok\" | \"absent-ui-manifest\"> => {\n  if (message.includes(\"path collision:\")) return \"path-collision\";\n  if (message.includes(\"unresolvable parentage\") && shadowedARoute) return \"cascade-bug\";\n  return \"compose-failed\";\n};\n\n/** Pure — every manifest route id needs a loader or constructTree refuses. */\nexport const stubRouteConfigLoaders = (manifest: PluginManifestData) =>\n  Object.fromEntries(manifest.routes.map((record) => [record.id, async () => ({})]));\n\nexport const uiClaimsLogin = (manifest: PluginManifestData): boolean =>\n  manifest.routes.some((record) => record.id === \"_public/login\" && record.path === \"/login\");\n\nconst captureConsole = async (fn: () => Promise<{ mountCounts: Record<string, number> }>) => {\n  const lines: string[] = [];\n  const original = console.log;\n  console.log = (...args: unknown[]) => {\n    lines.push(args.map(String).join(\" \"));\n  };\n  let result: { mountCounts: Record<string, number> } | undefined;\n  let error: unknown;\n  try {\n    result = await fn();\n  } catch (cause) {\n    error = cause;\n  } finally {\n    console.log = original;\n  }\n  return { lines, result, error };\n};\n\n/**\n * Runs the real composer (or an injected stand-in) over the two manifests.\n * The auth manifest's /login tree is the collision shape; classification\n * only — never throws.\n */\nexport const runComposeProbe = async (\n  uiManifest: PluginManifestData,\n  authManifest: PluginManifestData,\n  constructTree: ConstructTreeFn,\n): Promise<ComposeProbe> => {\n  const claimsLogin = uiClaimsLogin(uiManifest);\n  const manifests = new Map<string, PluginManifestData>([\n    [CORE_UI_KEY, uiManifest],\n    [AUTH_PLUGIN_KEY, authManifest],\n  ]);\n  try {\n    const { lines, result, error } = await captureConsole(() =>\n      constructTree({\n        name: \"compose-seam-probe\",\n        plugins: [\n          { key: CORE_UI_KEY, mfName: CORE_UI_KEY },\n          { key: AUTH_PLUGIN_KEY, mfName: AUTH_PLUGIN_KEY },\n        ],\n        resolve: async (ref) => {\n          const manifest = manifests.get(ref.key);\n          if (!manifest) throw new Error(`probe has no manifest for \"${ref.key}\"`);\n          return {\n            key: ref.key,\n            manifest,\n            routeConfig: { routeConfigLoaders: stubRouteConfigLoaders(manifest) },\n          };\n        },\n      }),\n    );\n    if (error === undefined) {\n      return {\n        seam: \"ok\",\n        composeLog: lines,\n        mountCounts: result?.mountCounts,\n        uiClaimsLogin: claimsLogin,\n      };\n    }\n    const message = error instanceof Error ? error.message : String(error);\n    const shadowedARoute = lines.some((line) => line.includes(SHADOW_LOG_MARKER));\n    return {\n      seam: classifyComposeError(message, shadowedARoute),\n      composeLog: lines,\n      error: message,\n      uiClaimsLogin: claimsLogin,\n    };\n  } catch (cause) {\n    const message = cause instanceof Error ? cause.message : String(cause);\n    return {\n      seam: \"compose-failed\",\n      composeLog: [],\n      error: message,\n      uiClaimsLogin: claimsLogin,\n    };\n  }\n};\n\nconst loadManifest = (source: string, label: string): Effect.Effect<PluginManifestData, Error> =>\n  Effect.try({\n    try: () => {\n      const parsed = JSON.parse(source) as PluginManifestData;\n      if (!Array.isArray(parsed.routes) || typeof parsed.name !== \"string\") {\n        throw new Error(\"not a plugin manifest (name/routes missing)\");\n      }\n      return parsed;\n    },\n    catch: (error) => new Error(`${label}: ${String(error)}`),\n  });\n\nexport interface ProbeOptions {\n  /** fetch the deployed auth-ui manifest instead of the committed fixture */\n  readonly live?: boolean;\n}\n\n/** Reads both manifests and runs the probe; succeeds even when compose fails. */\nexport const probeComposeSeam = (\n  projectDir: string,\n  options?: ProbeOptions,\n): Effect.Effect<ComposeProbe, Error> =>\n  Effect.gen(function* () {\n    const uiExists = yield* fileExists(projectDir, UI_MANIFEST_PATH);\n    if (!uiExists) {\n      return {\n        seam: \"absent-ui-manifest\",\n        composeLog: [],\n        uiClaimsLogin: false,\n      } satisfies ComposeProbe;\n    }\n\n    const uiManifest = yield* loadManifest(\n      yield* readText(projectDir, UI_MANIFEST_PATH),\n      UI_MANIFEST_PATH,\n    );\n\n    let authSource: string;\n    if (options?.live) {\n      authSource = yield* Effect.tryPromise({\n        try: async () => {\n          const response = await fetch(AUTH_MANIFEST_URL);\n          if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);\n          return await response.text();\n        },\n        catch: (error) => new Error(`failed to fetch deployed auth-ui manifest: ${String(error)}`),\n      });\n    } else {\n      authSource = yield* Effect.tryPromise({\n        try: () => readFile(new URL(`../${AUTH_FIXTURE_PATH}`, import.meta.url), \"utf-8\"),\n        catch: (error) =>\n          new Error(`failed to load the committed auth-ui fixture: ${String(error)}`),\n      });\n    }\n    const authManifest = yield* loadManifest(authSource, \"auth-ui manifest\");\n\n    const { constructTree } = yield* Effect.tryPromise({\n      try: () => import(\"every-plugin/ui/manifest\"),\n      catch: (error) => new Error(`failed to load every-plugin's composer: ${String(error)}`),\n    });\n\n    return yield* Effect.tryPromise({\n      try: () =>\n        runComposeProbe(uiManifest, authManifest, constructTree as unknown as ConstructTreeFn),\n      catch: (error) => new Error(`compose probe failed: ${String(error)}`),\n    });\n  });\n\nexport const readEveryPluginVersion = (projectDir: string): Effect.Effect<string> =>\n  Effect.sync(() => {\n    try {\n      const source = readFileSync(`${projectDir}/node_modules/every-plugin/package.json`, \"utf-8\");\n      return (JSON.parse(source) as { version?: string }).version ?? \"unknown\";\n    } catch {\n      return \"unknown\";\n    }\n  });\n\n/** Pure — probe outcome → audit findings. */\nexport const composeFindings = (probe: ComposeProbe, everyPluginVersion: string): Finding[] => {\n  if (probe.seam === \"ok\") return [];\n  if (probe.seam === \"absent-ui-manifest\") {\n    return [\n      new Finding({\n        check: \"compose-seam\",\n        path: UI_MANIFEST_PATH,\n        severity: \"warn\",\n        message:\n          \"ui/src/manifest.gen.json is absent — run `pnpm types:gen`; the compose probe needs the generated manifest\",\n      }),\n    ];\n  }\n  const messages: Record<\"cascade-bug\" | \"path-collision\" | \"compose-failed\", string> = {\n    \"cascade-bug\":\n      `every-plugin ${everyPluginVersion} still throws a false \"unresolvable parentage\" for the /login shadow cascade ` +\n      \"(constructTree marks shadowed children without splicing them out of pending) — prod boot SSR composition fails with this. \" +\n      \"Bump everything-dev/every-plugin to the release with the cascade-splice fix, then redeploy\",\n    \"path-collision\":\n      'the composer treats the /login shadow as a hard path collision instead of \"core ui wins\" — shadow handling regressed; ' +\n      `check every-plugin ${everyPluginVersion} constructTree against the auth-ui manifest`,\n    \"compose-failed\": `compose probe failed: ${probe.error ?? \"unknown error\"}`,\n  };\n  return [\n    new Finding({\n      check: \"compose-seam\",\n      path: \"ui/src/manifest.gen.json + auth-ui manifest\",\n      severity: \"blocker\",\n      message: messages[probe.seam],\n    }),\n  ];\n};\n\nexport const checkComposeSeam = (\n  projectDir: string,\n  options?: ProbeOptions,\n): Effect.Effect<Finding[], Error> =>\n  Effect.gen(function* () {\n    const probe = yield* probeComposeSeam(projectDir, options);\n    const version = yield* readEveryPluginVersion(projectDir);\n    return composeFindings(probe, version);\n  });\n"],"mappings":"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"}