Skip to content

feat: add pp:<provider> keyword to force profile lookup provider#245

Closed
dergigi wants to merge 13 commits intomasterfrom
feat/pp-provider-keyword-244
Closed

feat: add pp:<provider> keyword to force profile lookup provider#245
dergigi wants to merge 13 commits intomasterfrom
feat/pp-provider-keyword-244

Conversation

@dergigi
Copy link
Copy Markdown
Owner

@dergigi dergigi commented Mar 23, 2026

Adds the pp:<provider> query keyword that forces a specific profile lookup provider for by: and mentions: resolution, addressing the issue where by:dergigi resolves to the wrong npub via relatr when logged out. Users can now write by:dergigi pp:vertex to bypass relatr entirely, or by:someone pp:relatr to force it. Valid values are vertex, relatr, and relay.

  • Extracts pp: in query parsing and threads profileProvider through SearchContext, all search strategies, OR handlers, and the full resolution chain down to tryQueryProviders
  • When pp: is set, only the specified provider is tried (no fallback cascade)
  • QueryTranslation UI shows [pp:vertex] prefix when the keyword is active

Closes #244

Summary by CodeRabbit

  • New Features
    • Add pp: to force which profile source is used in searches; previews are prefixed with [pp:] when present.
    • Profile-provider option threaded through all search flows so by:/mentions/p: resolution honors the forced provider.
  • Improvements
    • More consistent author/profile resolution and caching across query types.
    • Improved parsing for kinds, dates, NIP‑50 extensions, relay filters, and by/p token extraction for more accurate search results.

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Mar 23, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ants Ready Ready Preview, Comment Apr 2, 2026 8:13am

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 23, 2026

📝 Walkthrough

Walkthrough

Adds a pp: token to force profile lookup provider (vertex|relatr|relay), extracts it during query parsing, threads the provider through query translation, author/profile resolution, OR-expansion, and search strategy codepaths; extracts shared query/token helpers into new modules.

Changes

Cohort / File(s) Summary
Query translation & UI
src/components/QueryTranslation.tsx
Removed inline debounce/author helpers; import and use @/lib/queryTranslationHelpers. Parse and strip pp: provider, pass it into author-resolution, and prefix previews with [pp:<provider>] when present.
Query translation helpers
src/lib/queryTranslationHelpers.ts
New helpers: getAdaptiveDebounceMs, resolveByTokensInQuery, resolvePTokensInQuery to normalize by:/p: tokens and cache resolutions.
Profile provider APIs
src/lib/profile/providers.ts, src/lib/profile/resolver.ts, src/lib/profile/search.ts
Add optional forcedProvider?: string param; tryQueryProviders honors/validates it (special-case relay), and resolver/search functions thread it into profile lookups.
Query parsing & token extractors
src/lib/search/tokenExtractors.ts, src/lib/search/queryParsing.ts
New token extractors (NIP-50, date/kind, by:, profileProvider). parseSearchQuery now extracts profileProvider and returns it on ParsedQuery; downstream parsing uses the cleaned query.
Search context & plumbing
src/lib/search/types.ts, src/lib/search.ts
Add profileProvider?: string to SearchContext; searchEvents extracts and propagates it into handlers and OR/top-level handling.
OR expansion & helpers
src/lib/search/orExpansion.ts, src/lib/search/orHelpers.ts
Refactor OR helpers into new orHelpers.ts and tokenExtractors; thread profileProvider through OR handlers and re-export dedupe/profile-seed helpers.
Search strategies & term handling
src/lib/search/strategies/*, src/lib/search/authorResolve.ts, src/lib/search/termSearch.ts, src/lib/search/topLevelOr.ts
All relevant strategies and term/author-resolve utilities accept/forward profileProvider when resolving by:/mentions, parsing residuals, and calling full-text profile search.
New search helpers and tokenizers
src/lib/search/tokenExtractors.ts, src/lib/queryTranslationHelpers.ts, src/lib/search/orHelpers.ts
Added parsing helpers (NIP-50 extraction, date/kind filters, profile provider extraction) and OR/search orchestration utilities (dedupe, collect-and-filter, multi-author/tag handlers).

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant UI as QueryTranslation (UI)
  participant SearchSvc as Search (searchEvents)
  participant Resolver as ProfileResolver (resolver)
  participant Providers as ProfileProviders

  User->>UI: Enter query "GM pp:vertex by:alice"
  UI->>UI: extractProfileProvider -> cleanedQuery + profileProvider=vertex
  UI->>SearchSvc: call search(cleanedQuery, profileProvider=vertex)
  SearchSvc->>Resolver: resolveAuthorTokens(by:alice, forcedProvider=vertex)
  Resolver->>Providers: tryQueryProviders(query=alice, forcedProvider=vertex)
  Providers-->>Resolver: provider result (npub...)
  Resolver-->>SearchSvc: resolved npub
  SearchSvc-->>UI: search results
  UI-->>User: show results with "[pp:vertex]" preview
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • Ari4ka
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and specifically describes the main feature: adding the pp: keyword to force profile lookup provider selection.
Linked Issues check ✅ Passed The PR implements the primary objective from #244: adding pp: keyword to force specific profile lookup providers (vertex, relatr, relay) for by: and mentions: resolution, and shows the provider in the UI.
Out of Scope Changes check ✅ Passed All changes are scoped to implementing the pp: feature and required supporting infrastructure. No unrelated modifications are present.
Docstring Coverage ✅ Passed Docstring coverage is 89.47% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/pp-provider-keyword-244

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/lib/profile/search.ts (1)

25-36: ⚠️ Potential issue | 🟠 Major

Cache key should include forcedProvider to avoid cross-provider cache hits.

The cache key at line 31 is built from query and loggedIn, but not forcedProvider. If a user searches by:dergigi pp:vertex and then by:dergigi pp:relatr, the second query will return the cached vertex results instead of querying relatr.

🐛 Proposed fix to include forcedProvider in cache key
- const cacheKey = makeProfileSearchCacheKey(query, loggedIn);
+ const cacheKey = makeProfileSearchCacheKey(query, loggedIn) + (forcedProvider ? `:${forcedProvider}` : '');

Alternatively, modify makeProfileSearchCacheKey to accept and incorporate the provider:

- const cacheKey = makeProfileSearchCacheKey(query, loggedIn);
+ const cacheKey = makeProfileSearchCacheKey(query, loggedIn, forcedProvider);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/profile/search.ts` around lines 25 - 36, The cache key built in
searchProfilesFullText (variable cacheKey) omits the forcedProvider, causing
cross-provider cache hits; update the code to incorporate forcedProvider into
the key (either by passing forcedProvider into makeProfileSearchCacheKey or by
concatenating it with query/loggedIn when building cacheKey) so that
getCachedProfileSearch and subsequent cache slices are provider-specific; ensure
the same change is applied wherever makeProfileSearchCacheKey is used so cache
generation and lookups remain consistent.
src/lib/search/orExpansion.ts (2)

115-119: ⚠️ Potential issue | 🟠 Major

pp: override is dropped in the parenthesized-OR fallback path.

When execution reaches Line 115, searchByAnyTerms(...) is called without profileProvider, so forced provider selection is silently ignored for this branch.

💡 Minimal fix
   const seedResults = await searchByAnyTerms(
     translatedSeeds, Math.max(limit, 500), nip50RelaySet, abortSignal,
     nip50Extensions, applyDateFilter({ kinds: effectiveKinds }, dateFilter),
-    () => Promise.resolve(broadRelaySet)
+    () => Promise.resolve(broadRelaySet), profileProvider
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/search/orExpansion.ts` around lines 115 - 119, The parenthesized-OR
fallback call to searchByAnyTerms (the seedResults assignment) omits the
profileProvider override (the "pp" forced provider), so forced provider
selection is ignored for that path; update the searchByAnyTerms(...) call that
produces seedResults to include the profileProvider/pp parameter (same
identifier used elsewhere in this module) in its argument list and pass it
through to searchByAnyTerms, ensuring the parenthesized-OR fallback uses the
same provider override as the other branches.

125-220: 🛠️ Refactor suggestion | 🟠 Major

This file also exceeds the 210-line limit and should be split.

Current size is 220 lines (Line 220). Please extract one of the specialized handlers (handleSameContentMultiAuthor / handleTagAuthorCombination / profile-seed flow) into a dedicated module.

As per coding guidelines: **/*.{js,ts,tsx,jsx,py,java,cs,cpp,c,go,rb,php,swift,kt,rs,scala,groovy,clj,edn,sql,yaml,yml,json,xml,html,css,scss,less,vue,svelte}: Keep files below 210 lines.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/search/orExpansion.ts` around lines 125 - 220, This file exceeds the
210-line limit; extract one specialized handler (recommended:
handleSameContentMultiAuthor or handleTagAuthorCombination) into its own module:
move the full async function (including its helper usage of
applySimpleReplacements, extractResidual, resolveAuthorTokens,
buildSearchQueryWithExtensions and the call to collectAndFilter) into a new
file, export it (e.g., export async function handleSameContentMultiAuthor(...)
{}), update the original file to import that handler and remove the moved
function, and ensure shared helpers (extractResidual, collectAndFilter,
resolveAuthorTokens, buildSearchQueryWithExtensions, Nip50Extensions/NDKRelaySet
types) are either exported from this file or imported from their original
modules so the new module compiles; run a quick search to replace any local
references to the moved function name to the new import.
src/lib/search/queryParsing.ts (1)

155-223: 🛠️ Refactor suggestion | 🟠 Major

Split this module to satisfy the max-file-length rule.

This file is now 225 lines (Line 225), exceeding the 210-line cap. Please extract part of the parsing helpers (e.g., token extractors) into a sibling module.

As per coding guidelines: **/*.{js,ts,tsx,jsx,py,java,cs,cpp,c,go,rb,php,swift,kt,rs,scala,groovy,clj,edn,sql,yaml,yml,json,xml,html,css,scss,less,vue,svelte}: Keep files below 210 lines.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/search/queryParsing.ts` around lines 155 - 223, The file is too
long—extract the small parsing helper functions into a sibling module and import
them back so parseSearchQuery stays under 210 lines: move token/extractor logic
(for example extractProfileProvider, extractKindFilter, extractDateFilter,
resolveRelativeDates, parseOrQuery and any tiny helpers used only for token
extraction) into a new module, export those functions, leave the ParsedQuery
interface and parseSearchQuery implementation in the current file, then replace
the local definitions with imports and update all references (profileProvider,
kindExtraction, dateExtraction, dateResolvedQuery, topLevelOrParts) to use the
imported symbols; ensure exports/imports preserve types and runtime behavior.
🧹 Nitpick comments (2)
src/lib/profile/providers.ts (1)

100-104: Consider validating forcedProvider and clarifying pp:relay behavior.

The forcedProvider string is cast directly to ProfileLookupProvider without validation. If an invalid value is passed (e.g., pp:foo), it will silently fall through and return null. Additionally, pp:relay would immediately hit line 104's break and return null from tryQueryProviders, which might be confusing since relay is listed as a valid provider in the PR description.

♻️ Proposed fix to validate forcedProvider
 export async function tryQueryProviders(
   query: string,
   limit: number,
   loggedIn: boolean,
   forcedProvider?: string
 ): Promise<NDKEvent[] | null> {
+  const validProviders: ProfileLookupProvider[] = ['vertex', 'relatr', 'relay'];
+  if (forcedProvider && !validProviders.includes(forcedProvider as ProfileLookupProvider)) {
+    console.warn(`Invalid profile provider: ${forcedProvider}, ignoring`);
+    forcedProvider = undefined;
+  }
   const providerOrder = forcedProvider
     ? [forcedProvider as ProfileLookupProvider]
     : getProfileLookupProviderOrder(loggedIn);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/profile/providers.ts` around lines 100 - 104, Validate the
forcedProvider value before casting and clarify relay behavior: in
tryQueryProviders, check that forcedProvider (the incoming string) is one of the
allowed ProfileLookupProvider values (use the same set used by
getProfileLookupProviderOrder) and return a clear error or fallback when it is
invalid instead of silently casting; additionally, handle the 'relay' provider
explicitly so that a forced 'pp:relay' does not immediately break out and return
null—either allow relay to be processed or document/throw when relay is
unsupported—update the code paths around providerOrder, forcedProvider, and the
break on 'relay' to implement these checks and consistent behavior.
src/components/QueryTranslation.tsx (1)

1-319: File exceeds the 210-line limit.

At 319 lines, this file exceeds the coding guideline limit. Consider extracting helper functions (e.g., resolveByTokensInQuery, resolvePTokensInQuery, getAdaptiveDebounceMs) into a separate utility module.

As per coding guidelines: **/*.{js,ts,tsx,jsx,...}: Keep files below 210 lines.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/QueryTranslation.tsx` around lines 1 - 319, This file is too
long (319 lines) — split out reusable helpers to reduce length: move
getAdaptiveDebounceMs, resolveByTokensInQuery, resolvePTokensInQuery (and any
small helpers used only by generateTranslation) into a new utility module and
export them; in QueryTranslation.tsx import those helpers and call them (keep
generateTranslation in the component but delegate author/p-token resolution and
debounce calculation), ensure the authorResolutionCache ref is still passed or
accessed as needed (or provide a small adapter wrapper around
resolveByTokensInQuery to accept the cache and ppProvider), and update any type
signatures/exports so TypeScript imports compile. Ensure no behavioral changes
(preserve async await usage, skipAuthorResolution flag, and
nip19/getStoredPubkey calls) and remove the moved code from the component to
bring the file under 210 lines.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib/search/termSearch.ts`:
- Around line 24-25: Call sites of searchByAnyTerms are not passing the new
parameters, causing author (by:) and profile provider (pp:) modifiers to be
ignored; update the calls that invoke searchByAnyTerms in the OR expansion and
author fallback to forward the missing fallbackRelaySetFactory and
profileProvider arguments. Specifically, in the call site in orExpansion (the
invocation that currently passes 7 args) add the profileProvider argument as the
final parameter, and in the authorSearchStrategy fallback call (the invocation
with 6 args) pass both fallbackRelaySetFactory and profileProvider as the last
two arguments so the searchByAnyTerms signature (including
fallbackRelaySetFactory?: () => Promise<NDKRelaySet, profileProvider?: string)
is satisfied.

---

Outside diff comments:
In `@src/lib/profile/search.ts`:
- Around line 25-36: The cache key built in searchProfilesFullText (variable
cacheKey) omits the forcedProvider, causing cross-provider cache hits; update
the code to incorporate forcedProvider into the key (either by passing
forcedProvider into makeProfileSearchCacheKey or by concatenating it with
query/loggedIn when building cacheKey) so that getCachedProfileSearch and
subsequent cache slices are provider-specific; ensure the same change is applied
wherever makeProfileSearchCacheKey is used so cache generation and lookups
remain consistent.

In `@src/lib/search/orExpansion.ts`:
- Around line 115-119: The parenthesized-OR fallback call to searchByAnyTerms
(the seedResults assignment) omits the profileProvider override (the "pp" forced
provider), so forced provider selection is ignored for that path; update the
searchByAnyTerms(...) call that produces seedResults to include the
profileProvider/pp parameter (same identifier used elsewhere in this module) in
its argument list and pass it through to searchByAnyTerms, ensuring the
parenthesized-OR fallback uses the same provider override as the other branches.
- Around line 125-220: This file exceeds the 210-line limit; extract one
specialized handler (recommended: handleSameContentMultiAuthor or
handleTagAuthorCombination) into its own module: move the full async function
(including its helper usage of applySimpleReplacements, extractResidual,
resolveAuthorTokens, buildSearchQueryWithExtensions and the call to
collectAndFilter) into a new file, export it (e.g., export async function
handleSameContentMultiAuthor(...) {}), update the original file to import that
handler and remove the moved function, and ensure shared helpers
(extractResidual, collectAndFilter, resolveAuthorTokens,
buildSearchQueryWithExtensions, Nip50Extensions/NDKRelaySet types) are either
exported from this file or imported from their original modules so the new
module compiles; run a quick search to replace any local references to the moved
function name to the new import.

In `@src/lib/search/queryParsing.ts`:
- Around line 155-223: The file is too long—extract the small parsing helper
functions into a sibling module and import them back so parseSearchQuery stays
under 210 lines: move token/extractor logic (for example extractProfileProvider,
extractKindFilter, extractDateFilter, resolveRelativeDates, parseOrQuery and any
tiny helpers used only for token extraction) into a new module, export those
functions, leave the ParsedQuery interface and parseSearchQuery implementation
in the current file, then replace the local definitions with imports and update
all references (profileProvider, kindExtraction, dateExtraction,
dateResolvedQuery, topLevelOrParts) to use the imported symbols; ensure
exports/imports preserve types and runtime behavior.

---

Nitpick comments:
In `@src/components/QueryTranslation.tsx`:
- Around line 1-319: This file is too long (319 lines) — split out reusable
helpers to reduce length: move getAdaptiveDebounceMs, resolveByTokensInQuery,
resolvePTokensInQuery (and any small helpers used only by generateTranslation)
into a new utility module and export them; in QueryTranslation.tsx import those
helpers and call them (keep generateTranslation in the component but delegate
author/p-token resolution and debounce calculation), ensure the
authorResolutionCache ref is still passed or accessed as needed (or provide a
small adapter wrapper around resolveByTokensInQuery to accept the cache and
ppProvider), and update any type signatures/exports so TypeScript imports
compile. Ensure no behavioral changes (preserve async await usage,
skipAuthorResolution flag, and nip19/getStoredPubkey calls) and remove the moved
code from the component to bring the file under 210 lines.

In `@src/lib/profile/providers.ts`:
- Around line 100-104: Validate the forcedProvider value before casting and
clarify relay behavior: in tryQueryProviders, check that forcedProvider (the
incoming string) is one of the allowed ProfileLookupProvider values (use the
same set used by getProfileLookupProviderOrder) and return a clear error or
fallback when it is invalid instead of silently casting; additionally, handle
the 'relay' provider explicitly so that a forced 'pp:relay' does not immediately
break out and return null—either allow relay to be processed or document/throw
when relay is unsupported—update the code paths around providerOrder,
forcedProvider, and the break on 'relay' to implement these checks and
consistent behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f56b806d-313a-423c-b208-7eb786c4714a

📥 Commits

Reviewing files that changed from the base of the PR and between 27558eb and d85c2f6.

📒 Files selected for processing (19)
  • src/components/QueryTranslation.tsx
  • src/lib/profile/providers.ts
  • src/lib/profile/resolver.ts
  • src/lib/profile/search.ts
  • src/lib/search.ts
  • src/lib/search/authorResolve.ts
  • src/lib/search/orExpansion.ts
  • src/lib/search/queryParsing.ts
  • src/lib/search/strategies/authorSearchStrategy.ts
  • src/lib/search/strategies/dTagSearchStrategy.ts
  • src/lib/search/strategies/linkSearchStrategy.ts
  • src/lib/search/strategies/mentionsSearchStrategy.ts
  • src/lib/search/strategies/profileSearchStrategy.ts
  • src/lib/search/strategies/refSearchStrategy.ts
  • src/lib/search/strategies/replySearchStrategy.ts
  • src/lib/search/strategies/strategyUtils.ts
  • src/lib/search/termSearch.ts
  • src/lib/search/topLevelOr.ts
  • src/lib/search/types.ts

Comment on lines +24 to +25
fallbackRelaySetFactory?: () => Promise<NDKRelaySet>,
profileProvider?: string
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== All searchByAnyTerms call sites =="
rg -nP --type=ts -C2 '\bsearchByAnyTerms\s*\('

echo
echo "== Calls with 7 args (likely missing profileProvider after signature change) =="
ast-grep --lang ts --pattern 'searchByAnyTerms($_, $_, $_, $_, $_, $_, $_)' || true

echo
echo "== Calls with 8 args (includes profileProvider) =="
ast-grep --lang ts --pattern 'searchByAnyTerms($_, $_, $_, $_, $_, $_, $_, $_)' || true

Repository: dergigi/ants

Length of output: 2682


🏁 Script executed:

#!/bin/bash
# Get full context of the authorSearchStrategy.ts call
sed -n '130,140p' src/lib/search/strategies/authorSearchStrategy.ts

Repository: dergigi/ants

Length of output: 451


Three searchByAnyTerms call sites have incomplete parameter propagation.

The signature now requires profileProvider (and fallbackRelaySetFactory), but three call sites are missing one or both parameters:

  1. orExpansion.ts:115 — Missing profileProvider (7 args instead of 8)
  2. authorSearchStrategy.ts:132 — Missing both fallbackRelaySetFactory and profileProvider (6 args instead of 8)
  3. topLevelOr.ts:50 — Correct (includes profileProvider)

This causes by: (author filter) and pp: (profile provider) modifiers to silently fail in the OR expansion and author search fallback paths.

Required fixes

orExpansion.ts (line 115):

  const seedResults = await searchByAnyTerms(
    translatedSeeds, Math.max(limit, 500), nip50RelaySet, abortSignal,
    nip50Extensions, applyDateFilter({ kinds: effectiveKinds }, dateFilter),
-   () => Promise.resolve(broadRelaySet)
+   () => Promise.resolve(broadRelaySet), profileProvider
  );

authorSearchStrategy.ts (line 132):

  const seeded = await searchByAnyTerms(
    seedTerms, limit, authorRelaySet, abortSignal, nip50Extensions,
-   applyDateFilter({ authors: pubkeys, kinds: effectiveKinds }, dateFilter)
+   applyDateFilter({ authors: pubkeys, kinds: effectiveKinds }, dateFilter),
+   () => Promise.resolve(authorRelaySet), profileProvider
  );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fallbackRelaySetFactory?: () => Promise<NDKRelaySet>,
profileProvider?: string
const seedResults = await searchByAnyTerms(
translatedSeeds, Math.max(limit, 500), nip50RelaySet, abortSignal,
nip50Extensions, applyDateFilter({ kinds: effectiveKinds }, dateFilter),
() => Promise.resolve(broadRelaySet), profileProvider
);
Suggested change
fallbackRelaySetFactory?: () => Promise<NDKRelaySet>,
profileProvider?: string
const seeded = await searchByAnyTerms(
seedTerms, limit, authorRelaySet, abortSignal, nip50Extensions,
applyDateFilter({ authors: pubkeys, kinds: effectiveKinds }, dateFilter),
() => Promise.resolve(authorRelaySet), profileProvider
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/search/termSearch.ts` around lines 24 - 25, Call sites of
searchByAnyTerms are not passing the new parameters, causing author (by:) and
profile provider (pp:) modifiers to be ignored; update the calls that invoke
searchByAnyTerms in the OR expansion and author fallback to forward the missing
fallbackRelaySetFactory and profileProvider arguments. Specifically, in the call
site in orExpansion (the invocation that currently passes 7 args) add the
profileProvider argument as the final parameter, and in the authorSearchStrategy
fallback call (the invocation with 6 args) pass both fallbackRelaySetFactory and
profileProvider as the last two arguments so the searchByAnyTerms signature
(including fallbackRelaySetFactory?: () => Promise<NDKRelaySet,
profileProvider?: string) is satisfied.

dergigi added 4 commits March 23, 2026 16:21
Extract token extractors from queryParsing.ts into tokenExtractors.ts,
OR handler helpers from orExpansion.ts into orHelpers.ts, and
query translation helpers from QueryTranslation.tsx into
queryTranslationHelpers.ts.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/QueryTranslation.tsx`:
- Around line 88-99: The branch that triggers generateTranslation and the
delayed onAuthorResolved logic only checks query.includes('by:') and misses
mixed-case tokens; change the detection to match the resolver's case-insensitive
behavior by using a case-insensitive test (e.g. /by:/i.test(query)) or call the
existing resolveByTokensInQuery(query) predicate before calling
generateTranslation; keep the rest of the logic (generateTranslation,
setTranslation, resolutionTimeoutRef, lastResolvedQueryRef,
hasTriggeredSearchRef, onAuthorResolved) unchanged so the resolved preview and
delayed callback fire for BY:, By:, by:, etc.

In `@src/lib/queryTranslationHelpers.ts`:
- Around line 54-63: The authorResolutionCache is currently keyed only by the
author identifier (core) and ignores ppProvider, causing cross-provider cache
pollution; update the logic in the block that uses
skipAuthorResolution/core/ppProvider/authorResolutionCache/resolveAuthorToNpub
so the cache is keyed by both provider and author (e.g. compute a composite key
from ppProvider and core or use a nested Map), then use that composite key in
the authorResolutionCache.has(...), .get(...), and .set(...) calls so resolved
npubs are stored and retrieved per provider.

In `@src/lib/search/orHelpers.ts`:
- Around line 35-39: The extractResidual function currently only strips kind:
and hashtag tokens, so add removal of author/date modifiers (e.g., tokens
matching /\bby:[^\s]+/gi, /\bsince:[^\s]+/gi, and /\buntil:[^\s]+/gi) before
collapsing whitespace so residual never contains by:/since:/until:; update
extractResidual (and ensure normalizeResidualSearchText is still used when
normalize=true) so downstream code that assigns filter.search receives an empty
string for author-only/date-only queries and NIP-50 parsing stays correct.

In `@src/lib/search/tokenExtractors.ts`:
- Around line 63-74: The kind: parser currently uses kindRegex and
cleaned.replace to capture ([0-9,\s]+), which swallows space-separated numbers
(e.g., "kind:1 2024") and removes the following token from the residual text;
tighten the regex to only accept comma-separated numbers (allowing optional
spaces around commas) — e.g., replace the pattern in kindRegex with one matching
\d+(?:\s*,\s*\d+)* so the capture only accepts comma-separated numeric lists
(keep the same replacement logic that pushes parsed ints into kinds in the
cleaned.replace callback).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b1b161e5-a583-4ead-9b0a-5f602e78bfb6

📥 Commits

Reviewing files that changed from the base of the PR and between d85c2f6 and 5a219b3.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (9)
  • src/components/QueryTranslation.tsx
  • src/lib/profile/providers.ts
  • src/lib/profile/search.ts
  • src/lib/queryTranslationHelpers.ts
  • src/lib/search/orExpansion.ts
  • src/lib/search/orHelpers.ts
  • src/lib/search/queryParsing.ts
  • src/lib/search/strategies/authorSearchStrategy.ts
  • src/lib/search/tokenExtractors.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/lib/search/strategies/authorSearchStrategy.ts
  • src/lib/profile/search.ts
  • src/lib/search/queryParsing.ts
  • src/lib/search/orExpansion.ts
  • src/lib/profile/providers.ts

Comment thread src/components/QueryTranslation.tsx Outdated
Comment thread src/lib/queryTranslationHelpers.ts
Comment thread src/lib/search/orHelpers.ts Outdated
Comment thread src/lib/search/tokenExtractors.ts Outdated
- Case-insensitive by: detection in QueryTranslation
- Key author resolution cache by provider to prevent cross-provider pollution
- Strip by:/since:/until: from extractResidual to prevent NIP-50 leakage
- Tighten kind: regex to comma-separated lists only
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/lib/search/orHelpers.ts (1)

88-95: ⚠️ Potential issue | 🟠 Major

Normalize the residual before deciding the relay set.

Line 88 uses the raw residual. For OR-expanded queries that are only structured filters (by:, tags, since:/until:, kind:), stripping those tokens still leaves boolean scaffolding like OR / ( OR ), so Lines 89-94 make filter.search truthy and incorrectly switch the query onto nip50RelaySet. Those queries should stay on broadRelaySet.

🔧 Proposed fix
-  const residual = extractResidual(preprocessed);
+  const residual = extractResidual(preprocessed, true);

Based on learnings, NIP-50 relays are only for queries with an actual search term; author/hashtag/tag-only structured queries must stay on the broad relay set.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/search/orHelpers.ts` around lines 88 - 95, extractResidual may return
only OR/parenthesis boolean scaffolding for OR-expanded structured-only queries,
causing filter.search to be truthy and the code to pick nip50RelaySet
incorrectly; update the decision to normalize and validate the residual before
toggling relays: after calling extractResidual(preprocessed) compute a
normalizedResidual that strips boolean operators and parentheses (or run a
predicate like hasActualSearchTerm(normalizedResidual) which returns true only
if the residual contains a non-structured search token, not just tokens like
"by:", tag tokens, "since:", "until:", "kind:", "OR", "(" , ")"), then set
filter.search = nip50Extensions ?
buildSearchQueryWithExtensions(normalizedResidual, nip50Extensions) :
normalizedResidual and choose relaySet = filter.search ? nip50RelaySet :
broadRelaySet only when hasActualSearchTerm is true; otherwise leave
filter.search empty so collectAndFilter uses broadRelaySet (affecting symbols:
extractResidual, filter.search, buildSearchQueryWithExtensions, nip50RelaySet,
broadRelaySet, collectAndFilter).
🧹 Nitpick comments (2)
src/lib/search/tokenExtractors.ts (1)

111-118: Narrow profileProvider to a literal union type.

The runtime guard is good, but returning string weakens type safety downstream. Prefer 'vertex' | 'relatr' | 'relay' in the return contract.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/search/tokenExtractors.ts` around lines 111 - 118, The
extractProfileProvider function returns profileProvider as a plain string which
weakens type safety; change its return type to { cleaned: string;
profileProvider?: 'vertex' | 'relatr' | 'relay' } and narrow the runtime value
accordingly in the function body (e.g., derive provider as a string then
map/validate it into the union or use a type guard/const lookup) so the returned
profileProvider is strictly one of 'vertex' | 'relatr' | 'relay' when present;
keep the cleaned logic the same and ensure TypeScript infers the literal union
for callers.
src/lib/search/orHelpers.ts (1)

9-9: Break the orHelpersorExpansion import cycle.

orHelpers.ts (line 9) imports extractByTokens from ./orExpansion, while orExpansion.ts (line 14) imports from ./orHelpers and re-exports dedupeEvents and handleProfileSeeds from it. Create a neutral tokenExtractors.ts module for extractByTokens and import it from both files to eliminate this circular dependency and prevent regressions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/search/orHelpers.ts` at line 9, Create a new neutral module
tokenExtractors.ts that exports the extractByTokens function, then remove the
import of extractByTokens from orHelpers.ts and orExpansion.ts and have both
modules import extractByTokens from tokenExtractors.ts; update orExpansion.ts to
continue re-exporting dedupeEvents and handleProfileSeeds from orHelpers but
without causing a cycle (ensure orHelpers.ts imports extractByTokens from
tokenExtractors.ts instead of ./orExpansion and orExpansion.ts imports
extractByTokens from tokenExtractors.ts as well).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/lib/search/orHelpers.ts`:
- Around line 88-95: extractResidual may return only OR/parenthesis boolean
scaffolding for OR-expanded structured-only queries, causing filter.search to be
truthy and the code to pick nip50RelaySet incorrectly; update the decision to
normalize and validate the residual before toggling relays: after calling
extractResidual(preprocessed) compute a normalizedResidual that strips boolean
operators and parentheses (or run a predicate like
hasActualSearchTerm(normalizedResidual) which returns true only if the residual
contains a non-structured search token, not just tokens like "by:", tag tokens,
"since:", "until:", "kind:", "OR", "(" , ")"), then set filter.search =
nip50Extensions ? buildSearchQueryWithExtensions(normalizedResidual,
nip50Extensions) : normalizedResidual and choose relaySet = filter.search ?
nip50RelaySet : broadRelaySet only when hasActualSearchTerm is true; otherwise
leave filter.search empty so collectAndFilter uses broadRelaySet (affecting
symbols: extractResidual, filter.search, buildSearchQueryWithExtensions,
nip50RelaySet, broadRelaySet, collectAndFilter).

---

Nitpick comments:
In `@src/lib/search/orHelpers.ts`:
- Line 9: Create a new neutral module tokenExtractors.ts that exports the
extractByTokens function, then remove the import of extractByTokens from
orHelpers.ts and orExpansion.ts and have both modules import extractByTokens
from tokenExtractors.ts; update orExpansion.ts to continue re-exporting
dedupeEvents and handleProfileSeeds from orHelpers but without causing a cycle
(ensure orHelpers.ts imports extractByTokens from tokenExtractors.ts instead of
./orExpansion and orExpansion.ts imports extractByTokens from tokenExtractors.ts
as well).

In `@src/lib/search/tokenExtractors.ts`:
- Around line 111-118: The extractProfileProvider function returns
profileProvider as a plain string which weakens type safety; change its return
type to { cleaned: string; profileProvider?: 'vertex' | 'relatr' | 'relay' } and
narrow the runtime value accordingly in the function body (e.g., derive provider
as a string then map/validate it into the union or use a type guard/const
lookup) so the returned profileProvider is strictly one of 'vertex' | 'relatr' |
'relay' when present; keep the cleaned logic the same and ensure TypeScript
infers the literal union for callers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9f514a0a-2f35-4c7a-9002-ba5d688ff7e1

📥 Commits

Reviewing files that changed from the base of the PR and between 5a219b3 and 0b7d7fa.

📒 Files selected for processing (4)
  • src/components/QueryTranslation.tsx
  • src/lib/queryTranslationHelpers.ts
  • src/lib/search/orHelpers.ts
  • src/lib/search/tokenExtractors.ts
✅ Files skipped from review due to trivial changes (1)
  • src/lib/queryTranslationHelpers.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/QueryTranslation.tsx

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib/search/tokenExtractors.ts`:
- Around line 121-129: The function extractProfileProvider currently uses
regex.exec to pick the first pp: token but then removes all pp: tokens (global
regex), causing inconsistent behavior; change it to enumerate all matches (e.g.,
via rawQuery.matchAll or a loop over regex.exec with the global flag) and select
the last matched provider (to implement last-wins semantics consistent with
other modifiers), then keep the global replace to strip all pp: tokens from the
cleaned string; update references inside extractProfileProvider to use the last
match's group value when setting profileProvider.
- Around line 85-100: dateToTimestamp currently constructs a Date from a string
but doesn't verify the Date matches the original y/m/d parts (so inputs like
since:2026-02-31 silently normalize); change dateToTimestamp to parse year,
month, day from the input, construct a Date using Date.UTC(year, month-1, day),
then verify date.getUTCFullYear()==year, date.getUTCMonth()+1==month and
date.getUTCDate()==day; if the check fails return null (or NaN) instead of a
timestamp. Update the sinceRegex and untilRegex replace callbacks to check the
dateToTimestamp result and only assign to since/until when a valid timestamp is
returned; if invalid, do not set the filter value (and continue to strip/replace
the token the same way or leave it as a no-op per existing behavior). Ensure you
reference dateToTimestamp, sinceRegex and untilRegex when making changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8c045cfb-57e2-4c41-bd0e-da579f2ca18c

📥 Commits

Reviewing files that changed from the base of the PR and between 0b7d7fa and 2187a38.

📒 Files selected for processing (3)
  • src/lib/search/orExpansion.ts
  • src/lib/search/orHelpers.ts
  • src/lib/search/tokenExtractors.ts
✅ Files skipped from review due to trivial changes (1)
  • src/lib/search/orHelpers.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/lib/search/orExpansion.ts

Comment on lines +85 to +100
const dateToTimestamp = (dateStr: string): number => {
const date = new Date(dateStr + 'T00:00:00Z');
return Math.floor(date.getTime() / 1000);
};

const sinceRegex = /(?:^|\s)since:([0-9]{4}-[0-9]{2}-[0-9]{2})(?=\s|$)/gi;
cleaned = cleaned.replace(sinceRegex, (_, dateStr: string) => {
since = dateToTimestamp(dateStr);
return ' ';
});

const untilRegex = /(?:^|\s)until:([0-9]{4}-[0-9]{2}-[0-9]{2})(?=\s|$)/gi;
cleaned = cleaned.replace(untilRegex, (_, dateStr: string) => {
const startOfDay = dateToTimestamp(dateStr);
until = startOfDay + 86399;
return ' ';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

❓ Verification inconclusive

How does JavaScript new Date('2026-02-31T00:00:00Z') behave in V8/Node, and what is the recommended way to validate user-entered ISO calendar dates before converting them to Unix timestamps?


In V8 (used by Node.js), new Date('2026-02-31T00:00:00Z') parses successfully and normalizes to March 3, 2026 00:00:00 UTC (2026-03-03T00:00:00.000Z), as V8's date parser allows day values up to 31 and performs overflow normalization similar to the component constructor (e.g., new Date(2026, 1, 31)). This is implementation-defined behavior per ECMAScript spec for ISO strings with invalid calendar dates; Firefox returns Invalid Date (NaN), but V8/Node does not. The recommended way to validate user-entered ISO calendar dates before converting to Unix timestamps (Date.getTime) is to parse the string into components, construct a Date using those components (which normalizes), then verify the getters match the original parsed day/month/year: function isValidISODate(isoString) { const date = new Date(isoString); if (isNaN(date.getTime)) return false; // Parse YYYY-MM-DD from ISO const [, year, month, day] = isoString.match(/^(\d{4})-(\d{2})-(\d{2})/) || []; if (!year) return false; return date.getUTCFullYear == year && date.getUTCMonth + 1 == month && date.getUTCDate == day; } // Usage: if (isValidISODate('2026-02-31T00:00:00Z')) { const timestamp = new Date('2026-02-31T00:00:00Z').getTime; } else { // Invalid calendar date } This catches normalization (e.g., Feb 31 becomes Mar 3) by comparing reconstructed components to originals using UTC getters (since 'Z' specifies UTC). For non-UTC, adjust to local getters. Libraries like date-fns (isValid/parseISO) or Luxon provide similar safe parsing.

Citations:


Script executed:

find . -name "tokenExtractors.ts" -type f

Repository: dergigi/ants


Repository: dergigi/ants
Exit code: 0

stdout:

./src/lib/search/tokenExtractors.ts

Script executed:

if [ -f "src/lib/search/tokenExtractors.ts" ]; then
  wc -l "src/lib/search/tokenExtractors.ts"
  echo "---"
  sed -n '1,15p' "src/lib/search/tokenExtractors.ts"
  echo "..."
  sed -n '80,110p' "src/lib/search/tokenExtractors.ts"
fi

Repository: dergigi/ants


Repository: dergigi/ants
Exit code: 0

stdout:

130 src/lib/search/tokenExtractors.ts
---
import { Nip50Extensions } from './searchUtils';

/**
 * Extract NIP-50 extensions from the raw query string.
 * Removes extension tokens from the query and returns them separately.
 */
export function extractNip50Extensions(rawQuery: string): { cleaned: string; extensions: Nip50Extensions } {
  let cleaned = rawQuery;
  const extensions: Nip50Extensions = {};

  const includeSpamRegex = /(?:^|\s)include:spam(?:\s|$)/gi;
  if (includeSpamRegex.test(cleaned)) {
    extensions.includeSpam = true;
    cleaned = cleaned.replace(includeSpamRegex, ' ');
  }
...
export function extractDateFilter(rawQuery: string): { cleaned: string; since?: number; until?: number } {
  let cleaned = rawQuery;
  let since: number | undefined;
  let until: number | undefined;

  const dateToTimestamp = (dateStr: string): number => {
    const date = new Date(dateStr + 'T00:00:00Z');
    return Math.floor(date.getTime() / 1000);
  };

  const sinceRegex = /(?:^|\s)since:([0-9]{4}-[0-9]{2}-[0-9]{2})(?=\s|$)/gi;
  cleaned = cleaned.replace(sinceRegex, (_, dateStr: string) => {
    since = dateToTimestamp(dateStr);
    return ' ';
  });

  const untilRegex = /(?:^|\s)until:([0-9]{4}-[0-9]{2}-[0-9]{2})(?=\s|$)/gi;
  cleaned = cleaned.replace(untilRegex, (_, dateStr: string) => {
    const startOfDay = dateToTimestamp(dateStr);
    until = startOfDay + 86399;
    return ' ';
  });

  return { cleaned: cleaned.trim(), since, until };
}

/**
 * Extract pp:<provider> keyword from query string.
 * Forces a specific profile lookup provider for by:/mentions: resolution.
 * Valid values: vertex, relatr, relay
 */

Script executed:

# Check if there are callers of extractDateFilter and how they handle undefined returns
rg "extractDateFilter|dateToTimestamp" --type ts --type tsx -B 2 -A 2

Repository: dergigi/ants


Repository: dergigi/ants
Exit code: 2

stderr:

unrecognized file type: tsx

Script executed:

rg "extractDateFilter" -A 3 -B 1

Repository: dergigi/ants


Repository: dergigi/ants
Exit code: 0

stdout:

src/lib/search/tokenExtractors.ts-/** Extract date filter(s) from query string: since:YYYY-MM-DD and until:YYYY-MM-DD */
src/lib/search/tokenExtractors.ts:export function extractDateFilter(rawQuery: string): { cleaned: string; since?: number; until?: number } {
src/lib/search/tokenExtractors.ts-  let cleaned = rawQuery;
src/lib/search/tokenExtractors.ts-  let since: number | undefined;
src/lib/search/tokenExtractors.ts-  let until: number | undefined;
--
src/lib/search/queryParsing.ts-  extractKindFilter,
src/lib/search/queryParsing.ts:  extractDateFilter,
src/lib/search/queryParsing.ts-  extractProfileProvider
src/lib/search/queryParsing.ts-} from './tokenExtractors';
src/lib/search/queryParsing.ts-
--
src/lib/search/queryParsing.ts-  extractKindFilter,
src/lib/search/queryParsing.ts:  extractDateFilter,
src/lib/search/queryParsing.ts-  extractProfileProvider
src/lib/search/queryParsing.ts-} from './tokenExtractors';
src/lib/search/queryParsing.ts-
--
src/lib/search/queryParsing.ts-
src/lib/search/queryParsing.ts:  const dateExtraction = extractDateFilter(dateResolvedQuery);
src/lib/search/queryParsing.ts-  const dateFilter = { since: dateExtraction.since, until: dateExtraction.until };
src/lib/search/queryParsing.ts-  const cleanedQuery = dateExtraction.cleaned;
src/lib/search/queryParsing.ts-
--
src/components/SearchView.tsx-import { fetchSpellSummaries } from '@/lib/spellDiscovery';
src/components/SearchView.tsx:import { extractNip50Extensions, extractKindFilter, extractDateFilter, stripRelayFilters } from '@/lib/search/queryParsing';
src/components/SearchView.tsx-import { resolveAuthorToNpub } from '@/lib/vertex';
src/components/SearchView.tsx-import { NDKEvent } from '@nostr-dev-kit/ndk';
src/components/SearchView.tsx-import { searchEvents } from '@/lib/search';
--
src/components/SearchView.tsx-    cleaned = extractKindFilter(cleaned).cleaned;
src/components/SearchView.tsx:    cleaned = extractDateFilter(cleaned).cleaned;
src/components/SearchView.tsx-    cleaned = stripRelayFilters(cleaned);
src/components/SearchView.tsx-    cleaned = cleaned.replace(/\bby:\S+/gi, '');
src/components/SearchView.tsx-    cleaned = cleaned.replace(/\bmentions:\S+/gi, '');

Script executed:

# Read the entire file to see full context and check for other issues
cat -n "src/lib/search/tokenExtractors.ts"

Repository: dergigi/ants


Repository: dergigi/ants
Exit code: 0

stdout:

     1	import { Nip50Extensions } from './searchUtils';
     2	
     3	/**
     4	 * Extract NIP-50 extensions from the raw query string.
     5	 * Removes extension tokens from the query and returns them separately.
     6	 */
     7	export function extractNip50Extensions(rawQuery: string): { cleaned: string; extensions: Nip50Extensions } {
     8	  let cleaned = rawQuery;
     9	  const extensions: Nip50Extensions = {};
    10	
    11	  const includeSpamRegex = /(?:^|\s)include:spam(?:\s|$)/gi;
    12	  if (includeSpamRegex.test(cleaned)) {
    13	    extensions.includeSpam = true;
    14	    cleaned = cleaned.replace(includeSpamRegex, ' ');
    15	  }
    16	
    17	  const domainRegex = /(?:^|\s)domain:([^\s]+)(?:\s|$)/gi;
    18	  cleaned = cleaned.replace(domainRegex, (_, domain: string) => {
    19	    const value = (domain || '').trim();
    20	    if (value) extensions.domain = value;
    21	    return ' ';
    22	  });
    23	
    24	  const languageRegex = /(?:^|\s)language:([a-z]{2})(?:\s|$)/gi;
    25	  cleaned = cleaned.replace(languageRegex, (_, lang: string) => {
    26	    const value = (lang || '').trim().toLowerCase();
    27	    if (value && value.length === 2) extensions.language = value;
    28	    return ' ';
    29	  });
    30	
    31	  const sentimentRegex = /(?:^|\s)sentiment:(negative|neutral|positive)(?:\s|$)/gi;
    32	  cleaned = cleaned.replace(sentimentRegex, (_, sentiment: string) => {
    33	    const value = (sentiment || '').trim().toLowerCase();
    34	    if (['negative', 'neutral', 'positive'].includes(value)) {
    35	      extensions.sentiment = value as 'negative' | 'neutral' | 'positive';
    36	    }
    37	    return ' ';
    38	  });
    39	
    40	  const nsfwRegex = /(?:^|\s)nsfw:(true|false)(?:\s|$)/gi;
    41	  cleaned = cleaned.replace(nsfwRegex, (_, nsfw: string) => {
    42	    const value = (nsfw || '').trim().toLowerCase();
    43	    if (value === 'true') extensions.nsfw = true;
    44	    else if (value === 'false') extensions.nsfw = false;
    45	    return ' ';
    46	  });
    47	
    48	  return { cleaned: cleaned.trim(), extensions };
    49	}
    50	
    51	/** Strip legacy relay filters from query (relay:..., relays:mine) */
    52	export function stripRelayFilters(rawQuery: string): string {
    53	  return rawQuery
    54	    .replace(/(?:^|\s)relay:[^\s]+(?:\s|$)/gi, ' ')
    55	    .replace(/(?:^|\s)relays:mine(?:\s|$)/gi, ' ')
    56	    .trim();
    57	}
    58	
    59	/** Extract kind filter(s) from query string: supports comma-separated numbers */
    60	export function extractKindFilter(rawQuery: string): { cleaned: string; kinds?: number[] } {
    61	  let cleaned = rawQuery;
    62	  const kinds: number[] = [];
    63	  const kindRegex = /(?:^|\s)kind:([0-9]+(?:\s*,\s*[0-9]+)*)(?=\s|$)/gi;
    64	  cleaned = cleaned.replace(kindRegex, (_, list: string) => {
    65	    (list || '')
    66	      .split(',')
    67	      .map((s) => s.trim())
    68	      .filter(Boolean)
    69	      .forEach((token) => {
    70	        const num = parseInt(token, 10);
    71	        if (!Number.isNaN(num)) kinds.push(num);
    72	      });
    73	    return ' ';
    74	  });
    75	  const uniqueKinds = Array.from(new Set(kinds));
    76	  return { cleaned: cleaned.trim(), kinds: uniqueKinds.length ? uniqueKinds : undefined };
    77	}
    78	
    79	/** Extract date filter(s) from query string: since:YYYY-MM-DD and until:YYYY-MM-DD */
    80	export function extractDateFilter(rawQuery: string): { cleaned: string; since?: number; until?: number } {
    81	  let cleaned = rawQuery;
    82	  let since: number | undefined;
    83	  let until: number | undefined;
    84	
    85	  const dateToTimestamp = (dateStr: string): number => {
    86	    const date = new Date(dateStr + 'T00:00:00Z');
    87	    return Math.floor(date.getTime() / 1000);
    88	  };
    89	
    90	  const sinceRegex = /(?:^|\s)since:([0-9]{4}-[0-9]{2}-[0-9]{2})(?=\s|$)/gi;
    91	  cleaned = cleaned.replace(sinceRegex, (_, dateStr: string) => {
    92	    since = dateToTimestamp(dateStr);
    93	    return ' ';
    94	  });
    95	
    96	  const untilRegex = /(?:^|\s)until:([0-9]{4}-[0-9]{2}-[0-9]{2})(?=\s|$)/gi;
    97	  cleaned = cleaned.replace(untilRegex, (_, dateStr: string) => {
    98	    const startOfDay = dateToTimestamp(dateStr);
    99	    until = startOfDay + 86399;
   100	    return ' ';
   101	  });
   102	
   103	  return { cleaned: cleaned.trim(), since, until };
   104	}
   105	
   106	/**
   107	 * Extract pp:<provider> keyword from query string.
   108	 * Forces a specific profile lookup provider for by:/mentions: resolution.
   109	 * Valid values: vertex, relatr, relay
   110	 */
   111	/** Extract all by: tokens from a string */
   112	export function extractByTokens(seed: string): string[] {
   113	  const matches = Array.from(seed.matchAll(/\bby:(\S+)/gi));
   114	  return matches.map((m) => m[1] || '').filter(Boolean);
   115	}
   116	
   117	export type ProfileProviderKeyword = 'vertex' | 'relatr' | 'relay';
   118	
   119	const VALID_PROFILE_PROVIDERS: readonly ProfileProviderKeyword[] = ['vertex', 'relatr', 'relay'] as const;
   120	
   121	export function extractProfileProvider(rawQuery: string): { cleaned: string; profileProvider?: ProfileProviderKeyword } {
   122	  const regex = /(?:^|\s)pp:(vertex|relatr|relay)(?:\s|$)/gi;
   123	  const match = regex.exec(rawQuery);
   124	  if (!match) return { cleaned: rawQuery };
   125	  const raw = (match[1] || '').toLowerCase();
   126	  const provider = VALID_PROFILE_PROVIDERS.find((p) => p === raw);
   127	  if (!provider) return { cleaned: rawQuery };
   128	  const cleaned = rawQuery.replace(regex, ' ').trim();
   129	  return { cleaned, profileProvider: provider };
   130	}

Add validation to reject invalid calendar dates in date filters.

dateToTimestamp() never validates whether the parsed Date represents the input calendar date. In V8/Node.js, invalid dates like since:2026-02-31 normalize silently to March 3 instead of being rejected, causing wrong query results instead of ignoring the malformed modifier.

Validate that the reconstructed date matches the input before converting to timestamp:

Suggested fix
   const dateToTimestamp = (dateStr: string): number | undefined => {
     const date = new Date(dateStr + 'T00:00:00Z');
+    if (Number.isNaN(date.getTime()) || date.toISOString().slice(0, 10) !== dateStr) {
+      return undefined;
+    }
     return Math.floor(date.getTime() / 1000);
   };

   const sinceRegex = /(?:^|\s)since:([0-9]{4}-[0-9]{2}-[0-9]{2})(?=\s|$)/gi;
   cleaned = cleaned.replace(sinceRegex, (_, dateStr: string) => {
-    since = dateToTimestamp(dateStr);
+    const ts = dateToTimestamp(dateStr);
+    if (ts !== undefined) since = ts;
     return ' ';
   });

   const untilRegex = /(?:^|\s)until:([0-9]{4}-[0-9]{2}-[0-9]{2})(?=\s|$)/gi;
   cleaned = cleaned.replace(untilRegex, (_, dateStr: string) => {
     const startOfDay = dateToTimestamp(dateStr);
+    if (startOfDay === undefined) return ' ';
     until = startOfDay + 86399;
     return ' ';
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const dateToTimestamp = (dateStr: string): number => {
const date = new Date(dateStr + 'T00:00:00Z');
return Math.floor(date.getTime() / 1000);
};
const sinceRegex = /(?:^|\s)since:([0-9]{4}-[0-9]{2}-[0-9]{2})(?=\s|$)/gi;
cleaned = cleaned.replace(sinceRegex, (_, dateStr: string) => {
since = dateToTimestamp(dateStr);
return ' ';
});
const untilRegex = /(?:^|\s)until:([0-9]{4}-[0-9]{2}-[0-9]{2})(?=\s|$)/gi;
cleaned = cleaned.replace(untilRegex, (_, dateStr: string) => {
const startOfDay = dateToTimestamp(dateStr);
until = startOfDay + 86399;
return ' ';
const dateToTimestamp = (dateStr: string): number | undefined => {
const date = new Date(dateStr + 'T00:00:00Z');
if (Number.isNaN(date.getTime()) || date.toISOString().slice(0, 10) !== dateStr) {
return undefined;
}
return Math.floor(date.getTime() / 1000);
};
const sinceRegex = /(?:^|\s)since:([0-9]{4}-[0-9]{2}-[0-9]{2})(?=\s|$)/gi;
cleaned = cleaned.replace(sinceRegex, (_, dateStr: string) => {
const ts = dateToTimestamp(dateStr);
if (ts !== undefined) since = ts;
return ' ';
});
const untilRegex = /(?:^|\s)until:([0-9]{4}-[0-9]{2}-[0-9]{2})(?=\s|$)/gi;
cleaned = cleaned.replace(untilRegex, (_, dateStr: string) => {
const startOfDay = dateToTimestamp(dateStr);
if (startOfDay === undefined) return ' ';
until = startOfDay + 86399;
return ' ';
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/search/tokenExtractors.ts` around lines 85 - 100, dateToTimestamp
currently constructs a Date from a string but doesn't verify the Date matches
the original y/m/d parts (so inputs like since:2026-02-31 silently normalize);
change dateToTimestamp to parse year, month, day from the input, construct a
Date using Date.UTC(year, month-1, day), then verify
date.getUTCFullYear()==year, date.getUTCMonth()+1==month and
date.getUTCDate()==day; if the check fails return null (or NaN) instead of a
timestamp. Update the sinceRegex and untilRegex replace callbacks to check the
dateToTimestamp result and only assign to since/until when a valid timestamp is
returned; if invalid, do not set the filter value (and continue to strip/replace
the token the same way or leave it as a no-op per existing behavior). Ensure you
reference dateToTimestamp, sinceRegex and untilRegex when making changes.

Comment on lines +121 to +129
export function extractProfileProvider(rawQuery: string): { cleaned: string; profileProvider?: ProfileProviderKeyword } {
const regex = /(?:^|\s)pp:(vertex|relatr|relay)(?:\s|$)/gi;
const match = regex.exec(rawQuery);
if (!match) return { cleaned: rawQuery };
const raw = (match[1] || '').toLowerCase();
const provider = VALID_PROFILE_PROVIDERS.find((p) => p === raw);
if (!provider) return { cleaned: rawQuery };
const cleaned = rawQuery.replace(regex, ' ').trim();
return { cleaned, profileProvider: provider };
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Make duplicate pp: modifiers resolve consistently.

Line 123 locks onto the first pp: token, but Line 128 strips all of them. So by:alice pp:relatr pp:vertex still resolves with relatr, unlike the other singleton modifiers here, which are effectively last-wins because they assign inside replace().

🔧 Proposed fix
 export function extractProfileProvider(rawQuery: string): { cleaned: string; profileProvider?: ProfileProviderKeyword } {
   const regex = /(?:^|\s)pp:(vertex|relatr|relay)(?:\s|$)/gi;
-  const match = regex.exec(rawQuery);
-  if (!match) return { cleaned: rawQuery };
-  const raw = (match[1] || '').toLowerCase();
-  const provider = VALID_PROFILE_PROVIDERS.find((p) => p === raw);
-  if (!provider) return { cleaned: rawQuery };
-  const cleaned = rawQuery.replace(regex, ' ').trim();
+  let provider: ProfileProviderKeyword | undefined;
+  const cleaned = rawQuery.replace(regex, (_, raw: string) => {
+    provider = VALID_PROFILE_PROVIDERS.find((p) => p === raw.toLowerCase());
+    return ' ';
+  }).trim();
+  if (!provider) return { cleaned: rawQuery };
   return { cleaned, profileProvider: provider };
 }

As per coding guidelines, src/lib/search/**: "Pay close attention to: Query parsing correctness (boolean OR, modifiers like by:, has:, is:, site:, mentions:)".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/search/tokenExtractors.ts` around lines 121 - 129, The function
extractProfileProvider currently uses regex.exec to pick the first pp: token but
then removes all pp: tokens (global regex), causing inconsistent behavior;
change it to enumerate all matches (e.g., via rawQuery.matchAll or a loop over
regex.exec with the global flag) and select the last matched provider (to
implement last-wins semantics consistent with other modifiers), then keep the
global replace to strip all pp: tokens from the cleaned string; update
references inside extractProfileProvider to use the last match's group value
when setting profileProvider.

@dergigi
Copy link
Copy Markdown
Owner Author

dergigi commented Apr 17, 2026

Closing — this PR depends on src/lib/profile/providers.ts and the relatr integration, both of which were removed by the revert to v0.2.11 in #248. The pp:<provider> keyword only makes sense if we have multiple profile providers, and that architecture is gone. Happy to revisit if/when we re-introduce a multi-provider lookup system — at which point a fresh PR scoped to that design would be cleaner than rebasing this one.

@dergigi dergigi closed this Apr 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

by: resolves to wrong npub via relatr, breaking most combined queries

1 participant