{"version":3,"file":"index8.cjs","sources":["../src/discovery/discovery.ts"],"sourcesContent":["/**\n * Hierarchical Configuration Discovery\n * \n * Follows Cardigantime pattern: walks up directory tree finding .protokoll/\n * directories. Merges config with local taking precedence.\n * \n * Example:\n *   /home/user/projects/work/projectA/  <- CWD\n *       └── .protokoll/config.yaml     <- Highest precedence\n *   /home/user/projects/work/\n *       └── .protokoll/config.yaml     <- Work context\n *   /home/user/\n *       └── .protokoll/config.yaml     <- User defaults\n * \n * Design Note: This module is designed to be self-contained and may be\n * extracted for use in other tools (kronologi, observasjon) in the future.\n */\n\nimport * as path from 'node:path';\nimport * as fs from 'fs/promises';\nimport * as yaml from 'js-yaml';\nimport { ContextDiscoveryOptions, DiscoveredContextDir, HierarchicalContextResult } from './types';\n\n/**\n * Discover configuration directories by walking up the directory tree\n */\nexport const discoverConfigDirectories = async (\n    options: ContextDiscoveryOptions\n): Promise<DiscoveredContextDir[]> => {\n    const {\n        configDirName,\n        maxLevels = 10,\n        startingDir = process.cwd(),\n    } = options;\n\n    const discovered: DiscoveredContextDir[] = [];\n    let currentDir = path.resolve(startingDir);\n    let level = 0;\n    const visited = new Set<string>();\n\n    while (level < maxLevels) {\n        const realPath = path.resolve(currentDir);\n        if (visited.has(realPath)) break;\n        visited.add(realPath);\n\n        const configDirPath = path.join(currentDir, configDirName);\n    \n        try {\n            const stat = await fs.stat(configDirPath);\n            if (stat.isDirectory()) {\n                discovered.push({ path: configDirPath, level });\n            }\n        } catch {\n            // Directory doesn't exist, continue searching\n        }\n\n        const parentDir = path.dirname(currentDir);\n        if (parentDir === currentDir) break; // Reached root\n    \n        currentDir = parentDir;\n        level++;\n    }\n\n    return discovered;\n};\n\n/**\n * Resolve context directory path based on configuration.\n * Priority:\n * 1. Explicit contextDirectory in config.yaml\n * 2. ./context/ at repository root (sibling to .protokoll/)\n * 3. .protokoll/context/ (backward compatibility)\n * \n * @param protokollDirPath - Path to the .protokoll directory\n * @param config - Parsed config.yaml content (if exists)\n */\nconst resolveContextDirectory = async (\n    protokollDirPath: string,\n    config: Record<string, unknown> | null\n): Promise<string | null> => {\n    // Get repository root (parent of .protokoll/)\n    const repoRoot = path.dirname(protokollDirPath);\n    \n    // If config specifies a contextDirectory, use it\n    if (config && typeof config.contextDirectory === 'string') {\n        const explicitPath = path.isAbsolute(config.contextDirectory)\n            ? config.contextDirectory\n            : path.resolve(repoRoot, config.contextDirectory);\n        \n        try {\n            const stat = await fs.stat(explicitPath);\n            if (stat.isDirectory()) {\n                return explicitPath;\n            }\n        } catch {\n            // Explicit path doesn't exist, continue to defaults\n        }\n    }\n    \n    // Default: Look for ./context/ at repository root (sibling to .protokoll/)\n    const rootContextDir = path.join(repoRoot, 'context');\n    \n    try {\n        const stat = await fs.stat(rootContextDir);\n        if (stat.isDirectory()) {\n            return rootContextDir;\n        }\n    } catch {\n        // Root context doesn't exist, try fallback\n    }\n    \n    // Fallback: .protokoll/context/ (backward compatibility)\n    const legacyContextDir = path.join(protokollDirPath, 'context');\n    \n    try {\n        const stat = await fs.stat(legacyContextDir);\n        if (stat.isDirectory()) {\n            return legacyContextDir;\n        }\n    } catch {\n        // No context directory found\n    }\n    \n    return null;\n};\n\n/**\n * Load and merge hierarchical configuration\n */\nexport const loadHierarchicalConfig = async (\n    options: ContextDiscoveryOptions\n): Promise<HierarchicalContextResult> => {\n    const discoveredDirs = await discoverConfigDirectories(options);\n  \n    if (discoveredDirs.length === 0) {\n        return {\n            config: {},\n            discoveredDirs: [],\n            contextDirs: [],\n        };\n    }\n\n    // Sort by level descending (lowest precedence first)\n    const sortedDirs = [...discoveredDirs].sort((a, b) => b.level - a.level);\n  \n    const configs: Record<string, unknown>[] = [];\n    const contextDirs: string[] = [];\n  \n    for (const dir of sortedDirs) {\n        const configPath = path.join(dir.path, options.configFileName);\n        let parsedConfig: Record<string, unknown> | null = null;\n    \n        try {\n            const content = await fs.readFile(configPath, 'utf-8');\n            const parsed = yaml.load(content);\n            if (parsed && typeof parsed === 'object') {\n                parsedConfig = parsed as Record<string, unknown>;\n                configs.push(parsedConfig);\n            }\n        } catch {\n            // No config file in this directory\n        }\n    \n        // Resolve context directory using new logic\n        const contextDir = await resolveContextDirectory(dir.path, parsedConfig);\n        if (contextDir) {\n            contextDirs.push(contextDir);\n        }\n    }\n\n    // Merge configs (later entries override earlier)\n    const mergedConfig = configs.reduce(\n        (acc, curr) => deepMerge(acc, curr), \n    {} as Record<string, unknown>\n    );\n\n    return {\n        config: mergedConfig,\n        discoveredDirs,\n        contextDirs,\n    };\n};\n\n/**\n * Deep merge utility (similar to Cardigantime's implementation)\n */\nexport function deepMerge<T extends Record<string, unknown>>(target: T, source: T): T {\n    if (source === null || source === undefined) return target;\n    if (target === null || target === undefined) return source;\n  \n    if (typeof source !== 'object' || typeof target !== 'object') {\n        return source;\n    }\n  \n    if (Array.isArray(source)) {\n        return [...source] as unknown as T;\n    }\n  \n    const result = { ...target } as Record<string, unknown>;\n  \n    for (const key in source) {\n        if (Object.prototype.hasOwnProperty.call(source, key)) {\n            const targetVal = result[key];\n            const sourceVal = source[key];\n      \n            if (\n                typeof targetVal === 'object' && \n        typeof sourceVal === 'object' &&\n        targetVal !== null &&\n        sourceVal !== null &&\n        !Array.isArray(targetVal) && \n        !Array.isArray(sourceVal)\n            ) {\n                result[key] = deepMerge(\n          targetVal as Record<string, unknown>, \n          sourceVal as Record<string, unknown>\n                );\n            } else {\n                result[key] = sourceVal;\n            }\n        }\n    }\n  \n    return result as T;\n}\n\n"],"names":[],"mappings":";;;;;;;;AA0LO,SAAS,SAAA,CAA6C,QAAW,MAAA,EAAc;AAClF,EAAA,IAAI,MAAA,KAAW,IAAA,IAAQ,MAAA,KAAW,MAAA,EAAW,OAAO,MAAA;AACpD,EAAA,IAAI,MAAA,KAAW,IAAA,IAAQ,MAAA,KAAW,MAAA,EAAW,OAAO,MAAA;AAEpD,EAAA,IAAI,OAAO,MAAA,KAAW,QAAA,IAAY,OAAO,WAAW,QAAA,EAAU;AAC1D,IAAA,OAAO,MAAA;AAAA,EACX;AAEA,EAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,MAAM,CAAA,EAAG;AACvB,IAAA,OAAO,CAAC,GAAG,MAAM,CAAA;AAAA,EACrB;AAEA,EAAA,MAAM,MAAA,GAAS,EAAE,GAAG,MAAA,EAAO;AAE3B,EAAA,KAAA,MAAW,OAAO,MAAA,EAAQ;AACtB,IAAA,IAAI,OAAO,SAAA,CAAU,cAAA,CAAe,IAAA,CAAK,MAAA,EAAQ,GAAG,CAAA,EAAG;AACnD,MAAA,MAAM,SAAA,GAAY,OAAO,GAAG,CAAA;AAC5B,MAAA,MAAM,SAAA,GAAY,OAAO,GAAG,CAAA;AAE5B,MAAA,IACI,OAAO,SAAA,KAAc,QAAA,IAC7B,OAAO,SAAA,KAAc,QAAA,IACrB,cAAc,IAAA,IACd,SAAA,KAAc,QACd,CAAC,KAAA,CAAM,QAAQ,SAAS,CAAA,IACxB,CAAC,KAAA,CAAM,OAAA,CAAQ,SAAS,CAAA,EAClB;AACE,QAAA,MAAA,CAAO,GAAG,CAAA,GAAI,SAAA;AAAA,UACpB,SAAA;AAAA,UACA;AAAA,SACM;AAAA,MACJ,CAAA,MAAO;AACH,QAAA,MAAA,CAAO,GAAG,CAAA,GAAI,SAAA;AAAA,MAClB;AAAA,IACJ;AAAA,EACJ;AAEA,EAAA,OAAO,MAAA;AACX;;;;"}