Files
msd-core/src/research-provider.cts
Tom Boucher ba231ecbfc chore: clean up clear-cut ESLint warnings (#732) (#734)
Pay down pre-existing error→warn lint debt. Removes dead imports/vars, unused functions, redundant regex/string escapes, and stale eslint-disable directives; converts unused `catch (_e)` to optional catch binding (src/*.cts).

No behavior change. Lint 345→125 warnings (0 errors); deferred categories (n/no-process-exit, test-sleeps, control-regex) tracked in #732 for follow-up. Full test suite green (0 failures); code-review verified all removals unused and all escape fixes semantics-preserving.

Closes #732

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 11:24:48 -04:00

281 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Research Provider Module
*
* Encodes the Balanced-set provider decision: PROVIDER_WATERFALL constant,
* classifyConfidence, providerAvailability, and planResearch (with injectable
* store for testability).
*
* ADR-457 build-at-publish: authored as TypeScript .cts → emits .cjs via tsc.
*/
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
type ConfidenceLevel = 'HIGH' | 'MEDIUM' | 'LOW';
interface ProviderWaterfall {
docs: string[];
web: string[];
scrape: string[];
}
interface ClassifyConfidenceInput {
provider?: unknown;
verifiedAgainstOfficial?: unknown;
legitimacyVerdict?: unknown;
}
interface ProviderAvailabilityConfig {
exa_search?: unknown;
tavily_search?: unknown;
brave_search?: unknown;
firecrawl?: unknown;
ref_search?: unknown;
perplexity?: unknown;
jina?: unknown;
[key: string]: unknown;
}
interface Question {
text: string;
kind: string;
library?: string;
version?: string;
}
interface StoreResult {
hit: boolean;
stale: boolean;
entry: unknown;
}
interface ResearchStore {
researchKey(input: {
ecosystem?: unknown;
library?: unknown;
version?: unknown;
query?: unknown;
kind?: unknown;
}): string;
getResearch(
cwd: string,
key: string,
opts?: { clock?: typeof Date; homeDir?: string; kind?: string }
): StoreResult;
}
interface PlanResearchOptions {
questions: Question[];
ecosystem?: string;
cwd: string;
config?: ProviderAvailabilityConfig;
clock?: typeof Date;
homeDir?: string;
store?: ResearchStore;
}
interface CacheInfo {
hit: boolean;
stale: boolean;
}
interface FetchInfo {
provider: string;
query: string;
}
interface ResearchItem {
question: string;
key: string;
cache?: CacheInfo;
fetch?: FetchInfo;
}
interface PlanResearchResult {
items: ResearchItem[];
}
// ---------------------------------------------------------------------------
// Cycle 1 / Cycle 6: PROVIDER_WATERFALL (Balanced-set decision)
// firecrawl appears ONLY in scrape — demoted to known-URL scrape, NOT in docs/web
// ---------------------------------------------------------------------------
const PROVIDER_WATERFALL: ProviderWaterfall = {
docs: ['context7', 'ref', 'jina', 'websearch'],
web: ['exa', 'tavily', 'perplexity', 'brave', 'websearch'],
scrape: ['firecrawl', 'jina'],
};
// ---------------------------------------------------------------------------
// Cycle 4: classifyConfidence (evidence-driven)
//
// HIGH = corroborated against an authoritative source (registry ground-truth),
// NOT a correctness guarantee.
//
// Two-axis model:
// axis-1: authorityOf(provider) → 'official' | 'scrape' | 'web' | 'none'
// axis-2: groundTruth (legitimacyVerdict normalized to OK|SUS|SLOP)
//
// Decision table (evaluated in order):
// legitimacyVerdict === 'SLOP' → LOW (caps everything)
// groundTruth && authority !== 'none' → HIGH
// authority === 'official' || authority === 'scrape' → MEDIUM
// groundTruth (authority 'none') → MEDIUM
// authority === 'web' && verifiedAgainstOfficial → MEDIUM
// else → LOW
// ---------------------------------------------------------------------------
type ProviderAuthority = 'official' | 'scrape' | 'web' | 'none';
function authorityOf(provider: unknown): ProviderAuthority {
switch (provider) {
case 'context7':
case 'ref':
return 'official';
case 'jina':
case 'firecrawl':
return 'scrape';
case 'exa':
case 'tavily':
case 'perplexity':
case 'brave':
case 'websearch':
return 'web';
default:
return 'none';
}
}
function normalizeLegitimacyVerdict(raw: unknown): 'OK' | 'SUS' | 'SLOP' | null {
if (typeof raw !== 'string') return null;
const upper = raw.toUpperCase();
if (upper === 'OK' || upper === 'SUS' || upper === 'SLOP') return upper;
return null;
}
function classifyConfidence(input: ClassifyConfidenceInput): ConfidenceLevel {
try {
const { provider, verifiedAgainstOfficial, legitimacyVerdict } = input;
const authority = authorityOf(provider);
const verdict = normalizeLegitimacyVerdict(legitimacyVerdict);
const groundTruth = verdict === 'OK';
// SLOP caps everything — checked first
if (verdict === 'SLOP') return 'LOW';
// Ground-truth corroboration + known authority → HIGH
if (groundTruth && authority !== 'none') return 'HIGH';
// Official or scrape provider (authority alone) → MEDIUM
if (authority === 'official' || authority === 'scrape') return 'MEDIUM';
// Ground-truth but unknown provider → MEDIUM
if (groundTruth) return 'MEDIUM';
// Web provider with self-reported verification → MEDIUM
if (authority === 'web' && verifiedAgainstOfficial === true) return 'MEDIUM';
return 'LOW';
} catch {
return 'LOW';
}
}
// ---------------------------------------------------------------------------
// Cycle 5: providerAvailability
// ---------------------------------------------------------------------------
function providerAvailability(config?: ProviderAvailabilityConfig): Record<string, boolean> {
const cfg = config ?? {};
return {
context7: true,
jina: cfg.jina !== undefined ? Boolean(cfg.jina) : true,
websearch: true,
exa: Boolean(cfg.exa_search),
tavily: Boolean(cfg.tavily_search),
brave: Boolean(cfg.brave_search),
firecrawl: Boolean(cfg.firecrawl),
ref: Boolean(cfg.ref_search),
perplexity: Boolean(cfg.perplexity),
};
}
// ---------------------------------------------------------------------------
// Lazy-load default store (avoids circular require at module eval time)
// ---------------------------------------------------------------------------
let _defaultStore: ResearchStore | undefined;
function getDefaultStore(): ResearchStore {
if (!_defaultStore) {
// eslint-disable-next-line @typescript-eslint/no-require-imports -- lazy default store; tests inject their own
_defaultStore = require('./research-store.cjs') as ResearchStore;
}
return _defaultStore;
}
// ---------------------------------------------------------------------------
// Cycle 1–3, 5, 7: planResearch
// ---------------------------------------------------------------------------
function planResearch(options: PlanResearchOptions): PlanResearchResult {
const {
questions,
ecosystem = '',
cwd,
config,
clock = Date,
homeDir,
store = getDefaultStore(),
} = options;
const availability = providerAvailability(config);
const items: ResearchItem[] = questions.flatMap((q) => {
const { text, kind, library, version } = q;
// Skip questions without a non-empty string text — emitting an item with
// question:undefined / fetch.query:undefined would produce corrupt output.
if (typeof text !== 'string' || text.length === 0) {
return [];
}
const key = store.researchKey({ ecosystem, library, version, query: text, kind });
const res = store.getResearch(cwd, key, { clock, homeDir, kind });
// Fresh cache hit — no fetch needed
if (res.hit && !res.stale) {
return { question: text, key, cache: { hit: true, stale: false } };
}
// Determine which waterfall to use
const waterfall: string[] =
(PROVIDER_WATERFALL as unknown as Record<string, string[]>)[kind] ?? PROVIDER_WATERFALL.web;
// Pick first available provider
const provider = waterfall.find((p) => availability[p] === true) ?? 'websearch';
const item: ResearchItem = {
question: text,
key,
fetch: { provider, query: text },
};
// Stale hit: include cache info
if (res.hit) {
item.cache = { hit: true, stale: true };
}
return item;
});
return { items };
}
// ---------------------------------------------------------------------------
// Exports
// ---------------------------------------------------------------------------
export = { PROVIDER_WATERFALL, classifyConfidence, providerAvailability, planResearch };