{"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../../../src/core/workspace/discovery.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,EAAE,KAAK,oBAAoB,EAAgB,MAAM,eAAe,CAAC;AAGxE,MAAM,WAAW,gBAAgB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,UAAU,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;CAC1C;AAED,MAAM,WAAW,cAAc;IAC9B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,oBAAoB,CAAC;CACrC;AAmBD,UAAU,UAAU;IACnB,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;CACxB;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,gBAAgB,GAAG,UAAU,CAyI5E","sourcesContent":["/**\n * Deterministic, cancellable file discovery.\n *\n * Respects .gitignore and Jensen ignore policy, stays inside the workspace\n * boundary, never traverses external symlinks or junctions, skips caches and\n * binary blobs, and returns deterministically sorted workspace-relative paths.\n */\n\nimport { type Dirent, readdirSync, readFileSync, realpathSync, type Stats, statSync } from \"node:fs\";\nimport path from \"node:path\";\nimport ignore, { type Ignore } from \"ignore\";\nimport { type ClassificationResult, classifyPath } from \"./classify.js\";\nimport { detectSymlinkEscape, isSuperficiallyBinary } from \"./guard.js\";\n\nexport interface DiscoveryOptions {\n\tcwd: string;\n\troot: string; // canonical workspace root\n\tmaxFileBytes?: number;\n\tadditionalIgnores?: string[];\n\tincludeLockfilesAsMetadata?: boolean;\n\tsignal?: AbortSignal;\n\tonProgress?: (discovered: number) => void;\n}\n\nexport interface DiscoveredFile {\n\tworkspaceRelativePath: string;\n\tabsolutePath: string;\n\tsizeBytes: number;\n\tclassification: ClassificationResult;\n}\n\ninterface IgnoreLayer {\n\tmatcher: Ignore;\n\tdir: string;\n}\n\nfunction loadGitignore(dir: string): Ignore {\n\tconst ig = ignore();\n\ttry {\n\t\tconst content = readFileSync(path.join(dir, \".gitignore\"), \"utf-8\");\n\t\tig.add(content);\n\t\tig.add([\".git/**\", \"node_modules/**\", \"dist/**\", \"build/**\", \"coverage/**\", \".next/**\"]);\n\t} catch {\n\t\tig.add([\".git/**\", \"node_modules/**\", \"dist/**\", \"build/**\", \"coverage/**\", \".next/**\"]);\n\t}\n\treturn ig;\n}\n\ninterface WalkResult {\n\tfiles: DiscoveredFile[];\n\tskippedIgnored: number;\n\tskippedBinary: number;\n\tskippedTooLarge: number;\n\tescapesDetected: number;\n}\n\nexport function discoverWorkspaceFiles(options: DiscoveryOptions): WalkResult {\n\tconst { root, maxFileBytes = 8 * 1024 * 1024 } = options;\n\tconst signal = options.signal;\n\tif (signal?.aborted) throw new Error(\"Discovery cancelled\");\n\n\tconst result: WalkResult = {\n\t\tfiles: [],\n\t\tskippedIgnored: 0,\n\t\tskippedBinary: 0,\n\t\tskippedTooLarge: 0,\n\t\tescapesDetected: 0,\n\t};\n\n\t// Root ignore layer.\n\tconst layers: IgnoreLayer[] = [{ matcher: loadGitignore(root), dir: root }];\n\n\tconst projectIgnorePath = path.join(root, \".jensenindexignore\");\n\tconst projectIgnores: string[] = [];\n\ttry {\n\t\tprojectIgnores.push(...readFileSync(projectIgnorePath, \"utf-8\").split(/\\r?\\n/));\n\t} catch {\n\t\t/* optional */\n\t}\n\tif (options.additionalIgnores) projectIgnores.push(...options.additionalIgnores);\n\tif (projectIgnores.length) {\n\t\tconst ig = ignore();\n\t\tig.add(projectIgnores);\n\t\tlayers[0].matcher.add(projectIgnores);\n\t}\n\n\tconst pending: string[] = [root];\n\twhile (pending.length > 0) {\n\t\tif (signal?.aborted) throw new Error(\"Discovery cancelled\");\n\t\tconst dir = pending.pop() as string;\n\t\tif (dir !== root) {\n\t\t\t// nested .gitignore\n\t\t\tlayers.push({ matcher: loadGitignore(dir), dir });\n\t\t}\n\t\tlet entries: Dirent[];\n\t\ttry {\n\t\t\tentries = readdirSync(dir, { withFileTypes: true });\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tentries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));\n\n\t\tfor (const entry of entries) {\n\t\t\tif (signal?.aborted) throw new Error(\"Discovery cancelled\");\n\t\t\tconst abs = path.join(dir, entry.name);\n\t\t\tconst rel = path.relative(root, abs).replace(/\\\\/g, \"/\");\n\n\t\t\t// Respect ignore layers from root downward (each scoped to its dir).\n\t\t\tlet ignoredByAny = false;\n\t\t\tif (layers[0].matcher.ignores(rel)) ignoredByAny = true;\n\t\t\tif (!ignoredByAny) {\n\t\t\t\tfor (let li = 1; li < layers.length; li++) {\n\t\t\t\t\tconst layer = layers[li];\n\t\t\t\t\tif (!abs.startsWith(layer.dir + path.sep)) continue;\n\t\t\t\t\tconst relToLayer = path.relative(layer.dir, abs).replace(/\\\\/g, \"/\");\n\t\t\t\t\tif (layer.matcher.ignores(relToLayer)) {\n\t\t\t\t\t\tignoredByAny = true;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (ignoredByAny) {\n\t\t\t\tresult.skippedIgnored++;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tlet lst: Stats;\n\t\t\ttry {\n\t\t\t\tlst = statSync(abs);\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (entry.isSymbolicLink() || (lst && !lst.isDirectory() && isLikelyJunction(abs, root))) {\n\t\t\t\t// External symlink/junction escape detection.\n\t\t\t\tconst esc = detectSymlinkEscape(root, abs);\n\t\t\t\tif (esc) {\n\t\t\t\t\tresult.escapesDetected++;\n\t\t\t\t\tcontinue; // never traverse outside workspace\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (lst.isDirectory() && !entry.isSymbolicLink()) {\n\t\t\t\tpending.push(abs);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (lst.size > maxFileBytes) {\n\t\t\t\tresult.skippedTooLarge++;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst classification = classifyPath(rel);\n\t\t\tif (classification.ignoredByVendor || classification.ignoredByCache) {\n\t\t\t\tresult.skippedIgnored++;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (classification.isBinary) {\n\t\t\t\tresult.skippedBinary++;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t// Deep binary sniff for files with text-ish extensions.\n\t\t\tif (classification.classification === \"unknown\") {\n\t\t\t\ttry {\n\t\t\t\t\tconst fd = readFileSync(abs).subarray(0, 8192);\n\t\t\t\t\tif (isSuperficiallyBinary(fd)) {\n\t\t\t\t\t\tresult.skippedBinary++;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (classification.classification === \"lockfile\" && !options.includeLockfilesAsMetadata) {\n\t\t\t\tresult.skippedIgnored++;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tresult.files.push({\n\t\t\t\tworkspaceRelativePath: rel,\n\t\t\t\tabsolutePath: abs,\n\t\t\t\tsizeBytes: lst.size,\n\t\t\t\tclassification,\n\t\t\t});\n\t\t\toptions.onProgress?.(result.files.length);\n\t\t}\n\t\tif (dir !== root) {\n\t\t\tlayers.pop();\n\t\t}\n\t}\n\n\tresult.files.sort((a, b) => (a.workspaceRelativePath < b.workspaceRelativePath ? -1 : 1));\n\treturn result;\n}\n\nfunction isLikelyJunction(abs: string, root: string): boolean {\n\tif (process.platform !== \"win32\") return false;\n\ttry {\n\t\tconst real = realpathSync(abs);\n\t\tif (!real.startsWith(path.resolve(root) + path.sep)) return true;\n\t} catch {\n\t\t/* ignore */\n\t}\n\treturn false;\n}\n"]}