@@ -163,6 +163,31 @@ function toAllowedList(values, name, registry, { required }) {
163163 return [ ...new Set ( values ) ] ;
164164}
165165
166+ /**
167+ * Omitted or empty means no status filter. Not checked against `Opportunity.STATUSES` (importing
168+ * the model here is a dependency cycle); the data-service matches the enum, so a typo matches
169+ * nothing.
170+ */
171+ function toStatusList ( statuses ) {
172+ if ( statuses === undefined || statuses === null
173+ || ( Array . isArray ( statuses ) && statuses . length === 0 ) ) {
174+ return null ;
175+ }
176+ if ( ! Array . isArray ( statuses ) ) {
177+ throw new ValidationError ( 'statuses must be an array' ) ;
178+ }
179+ const rejected = [ ] ;
180+ for ( const status of statuses ) {
181+ if ( typeof status !== 'string' || status . trim ( ) === '' ) {
182+ rejected . push ( status ) ;
183+ }
184+ }
185+ if ( rejected . length > 0 ) {
186+ throw new ValidationError ( `statuses must only contain non-empty strings (got ${ describeValue ( rejected ) } )` ) ;
187+ }
188+ return [ ...new Set ( statuses ) ] ;
189+ }
190+
166191/** Only an empty/omitted `sources` clears; a non-empty input reduced to nothing throws. */
167192function assertClearable ( sources , entityId , sourceType ) {
168193 const explicitClear = sources === undefined || sources === null
@@ -453,6 +478,8 @@ export async function copyEntityVectors(postgrestClient, {
453478 * `OPPORTUNITY_SEMANTIC_SOURCE_TYPES` (required, non-empty)
454479 * @param {string[] } [params.entityTypes] - opportunity types to narrow to, from
455480 * `OPPORTUNITY_SEMANTIC_ENTITY_TYPES`; omitted or empty searches all
481+ * @param {string[] } [params.statuses] - opportunity statuses (`Opportunity.STATUSES` values) to
482+ * narrow to, applied before `k`; omitted or empty searches all
456483 * @param {number[][] } params.vectors - the query embeddings
457484 * @param {string } params.model - the model the queries were embedded with
458485 * @param {number } params.dims - the query embedding dimension
@@ -462,12 +489,13 @@ export async function copyEntityVectors(postgrestClient, {
462489 * best-first list per input vector, in input order
463490 */
464491export async function lookupOpportunitiesByVectors ( postgrestClient , {
465- siteId, sourceTypes, entityTypes, vectors, model, dims, k = 10 , minScore = 0 ,
492+ siteId, sourceTypes, entityTypes, statuses , vectors, model, dims, k = 10 , minScore = 0 ,
466493} = { } ) {
467494 assertClient ( postgrestClient ) ;
468495 assertId ( siteId , 'siteId' ) ;
469496 const sourceTypeList = toAllowedList ( sourceTypes , 'sourceTypes' , OPPORTUNITY_SEMANTIC_SOURCE_TYPES , { required : true } ) ;
470497 const entityTypeList = toAllowedList ( entityTypes , 'entityTypes' , OPPORTUNITY_SEMANTIC_ENTITY_TYPES , { required : false } ) ;
498+ const statusList = toStatusList ( statuses ) ;
471499 assertModel ( model ) ;
472500 assertDims ( dims ) ;
473501 if ( ! Array . isArray ( vectors ) ) {
@@ -495,12 +523,19 @@ export async function lookupOpportunitiesByVectors(postgrestClient, {
495523 p_dims : dims ,
496524 p_limit : k ,
497525 p_min_score : minScore ,
526+ // Only when set: a data-service without p_statuses rejects the unknown argument.
527+ ...( statusList ? { p_statuses : statusList } : { } ) ,
498528 } ) ;
499529 if ( error ) {
500530 throw rpcError (
501531 `Failed semantic search for site ${ siteId } ` ,
502532 SEMANTIC_SEARCH_RPC ,
503- { siteId, sourceTypes : sourceTypeList , entityTypes : entityTypeList } ,
533+ {
534+ siteId,
535+ sourceTypes : sourceTypeList ,
536+ entityTypes : entityTypeList ,
537+ ...( statusList ? { statuses : statusList } : { } ) ,
538+ } ,
504539 error ,
505540 ) ;
506541 }
0 commit comments