{"version":3,"file":"auto-route-CcD0OWWB.mjs","names":[],"sources":["../src/session/auto-route.ts"],"sourcesContent":["/**\n * Auto-route: automatic project routing suggestion on session start.\n *\n * Given a working directory (and optional conversation context), determine\n * which registered project the session belongs to.\n *\n * Strategy (in priority order):\n *   1. Path match   — exact or parent-directory match in the project registry\n *   2. Marker walk  — walk up from cwd looking for Notes/PAI.md, resolve slug\n *   3. Topic match  — BM25 keyword search against memory (requires context text)\n *\n * The function is stateless and works with direct DB access (no daemon\n * required), making it fast and safe to call during session startup.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport type { StorageBackend } from \"../storage/interface.js\";\nimport { resolve, dirname } from \"node:path\";\nimport { existsSync } from \"node:fs\";\nimport { readPaiMarker } from \"../registry/pai-marker.js\";\nimport { detectProject } from \"../cli/commands/detect.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type AutoRouteMethod = \"path\" | \"marker\" | \"topic\";\n\nexport interface AutoRouteResult {\n  /** Project slug */\n  slug: string;\n  /** Human-readable project name */\n  display_name: string;\n  /** Absolute path to the project root */\n  root_path: string;\n  /** How the project was detected */\n  method: AutoRouteMethod;\n  /** Confidence [0,1]: 1.0 for path/marker matches, BM25 fraction for topic */\n  confidence: number;\n}\n\n// ---------------------------------------------------------------------------\n// Core function\n// ---------------------------------------------------------------------------\n\n/**\n * Determine which project a session should be routed to.\n *\n * @param registryDb  Open PAI registry database\n * @param federation  Memory storage backend (needed only for topic fallback)\n * @param cwd         Working directory to detect from (defaults to process.cwd())\n * @param context     Optional conversation text for topic-based fallback\n * @returns           Best project match, or null if nothing matched\n */\nexport async function autoRoute(\n  registryDb: Database,\n  federation: Database | StorageBackend,\n  cwd?: string,\n  context?: string\n): Promise<AutoRouteResult | null> {\n  const target = resolve(cwd ?? process.cwd());\n\n  // -------------------------------------------------------------------------\n  // Strategy 1: Path match via registry\n  // -------------------------------------------------------------------------\n\n  const pathMatch = detectProject(registryDb, target);\n\n  if (pathMatch) {\n    return {\n      slug: pathMatch.slug,\n      display_name: pathMatch.display_name,\n      root_path: pathMatch.root_path,\n      method: \"path\",\n      confidence: 1.0,\n    };\n  }\n\n  // -------------------------------------------------------------------------\n  // Strategy 2: PAI.md marker file walk\n  //\n  // Walk up from cwd, checking <dir>/Notes/PAI.md at each level.\n  // Once found, resolve the slug against the registry to get full project info.\n  // -------------------------------------------------------------------------\n\n  const markerResult = findMarkerUpward(registryDb, target);\n  if (markerResult) {\n    return markerResult;\n  }\n\n  // -------------------------------------------------------------------------\n  // Strategy 3: Topic detection (requires context text)\n  // -------------------------------------------------------------------------\n\n  if (context && context.trim().length > 0) {\n    // Lazy import to avoid bundler pulling in daemon/index.mjs at module load time\n    const { detectTopicShift } = await import(\"../topics/detector.js\");\n    const topicResult = await detectTopicShift(registryDb, federation, {\n      context,\n      threshold: 0.5, // Lower threshold for initial routing (vs shift detection)\n    });\n\n    if (topicResult.suggestedProject && topicResult.confidence > 0) {\n      // Look up the full project info from the registry\n      const projectRow = registryDb\n        .prepare(\n          \"SELECT slug, display_name, root_path FROM projects WHERE slug = ? AND status != 'archived'\"\n        )\n        .get(topicResult.suggestedProject) as\n        | { slug: string; display_name: string; root_path: string }\n        | undefined;\n\n      if (projectRow) {\n        return {\n          slug: projectRow.slug,\n          display_name: projectRow.display_name,\n          root_path: projectRow.root_path,\n          method: \"topic\",\n          confidence: topicResult.confidence,\n        };\n      }\n    }\n  }\n\n  return null;\n}\n\n// ---------------------------------------------------------------------------\n// Marker walk helper\n// ---------------------------------------------------------------------------\n\n/**\n * Walk up the directory tree from `startDir`, checking each level for a\n * `Notes/PAI.md` file. If found, read the slug and look up the project.\n *\n * Stops at the filesystem root or after 20 levels (safety guard).\n */\nfunction findMarkerUpward(\n  registryDb: Database,\n  startDir: string\n): AutoRouteResult | null {\n  let current = startDir;\n  let depth = 0;\n\n  while (depth < 20) {\n    const markerPath = `${current}/Notes/PAI.md`;\n\n    if (existsSync(markerPath)) {\n      const marker = readPaiMarker(current);\n\n      if (marker && marker.status !== \"archived\") {\n        // Resolve slug to full project info in the registry\n        const projectRow = registryDb\n          .prepare(\n            \"SELECT slug, display_name, root_path FROM projects WHERE slug = ? AND status != 'archived'\"\n          )\n          .get(marker.slug) as\n          | { slug: string; display_name: string; root_path: string }\n          | undefined;\n\n        if (projectRow) {\n          return {\n            slug: projectRow.slug,\n            display_name: projectRow.display_name,\n            root_path: projectRow.root_path,\n            method: \"marker\",\n            confidence: 1.0,\n          };\n        }\n      }\n    }\n\n    const parent = dirname(current);\n    if (parent === current) break; // Reached filesystem root\n    current = parent;\n    depth++;\n  }\n\n  return null;\n}\n\n// ---------------------------------------------------------------------------\n// Format helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Format an AutoRouteResult as a human-readable string for CLI output.\n */\nexport function formatAutoRoute(result: AutoRouteResult): string {\n  const lines: string[] = [\n    `slug:         ${result.slug}`,\n    `display_name: ${result.display_name}`,\n    `root_path:    ${result.root_path}`,\n    `method:       ${result.method}`,\n    `confidence:   ${(result.confidence * 100).toFixed(0)}%`,\n  ];\n  return lines.join(\"\\n\");\n}\n\n/**\n * Format an AutoRouteResult as JSON for machine consumption.\n */\nexport function formatAutoRouteJson(result: AutoRouteResult): string {\n  return JSON.stringify(result, null, 2);\n}\n"],"mappings":";;;;;;;;;;;;;;;AAsDA,eAAsB,UACpB,YACA,YACA,KACA,SACiC;CACjC,MAAM,SAAS,QAAQ,OAAO,QAAQ,KAAK,CAAC;CAM5C,MAAM,YAAY,cAAc,YAAY,OAAO;AAEnD,KAAI,UACF,QAAO;EACL,MAAM,UAAU;EAChB,cAAc,UAAU;EACxB,WAAW,UAAU;EACrB,QAAQ;EACR,YAAY;EACb;CAUH,MAAM,eAAe,iBAAiB,YAAY,OAAO;AACzD,KAAI,aACF,QAAO;AAOT,KAAI,WAAW,QAAQ,MAAM,CAAC,SAAS,GAAG;EAExC,MAAM,EAAE,qBAAqB,MAAM,OAAO;EAC1C,MAAM,cAAc,MAAM,iBAAiB,YAAY,YAAY;GACjE;GACA,WAAW;GACZ,CAAC;AAEF,MAAI,YAAY,oBAAoB,YAAY,aAAa,GAAG;GAE9D,MAAM,aAAa,WAChB,QACC,6FACD,CACA,IAAI,YAAY,iBAAiB;AAIpC,OAAI,WACF,QAAO;IACL,MAAM,WAAW;IACjB,cAAc,WAAW;IACzB,WAAW,WAAW;IACtB,QAAQ;IACR,YAAY,YAAY;IACzB;;;AAKP,QAAO;;;;;;;;AAaT,SAAS,iBACP,YACA,UACwB;CACxB,IAAI,UAAU;CACd,IAAI,QAAQ;AAEZ,QAAO,QAAQ,IAAI;AAGjB,MAAI,WAFe,GAAG,QAAQ,eAEJ,EAAE;GAC1B,MAAM,SAAS,cAAc,QAAQ;AAErC,OAAI,UAAU,OAAO,WAAW,YAAY;IAE1C,MAAM,aAAa,WAChB,QACC,6FACD,CACA,IAAI,OAAO,KAAK;AAInB,QAAI,WACF,QAAO;KACL,MAAM,WAAW;KACjB,cAAc,WAAW;KACzB,WAAW,WAAW;KACtB,QAAQ;KACR,YAAY;KACb;;;EAKP,MAAM,SAAS,QAAQ,QAAQ;AAC/B,MAAI,WAAW,QAAS;AACxB,YAAU;AACV;;AAGF,QAAO;;;;;AAwBT,SAAgB,oBAAoB,QAAiC;AACnE,QAAO,KAAK,UAAU,QAAQ,MAAM,EAAE"}