@@ -13,6 +13,7 @@ import {
1313 calculateMatchScore ,
1414 calculateSpecScore ,
1515 containsAllTerms ,
16+ containsAnyTerm ,
1617 countOccurrences ,
1718 findMatchPositions ,
1819} from './scoring.js' ;
@@ -36,6 +37,33 @@ export interface SearchableSpec {
3637 content ?: string ;
3738}
3839
40+ /**
41+ * Check if spec contains all query terms across any combination of fields
42+ *
43+ * This enables cross-field matching: term A can be in title, term B in content
44+ *
45+ * @param spec - Spec to check
46+ * @param queryTerms - Terms that must all be present
47+ * @returns True if all terms are found somewhere in the spec, false for empty queryTerms
48+ */
49+ export function specContainsAllTerms ( spec : SearchableSpec , queryTerms : string [ ] ) : boolean {
50+ // Return false for empty query to match main search function behavior
51+ if ( queryTerms . length === 0 ) {
52+ return false ;
53+ }
54+
55+ // Combine all searchable text from the spec
56+ const allText = [
57+ spec . title || '' ,
58+ spec . name || '' ,
59+ spec . tags ?. join ( ' ' ) || '' ,
60+ spec . description || '' ,
61+ spec . content || '' ,
62+ ] . join ( ' ' ) . toLowerCase ( ) ;
63+
64+ return queryTerms . every ( term => allText . includes ( term ) ) ;
65+ }
66+
3967/**
4068 * Search specs with intelligent relevance ranking
4169 *
@@ -78,6 +106,13 @@ export function searchSpecs(
78106 const results : SearchResult [ ] = [ ] ;
79107
80108 for ( const spec of specs ) {
109+ // First check: does the spec contain all query terms (across any fields)?
110+ if ( ! specContainsAllTerms ( spec , queryTerms ) ) {
111+ continue ; // Skip specs that don't have all terms somewhere
112+ }
113+
114+ // Collect matches from fields that contain ANY query term
115+ // This provides context/highlighting even for partial field matches
81116 const matches = searchSpec ( spec , queryTerms , contextLength ) ;
82117
83118 if ( matches . length > 0 ) {
@@ -113,6 +148,9 @@ export function searchSpecs(
113148
114149/**
115150 * Search a single spec for query terms
151+ *
152+ * Returns matches from fields containing ANY query terms (for context/highlighting)
153+ * when doing cross-field search where spec-level matching is already confirmed
116154 */
117155function searchSpec (
118156 spec : SearchableSpec ,
@@ -121,8 +159,8 @@ function searchSpec(
121159) : SearchMatch [ ] {
122160 const matches : SearchMatch [ ] = [ ] ;
123161
124- // Search title
125- if ( spec . title && containsAllTerms ( spec . title , queryTerms ) ) {
162+ // Search title - include if it has ANY query terms
163+ if ( spec . title && containsAnyTerm ( spec . title , queryTerms ) ) {
126164 const occurrences = countOccurrences ( spec . title , queryTerms ) ;
127165 const highlights = findMatchPositions ( spec . title , queryTerms ) ;
128166 const score = calculateMatchScore (
@@ -141,8 +179,8 @@ function searchSpec(
141179 } ) ;
142180 }
143181
144- // Search name
145- if ( spec . name && containsAllTerms ( spec . name , queryTerms ) ) {
182+ // Search name - include if it has ANY query terms
183+ if ( spec . name && containsAnyTerm ( spec . name , queryTerms ) ) {
146184 const occurrences = countOccurrences ( spec . name , queryTerms ) ;
147185 const highlights = findMatchPositions ( spec . name , queryTerms ) ;
148186 const score = calculateMatchScore (
@@ -161,10 +199,10 @@ function searchSpec(
161199 } ) ;
162200 }
163201
164- // Search tags
202+ // Search tags - include tags that have ANY query terms
165203 if ( spec . tags && spec . tags . length > 0 ) {
166204 for ( const tag of spec . tags ) {
167- if ( containsAllTerms ( tag , queryTerms ) ) {
205+ if ( containsAnyTerm ( tag , queryTerms ) ) {
168206 const occurrences = countOccurrences ( tag , queryTerms ) ;
169207 const highlights = findMatchPositions ( tag , queryTerms ) ;
170208 const score = calculateMatchScore (
@@ -185,8 +223,8 @@ function searchSpec(
185223 }
186224 }
187225
188- // Search description
189- if ( spec . description && containsAllTerms ( spec . description , queryTerms ) ) {
226+ // Search description - include if it has ANY query terms
227+ if ( spec . description && containsAnyTerm ( spec . description , queryTerms ) ) {
190228 const occurrences = countOccurrences ( spec . description , queryTerms ) ;
191229 const highlights = findMatchPositions ( spec . description , queryTerms ) ;
192230 const score = calculateMatchScore (
@@ -220,6 +258,8 @@ function searchSpec(
220258
221259/**
222260 * Search content with context extraction
261+ *
262+ * Returns matches from lines containing ANY query terms
223263 */
224264function searchContent (
225265 content : string ,
@@ -232,7 +272,8 @@ function searchContent(
232272 for ( let i = 0 ; i < lines . length ; i ++ ) {
233273 const line = lines [ i ] ;
234274
235- if ( containsAllTerms ( line , queryTerms ) ) {
275+ // Include lines with ANY query terms (not all terms)
276+ if ( containsAnyTerm ( line , queryTerms ) ) {
236277 const occurrences = countOccurrences ( line , queryTerms ) ;
237278 const { text, highlights } = extractSmartContext (
238279 content ,
0 commit comments