dsh-search-enhance 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@ import { Service, type Context } from '@deepseek-ai/cordis';
2
2
  import type { Config } from '../config.js';
3
3
  import type { CanonicalSource, SourceRecordCandidate } from '../contracts/index.js';
4
4
  import { type ProviderAttemptRecord, type ProviderErrorKind } from '../provider-runtime/index.js';
5
- import { Context7RemoteClient, type Context7Library } from '../providers/context7.js';
5
+ import { type Context7RemoteClient, type Context7Library } from '../providers/context7.js';
6
6
  import type { BoundedSourceProvider, DocumentationSnippet, SourceProviderSearchInput, SourceProviderSearchOutcome } from '../providers/types.js';
7
7
  import type { Context7DocsCacheEntry, Context7DocRef } from './cache-domain.js';
8
8
  import { type Context7CachedDocLookup, type Context7CachedDocMatch } from './cache.js';
@@ -125,7 +125,7 @@ export declare class DocumentationSearchInfrastructureError extends Error {
125
125
  constructor(attempts: readonly ProviderAttemptRecord[], warnings: readonly DocumentationWarning[], cache: Readonly<DocumentationCacheReport>, cause: unknown);
126
126
  }
127
127
  export interface DocumentationSearchDependencies {
128
- readonly context7: Context7RemoteClient;
128
+ readonly context7: Pick<Context7RemoteClient, 'resolve' | 'docs'>;
129
129
  readonly context7Cache: Context7CachedOperations;
130
130
  readonly exa: BoundedSourceProvider;
131
131
  readonly getConfig: () => Config;
@@ -1,10 +1,11 @@
1
1
  import { Service } from '@deepseek-ai/cordis';
2
2
  import { createProviderAttemptRecord, isAbortError, isProviderError, ProviderError, throwIfAborted, } from '../provider-runtime/index.js';
3
3
  import { boundSourceProviderResult } from '../providers/bounded-result.js';
4
- import { Context7RemoteClient, context7LibrarySource, selectContext7Library, } from '../providers/context7.js';
4
+ import { context7LibrarySource, selectContext7Library, } from '../providers/context7.js';
5
5
  import { applySourceQuality } from '../search/index.js';
6
6
  import { CONTEXT7_CACHE_QUERY_MAX_SCAN_RECORDS, } from './cache.js';
7
7
  import { CONTEXT7_CACHE_STATES, Context7CachedOperations, Context7OperationFailure, isContext7LibraryId, normalizeContext7LibraryId, } from './context7-cache.js';
8
+ const CONTEXT7_RESOLVE_CANDIDATE_LIMIT = 30;
8
9
  export const DOCUMENTATION_SEARCH_SERVICE_KEY = 'searchEnhanceDocumentation';
9
10
  export const DOCUMENTATION_SEARCH_PROVIDERS = ['auto', 'context7', 'exa', 'all'];
10
11
  export const DOCUMENTATION_RESULT_PROVIDERS = ['context7', 'exa'];
@@ -347,6 +348,7 @@ export class DocumentationSearchService extends Service {
347
348
  await Promise.allSettled([...this.active]);
348
349
  }
349
350
  async executeContext7Resolve(input) {
351
+ const candidateLimit = input.candidateLimit ?? input.maxResults;
350
352
  const startedAt = safeClockValue(this.now);
351
353
  let dispatches = 0;
352
354
  const resolved = await this.context7Cache.resolve({
@@ -354,7 +356,7 @@ export class DocumentationSearchService extends Service {
354
356
  forceRefresh: input.forceRefresh,
355
357
  load: () => this.context7.resolve({
356
358
  config: input.config,
357
- limit: input.maxResults,
359
+ limit: candidateLimit,
358
360
  onDispatch: () => {
359
361
  dispatches += 1;
360
362
  input.onDispatch?.();
@@ -362,12 +364,12 @@ export class DocumentationSearchService extends Service {
362
364
  query: input.libraryName,
363
365
  signal: input.signal,
364
366
  }),
365
- maxResults: input.maxResults,
367
+ maxResults: candidateLimit,
366
368
  query: input.libraryName,
367
369
  signal: input.signal,
368
370
  });
369
- const candidates = Object.freeze([...resolved.entry.libraries]);
370
- const selected = selectContext7Library(candidates, input.libraryName, input.query);
371
+ const candidates = Object.freeze(resolved.entry.libraries.slice(0, input.maxResults));
372
+ const selected = selectContext7Library(resolved.entry.libraries, input.libraryName, input.query);
371
373
  return Object.freeze({
372
374
  attempts: successfulCacheAttempts('resolve', resolved, dispatches, elapsed(startedAt, this.now)),
373
375
  cache: cachePath(resolved.cache, resolved.evictedEntries),
@@ -638,6 +640,7 @@ export class DocumentationSearchService extends Service {
638
640
  forceRefresh: input.forceRefresh,
639
641
  libraryName: input.query,
640
642
  maxResults: input.maxResults,
643
+ candidateLimit: Math.min(CONTEXT7_RESOLVE_CANDIDATE_LIMIT, input.config.retention.providerMaxSources),
641
644
  onDispatch: () => { resolveDispatches += 1; },
642
645
  query: input.query,
643
646
  signal: input.signal,
@@ -121,17 +121,33 @@ export function parseContext7Libraries(body) {
121
121
  function normalizedMatchText(value) {
122
122
  return (value ?? '').toLowerCase().replace(/^@/, '').replace(/[^a-z0-9]+/g, '');
123
123
  }
124
- function context7LibraryScore(item, libraryName, query) {
124
+ function normalizedTerms(value) {
125
+ return value.toLowerCase()
126
+ .split(/[^a-z0-9@._-]+/)
127
+ .map(normalizedMatchText)
128
+ .filter(term => term.length > 0);
129
+ }
130
+ function queryMentionsName(queryTerms, value) {
131
+ const nameTerms = normalizedTerms(value ?? '');
132
+ if (nameTerms.length === 0)
133
+ return false;
134
+ return queryTerms.some((_term, index) => nameTerms.every((nameTerm, offset) => queryTerms[index + offset] === nameTerm));
135
+ }
136
+ function context7LibraryScore(item, libraryName, queryTerms) {
125
137
  const wanted = normalizedMatchText(libraryName);
126
138
  const rawText = `${item.id ?? ''} ${item.title ?? ''} ${item.description ?? ''}`.toLowerCase();
127
139
  const title = normalizedMatchText(item.title);
128
140
  const description = normalizedMatchText(item.description);
129
141
  const idSegments = (item.id ?? '').split('/').filter(Boolean).map(normalizedMatchText);
130
142
  let score = 0;
131
- if (/official.*documentation|documentation.*official|official docs|react\.dev/.test(rawText))
143
+ if (queryMentionsName(queryTerms, item.title))
144
+ score += 240;
145
+ for (const segment of (item.id ?? '').split('/').filter(Boolean)) {
146
+ if (queryMentionsName(queryTerms, segment))
147
+ score += 220;
148
+ }
149
+ if (/official.*documentation|documentation.*official|official docs/.test(rawText))
132
150
  score += 260;
133
- if (rawText.includes('react.dev'))
134
- score += 120;
135
151
  if (wanted.length > 0) {
136
152
  if (title === wanted)
137
153
  score += 240;
@@ -146,9 +162,8 @@ function context7LibraryScore(item, libraryName, query) {
146
162
  if (description.includes(wanted))
147
163
  score += 8;
148
164
  }
149
- for (const term of query.toLowerCase().split(/[^a-z0-9@.\-/]+/).filter(term => term.length > 1)) {
150
- const normalizedTerm = normalizedMatchText(term);
151
- if (normalizedTerm.length === 0)
165
+ for (const normalizedTerm of queryTerms) {
166
+ if (normalizedTerm.length <= 1)
152
167
  continue;
153
168
  if (title === normalizedTerm)
154
169
  score += 24;
@@ -174,11 +189,12 @@ function context7LibraryScore(item, libraryName, query) {
174
189
  }
175
190
  /** Stable selection by exact name, description relevance, trust, benchmark, and snippet coverage. */
176
191
  export function selectContext7Library(libraries, libraryName, query = libraryName) {
192
+ const queryTerms = normalizedTerms(query);
177
193
  return libraries
178
194
  .map((library, index) => ({
179
195
  index,
180
196
  library,
181
- score: context7LibraryScore(library, libraryName, query),
197
+ score: context7LibraryScore(library, libraryName, queryTerms),
182
198
  }))
183
199
  .filter(candidate => isContext7LibraryId(candidate.library.id))
184
200
  .sort((left, right) => right.score - left.score || left.index - right.index)[0]?.library;
@@ -29,8 +29,7 @@ export const DEFAULT_SEARCH_RESPONSE_PARSE_LIMITS = Object.freeze({
29
29
  maxUrlCharacters: 8192,
30
30
  });
31
31
  const URL_PATTERN = /https?:\/\/[^\s<>"'`,。、;:!?》)】)]+/g;
32
- const MARKDOWN_LINK_PATTERN = /\[([^\]]+)]\((https?:\/\/[^)]+)\)/g;
33
- const MARKDOWN_LINK_LINE_PATTERN = /\[[^\]]+]\(https?:\/\/[^)]+\)/;
32
+ const MARKDOWN_LINK_PATTERN = /(?<!\[)\[([^\[\]]+|\[\d+\])]\((https?:\/\/[^)]+)\)/g;
34
33
  const SOURCES_HEADING_PATTERN = /(?:^|\n)(?:#{1,6}\s*)?(?:\*\*|__)?\s*(?:sources?|references?|citations?|信源|参考资料|参考|引用|来源列表|来源)(?:\s*[((][^)\n]*[))])?\s*(?:(?:\*\*|__)\s*)?[::]?\s*(?:(?:\*\*|__)\s*)?$/gim;
35
34
  const SOURCES_FUNCTION_PATTERN = /(^|\n)\s*(sources|source|citations|citation|references|reference|citation_card|source_cards|source_card)\s*\(/gim;
36
35
  function isRecord(value) {
@@ -337,7 +336,7 @@ function isLinkOnlyLine(line) {
337
336
  const stripped = line.replace(/^\s*(?:[-*]|\d+\.)\s*/, '').trim();
338
337
  return stripped.length > 0 && (stripped.startsWith('http://')
339
338
  || stripped.startsWith('https://')
340
- || MARKDOWN_LINK_LINE_PATTERN.test(stripped));
339
+ || stripped.search(MARKDOWN_LINK_PATTERN) !== -1);
341
340
  }
342
341
  function splitTailLinkBlock(text, state, limits) {
343
342
  const lines = text.split(/\r?\n/);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-search-enhance",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Bounded search, documentation, extraction, progressively disclosed site mapping/research planning, and read-only diagnostics for DeepSeek Harness",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",