@@ -5,6 +5,11 @@ import * as fs from 'node:fs/promises';
55import * as path from 'node:path' ;
66import { getLogger } from './logger' ;
77
8+ type PerLanguageData = {
9+ method ?: string ;
10+ example ?: string ;
11+ } ;
12+
813type MethodEntry = {
914 name : string ;
1015 endpoint : string ;
@@ -16,6 +21,7 @@ type MethodEntry = {
1621 params ?: string [ ] ;
1722 response ?: string ;
1823 markdown ?: string ;
24+ perLanguage ?: Record < string , PerLanguageData > ;
1925} ;
2026
2127type ProseChunk = {
@@ -807,6 +813,8 @@ const EMBEDDED_METHODS: MethodEntry[] = [
807813 } ,
808814] ;
809815
816+ const EMBEDDED_READMES : { language : string ; content : string } [ ] = [ ] ;
817+
810818const INDEX_OPTIONS = {
811819 fields : [
812820 'name' ,
@@ -821,13 +829,15 @@ const INDEX_OPTIONS = {
821829 storeFields : [ 'kind' , '_original' ] ,
822830 searchOptions : {
823831 prefix : true ,
824- fuzzy : 0.2 ,
832+ fuzzy : 0.1 ,
825833 boost : {
826- name : 3 ,
827- endpoint : 2 ,
834+ name : 5 ,
835+ stainlessPath : 3 ,
836+ endpoint : 3 ,
837+ qualified : 3 ,
828838 summary : 2 ,
829- qualified : 2 ,
830839 content : 1 ,
840+ description : 1 ,
831841 } as Record < string , number > ,
832842 } ,
833843} ;
@@ -849,30 +859,45 @@ export class LocalDocsSearch {
849859 static async create ( opts ?: { docsDir ?: string } ) : Promise < LocalDocsSearch > {
850860 const instance = new LocalDocsSearch ( ) ;
851861 instance . indexMethods ( EMBEDDED_METHODS ) ;
862+ for ( const readme of EMBEDDED_READMES ) {
863+ instance . indexProse ( readme . content , `readme:${ readme . language } ` ) ;
864+ }
852865 if ( opts ?. docsDir ) {
853866 await instance . loadDocsDirectory ( opts . docsDir ) ;
854867 }
855868 return instance ;
856869 }
857870
858- // Note: Language is accepted for interface consistency with remote search, but currently has no
859- // effect since this local search only supports TypeScript docs.
860871 search ( props : {
861872 query : string ;
862873 language ?: string ;
863874 detail ?: string ;
864875 maxResults ?: number ;
865876 maxLength ?: number ;
866877 } ) : SearchResult {
867- const { query, detail = 'default' , maxResults = 5 , maxLength = 100_000 } = props ;
878+ const { query, language = 'typescript' , detail = 'default' , maxResults = 5 , maxLength = 100_000 } = props ;
868879
869880 const useMarkdown = detail === 'verbose' || detail === 'high' ;
870881
871- // Search both indices and merge results by score
882+ // Search both indices and merge results by score.
883+ // Filter prose hits so language-tagged content (READMEs and docs with
884+ // frontmatter) only matches the requested language.
872885 const methodHits = this . methodIndex
873886 . search ( query )
874887 . map ( ( hit ) => ( { ...hit , _kind : 'http_method' as const } ) ) ;
875- const proseHits = this . proseIndex . search ( query ) . map ( ( hit ) => ( { ...hit , _kind : 'prose' as const } ) ) ;
888+ const proseHits = this . proseIndex
889+ . search ( query )
890+ . filter ( ( hit ) => {
891+ const source = ( ( hit as Record < string , unknown > ) [ '_original' ] as ProseChunk | undefined ) ?. source ;
892+ if ( ! source ) return true ;
893+ // Check for language-tagged sources: "readme:<lang>" or "lang:<lang>:<filename>"
894+ let taggedLang : string | undefined ;
895+ if ( source . startsWith ( 'readme:' ) ) taggedLang = source . slice ( 'readme:' . length ) ;
896+ else if ( source . startsWith ( 'lang:' ) ) taggedLang = source . split ( ':' ) [ 1 ] ;
897+ if ( ! taggedLang ) return true ;
898+ return taggedLang === language || ( language === 'javascript' && taggedLang === 'typescript' ) ;
899+ } )
900+ . map ( ( hit ) => ( { ...hit , _kind : 'prose' as const } ) ) ;
876901 const merged = [ ...methodHits , ...proseHits ] . sort ( ( a , b ) => b . score - a . score ) ;
877902 const top = merged . slice ( 0 , maxResults ) ;
878903
@@ -885,11 +910,16 @@ export class LocalDocsSearch {
885910 if ( useMarkdown && m . markdown ) {
886911 fullResults . push ( m . markdown ) ;
887912 } else {
913+ // Use per-language data when available, falling back to the
914+ // top-level fields (which are TypeScript-specific in the
915+ // legacy codepath).
916+ const langData = m . perLanguage ?. [ language ] ;
888917 fullResults . push ( {
889- method : m . qualified ,
918+ method : langData ?. method ?? m . qualified ,
890919 summary : m . summary ,
891920 description : m . description ,
892921 endpoint : `${ m . httpMethod . toUpperCase ( ) } ${ m . endpoint } ` ,
922+ ...( langData ?. example ? { example : langData . example } : { } ) ,
893923 ...( m . params ? { params : m . params } : { } ) ,
894924 ...( m . response ? { response : m . response } : { } ) ,
895925 } ) ;
@@ -960,7 +990,19 @@ export class LocalDocsSearch {
960990 this . indexProse ( texts . join ( '\n\n' ) , file . name ) ;
961991 }
962992 } else {
963- this . indexProse ( content , file . name ) ;
993+ // Parse optional YAML frontmatter for language tagging.
994+ // Files with a "language" field in frontmatter will only
995+ // surface in searches for that language.
996+ //
997+ // Example:
998+ // ---
999+ // language: python
1000+ // ---
1001+ // # Error handling in Python
1002+ // ...
1003+ const frontmatter = parseFrontmatter ( content ) ;
1004+ const source = frontmatter . language ? `lang:${ frontmatter . language } :${ file . name } ` : file . name ;
1005+ this . indexProse ( content , source ) ;
9641006 }
9651007 } catch ( err ) {
9661008 getLogger ( ) . warn ( { err, file : file . name } , 'Failed to index docs file' ) ;
@@ -1038,3 +1080,12 @@ function extractTexts(data: unknown, depth = 0): string[] {
10381080 }
10391081 return [ ] ;
10401082}
1083+
1084+ /** Parses YAML frontmatter from a markdown string, extracting the language field if present. */
1085+ function parseFrontmatter ( markdown : string ) : { language ?: string } {
1086+ const match = markdown . match ( / ^ - - - \n ( [ \s \S ] * ?) \n - - - / ) ;
1087+ if ( ! match ) return { } ;
1088+ const body = match [ 1 ] ?? '' ;
1089+ const langMatch = body . match ( / ^ l a n g u a g e : \s * ( .+ ) $ / m) ;
1090+ return langMatch ? { language : langMatch [ 1 ] ! . trim ( ) } : { } ;
1091+ }
0 commit comments