{"version":3,"file":"query-feedback-CTs_jx6i.mjs","names":[],"sources":["../src/zettelkasten/query-feedback.ts"],"sourcesContent":["/**\n * Query feedback loop — persist search queries and results as markdown files.\n *\n * When memory_search or zettel_converse returns results, the query + result\n * metadata is saved to ~/.config/pai/queries/ as a markdown file with YAML\n * frontmatter. The daemon indexer picks these up on the next cycle and indexes\n * them into federation.db, creating a self-reinforcing feedback loop: past\n * queries become searchable context for future queries.\n */\n\nimport { existsSync, mkdirSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { paiHomePath, resolvePaiFile, migratePaiDir, type MigrateDirResult } from \"../config/pai-home.js\";\n\nexport interface QueryRecord {\n  /** The original query string. */\n  query: string;\n  /** Timestamp of the query. */\n  timestamp: number;\n  /** Tool that produced the result: 'memory_search' | 'zettel_converse'. */\n  source: string;\n  /** Slugs of the top result paths (for linking back). */\n  sourceSlugs: string[];\n  /** Preview of the answer/result (first 500 chars). */\n  answerPreview: string;\n  /** Number of results returned. */\n  resultCount: number;\n}\n\nfunction oldQueriesDir(): string {\n  return join(homedir(), \".config\", \"pai\", \"queries\");\n}\n\n/** Where queries live: the new PAI_HOME dir if it has anything in it or was\n *  already created, else the pre-2026-09-19 ~/.config/pai/queries (printing\n *  a one-time notice), else the new dir (the target a first write creates). */\nexport function queriesDirPath(): string {\n  return resolvePaiFile(paiHomePath(\"queries\"), [oldQueriesDir()], \"pai config migrate\");\n}\n\nexport function migrateQueriesDir(opts: { dryRun?: boolean } = {}): MigrateDirResult {\n  return migratePaiDir(paiHomePath(\"queries\"), [oldQueriesDir()], opts);\n}\n\n/**\n * Ensure the queries directory exists.\n */\nfunction ensureQueriesDir(): void {\n  const dir = paiHomePath(\"queries\");\n  if (!existsSync(dir)) {\n    mkdirSync(dir, { recursive: true });\n  }\n}\n\n/**\n * Generate a filename-safe slug from a query string.\n */\nfunction querySlug(query: string, timestamp: number): string {\n  const slug = query\n    .toLowerCase()\n    .replace(/[^a-z0-9\\s]/g, \"\")\n    .replace(/\\s+/g, \"-\")\n    .slice(0, 60);\n  const ts = new Date(timestamp).toISOString().slice(0, 10);\n  const shortHash = timestamp.toString(36).slice(-4);\n  return `${ts}-${slug}-${shortHash}`;\n}\n\n/**\n * Save a query + result record as a markdown file with YAML frontmatter.\n *\n * The file is written to PAI_HOME's queries/ dir and will be picked up by\n * the daemon indexer on the next cycle.\n */\nexport function saveQueryResult(record: QueryRecord): string | null {\n  try {\n    ensureQueriesDir();\n\n    const filename = querySlug(record.query, record.timestamp) + \".md\";\n    const filepath = join(paiHomePath(\"queries\"), filename);\n\n    // Don't overwrite if the exact file already exists\n    if (existsSync(filepath)) return filepath;\n\n    const frontmatter = [\n      \"---\",\n      `query: \"${record.query.replace(/\"/g, '\\\\\"')}\"`,\n      `timestamp: ${new Date(record.timestamp).toISOString()}`,\n      `source: ${record.source}`,\n      `result_count: ${record.resultCount}`,\n      `source_slugs:`,\n      ...record.sourceSlugs.map((s) => `  - \"${s}\"`),\n      \"---\",\n    ].join(\"\\n\");\n\n    const body = [\n      \"\",\n      `# Query: ${record.query}`,\n      \"\",\n      `**Source:** ${record.source}  `,\n      `**Date:** ${new Date(record.timestamp).toISOString().slice(0, 19).replace(\"T\", \" \")}  `,\n      `**Results:** ${record.resultCount}`,\n      \"\",\n      \"## Answer Preview\",\n      \"\",\n      record.answerPreview,\n      \"\",\n      \"## Source Paths\",\n      \"\",\n      ...record.sourceSlugs.map((s) => `- \\`${s}\\``),\n      \"\",\n    ].join(\"\\n\");\n\n    writeFileSync(filepath, frontmatter + body, \"utf8\");\n    return filepath;\n  } catch {\n    // Non-critical — don't crash the parent tool if query logging fails\n    return null;\n  }\n}\n"],"mappings":";;;;;;;;;;;;;;;AA8BA,SAAS,gBAAwB;AAC/B,QAAO,KAAK,SAAS,EAAE,WAAW,OAAO,UAAU;;;;;AAMrD,SAAgB,iBAAyB;AACvC,QAAO,eAAe,YAAY,UAAU,EAAE,CAAC,eAAe,CAAC,EAAE,qBAAqB;;AAGxF,SAAgB,kBAAkB,OAA6B,EAAE,EAAoB;AACnF,QAAO,cAAc,YAAY,UAAU,EAAE,CAAC,eAAe,CAAC,EAAE,KAAK;;;;;AAMvE,SAAS,mBAAyB;CAChC,MAAM,MAAM,YAAY,UAAU;AAClC,KAAI,CAAC,WAAW,IAAI,CAClB,WAAU,KAAK,EAAE,WAAW,MAAM,CAAC;;;;;AAOvC,SAAS,UAAU,OAAe,WAA2B;CAC3D,MAAM,OAAO,MACV,aAAa,CACb,QAAQ,gBAAgB,GAAG,CAC3B,QAAQ,QAAQ,IAAI,CACpB,MAAM,GAAG,GAAG;AAGf,QAAO,GAFI,IAAI,KAAK,UAAU,CAAC,aAAa,CAAC,MAAM,GAAG,GAAG,CAE5C,GAAG,KAAK,GADH,UAAU,SAAS,GAAG,CAAC,MAAM,GAAG;;;;;;;;AAUpD,SAAgB,gBAAgB,QAAoC;AAClE,KAAI;AACF,oBAAkB;EAElB,MAAM,WAAW,UAAU,OAAO,OAAO,OAAO,UAAU,GAAG;EAC7D,MAAM,WAAW,KAAK,YAAY,UAAU,EAAE,SAAS;AAGvD,MAAI,WAAW,SAAS,CAAE,QAAO;AA+BjC,gBAAc,UA7BM;GAClB;GACA,WAAW,OAAO,MAAM,QAAQ,MAAM,OAAM,CAAC;GAC7C,cAAc,IAAI,KAAK,OAAO,UAAU,CAAC,aAAa;GACtD,WAAW,OAAO;GAClB,iBAAiB,OAAO;GACxB;GACA,GAAG,OAAO,YAAY,KAAK,MAAM,QAAQ,EAAE,GAAG;GAC9C;GACD,CAAC,KAAK,KAAK,GAEC;GACX;GACA,YAAY,OAAO;GACnB;GACA,eAAe,OAAO,OAAO;GAC7B,aAAa,IAAI,KAAK,OAAO,UAAU,CAAC,aAAa,CAAC,MAAM,GAAG,GAAG,CAAC,QAAQ,KAAK,IAAI,CAAC;GACrF,gBAAgB,OAAO;GACvB;GACA;GACA;GACA,OAAO;GACP;GACA;GACA;GACA,GAAG,OAAO,YAAY,KAAK,MAAM,OAAO,EAAE,IAAI;GAC9C;GACD,CAAC,KAAK,KAAK,EAEgC,OAAO;AACnD,SAAO;SACD;AAEN,SAAO"}