feat: add pp:<provider> keyword to force profile lookup provider#245
feat: add pp:<provider> keyword to force profile lookup provider#245
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughAdds 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 | 🟠 MajorCache key should include
forcedProviderto avoid cross-provider cache hits.The cache key at line 31 is built from
queryandloggedIn, but notforcedProvider. If a user searchesby:dergigi pp:vertexand thenby: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
makeProfileSearchCacheKeyto 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 withoutprofileProvider, 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 | 🟠 MajorThis 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 | 🟠 MajorSplit 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 validatingforcedProviderand clarifyingpp:relaybehavior.The
forcedProviderstring is cast directly toProfileLookupProviderwithout validation. If an invalid value is passed (e.g.,pp:foo), it will silently fall through and returnnull. Additionally,pp:relaywould immediately hit line 104'sbreakand returnnullfromtryQueryProviders, which might be confusing sincerelayis 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
📒 Files selected for processing (19)
src/components/QueryTranslation.tsxsrc/lib/profile/providers.tssrc/lib/profile/resolver.tssrc/lib/profile/search.tssrc/lib/search.tssrc/lib/search/authorResolve.tssrc/lib/search/orExpansion.tssrc/lib/search/queryParsing.tssrc/lib/search/strategies/authorSearchStrategy.tssrc/lib/search/strategies/dTagSearchStrategy.tssrc/lib/search/strategies/linkSearchStrategy.tssrc/lib/search/strategies/mentionsSearchStrategy.tssrc/lib/search/strategies/profileSearchStrategy.tssrc/lib/search/strategies/refSearchStrategy.tssrc/lib/search/strategies/replySearchStrategy.tssrc/lib/search/strategies/strategyUtils.tssrc/lib/search/termSearch.tssrc/lib/search/topLevelOr.tssrc/lib/search/types.ts
| fallbackRelaySetFactory?: () => Promise<NDKRelaySet>, | ||
| profileProvider?: string |
There was a problem hiding this comment.
🧩 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($_, $_, $_, $_, $_, $_, $_, $_)' || trueRepository: 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.tsRepository: 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:
- orExpansion.ts:115 — Missing
profileProvider(7 args instead of 8) - authorSearchStrategy.ts:132 — Missing both
fallbackRelaySetFactoryandprofileProvider(6 args instead of 8) - 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.
| 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 | |
| ); |
| 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.
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.
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (9)
src/components/QueryTranslation.tsxsrc/lib/profile/providers.tssrc/lib/profile/search.tssrc/lib/queryTranslationHelpers.tssrc/lib/search/orExpansion.tssrc/lib/search/orHelpers.tssrc/lib/search/queryParsing.tssrc/lib/search/strategies/authorSearchStrategy.tssrc/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
- 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
There was a problem hiding this comment.
♻️ Duplicate comments (1)
src/lib/search/orHelpers.ts (1)
88-95:⚠️ Potential issue | 🟠 MajorNormalize 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 likeOR/( OR ), so Lines 89-94 makefilter.searchtruthy and incorrectly switch the query ontonip50RelaySet. Those queries should stay onbroadRelaySet.🔧 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: NarrowprofileProviderto a literal union type.The runtime guard is good, but returning
stringweakens 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 theorHelpers↔orExpansionimport cycle.
orHelpers.ts(line 9) importsextractByTokensfrom./orExpansion, whileorExpansion.ts(line 14) imports from./orHelpersand re-exportsdedupeEventsandhandleProfileSeedsfrom it. Create a neutraltokenExtractors.tsmodule forextractByTokensand 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
📒 Files selected for processing (4)
src/components/QueryTranslation.tsxsrc/lib/queryTranslationHelpers.tssrc/lib/search/orHelpers.tssrc/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
There was a problem hiding this comment.
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
📒 Files selected for processing (3)
src/lib/search/orExpansion.tssrc/lib/search/orHelpers.tssrc/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
| 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 ' '; |
There was a problem hiding this comment.
❓ 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:
- 1: For dates that do not exist, new dates respond inconsistently nodejs/node#47253
- 2: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date
- 3: https://dev.to/hi_iam_chris/dates-issue-and-how-to-validate-them-2edn
- 4: new Date('yyyy-m-d') gives incorrect result nodejs/node#20680
- 5: https://www.nodejs.org
- 6: https://v8-docs.vercel.app/dateparser-inl_8h_source.html
- 7: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse
Script executed:
find . -name "tokenExtractors.ts" -type fRepository: 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"
fiRepository: 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 2Repository: dergigi/ants
Repository: dergigi/ants
Exit code: 2
stderr:
unrecognized file type: tsx
Script executed:
rg "extractDateFilter" -A 3 -B 1Repository: 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.
| 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.
| 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 }; |
There was a problem hiding this comment.
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.
|
Closing — this PR depends on |
Adds the
pp:<provider>query keyword that forces a specific profile lookup provider forby:andmentions:resolution, addressing the issue whereby:dergigiresolves to the wrong npub via relatr when logged out. Users can now writeby:dergigi pp:vertexto bypass relatr entirely, orby:someone pp:relatrto force it. Valid values arevertex,relatr, andrelay.pp:in query parsing and threadsprofileProviderthroughSearchContext, all search strategies, OR handlers, and the full resolution chain down totryQueryProviderspp:is set, only the specified provider is tried (no fallback cascade)[pp:vertex]prefix when the keyword is activeCloses #244
Summary by CodeRabbit