@auxiora/research 1.0.0

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.
Files changed (43) hide show
  1. package/LICENSE +191 -0
  2. package/dist/brave-search.d.ts +15 -0
  3. package/dist/brave-search.d.ts.map +1 -0
  4. package/dist/brave-search.js +106 -0
  5. package/dist/brave-search.js.map +1 -0
  6. package/dist/citation.d.ts +12 -0
  7. package/dist/citation.d.ts.map +1 -0
  8. package/dist/citation.js +64 -0
  9. package/dist/citation.js.map +1 -0
  10. package/dist/credibility.d.ts +7 -0
  11. package/dist/credibility.d.ts.map +1 -0
  12. package/dist/credibility.js +57 -0
  13. package/dist/credibility.js.map +1 -0
  14. package/dist/engine.d.ts +24 -0
  15. package/dist/engine.d.ts.map +1 -0
  16. package/dist/engine.js +204 -0
  17. package/dist/engine.js.map +1 -0
  18. package/dist/index.d.ts +7 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +6 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/knowledge-graph.d.ts +22 -0
  23. package/dist/knowledge-graph.d.ts.map +1 -0
  24. package/dist/knowledge-graph.js +74 -0
  25. package/dist/knowledge-graph.js.map +1 -0
  26. package/dist/types.d.ts +75 -0
  27. package/dist/types.d.ts.map +1 -0
  28. package/dist/types.js +2 -0
  29. package/dist/types.js.map +1 -0
  30. package/package.json +27 -0
  31. package/src/brave-search.ts +130 -0
  32. package/src/citation.ts +80 -0
  33. package/src/credibility.ts +65 -0
  34. package/src/engine.ts +244 -0
  35. package/src/index.ts +18 -0
  36. package/src/knowledge-graph.ts +87 -0
  37. package/src/types.ts +78 -0
  38. package/tests/citation.test.ts +72 -0
  39. package/tests/credibility.test.ts +53 -0
  40. package/tests/engine.test.ts +104 -0
  41. package/tests/knowledge-graph.test.ts +68 -0
  42. package/tsconfig.json +12 -0
  43. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,80 @@
1
+ import { nanoid } from 'nanoid';
2
+ import type { Source, Finding } from './types.js';
3
+
4
+ export class CitationTracker {
5
+ private sources = new Map<string, Source>();
6
+ private findings: Finding[] = [];
7
+
8
+ addSource(url: string, title: string, credibilityScore?: number): Source {
9
+ const parsed = new URL(url);
10
+ const domain = parsed.hostname.replace(/^www\./, '');
11
+
12
+ const source: Source = {
13
+ id: nanoid(),
14
+ url,
15
+ title,
16
+ domain,
17
+ accessedAt: Date.now(),
18
+ credibilityScore: credibilityScore ?? 0.5,
19
+ };
20
+
21
+ this.sources.set(source.id, source);
22
+ return source;
23
+ }
24
+
25
+ addFinding(content: string, sourceId: string, relevance?: number, category?: string): Finding {
26
+ const finding: Finding = {
27
+ id: nanoid(),
28
+ content,
29
+ sourceId,
30
+ relevance: relevance ?? 0.5,
31
+ category: category ?? 'general',
32
+ };
33
+
34
+ this.findings.push(finding);
35
+ return finding;
36
+ }
37
+
38
+ getSources(): Source[] {
39
+ return Array.from(this.sources.values());
40
+ }
41
+
42
+ getFindings(sourceId?: string): Finding[] {
43
+ if (sourceId) {
44
+ return this.findings.filter((f) => f.sourceId === sourceId);
45
+ }
46
+ return [...this.findings];
47
+ }
48
+
49
+ formatCitations(style: 'inline' | 'footnote' | 'bibliography'): string {
50
+ const sources = this.getSources();
51
+
52
+ switch (style) {
53
+ case 'inline':
54
+ return sources
55
+ .map((s, i) => `[${i + 1}] ${s.title} (${s.domain})`)
56
+ .join('\n');
57
+
58
+ case 'footnote':
59
+ return sources
60
+ .map((s, i) => {
61
+ const date = new Date(s.accessedAt).toISOString().split('T')[0];
62
+ return `^${i + 1} ${s.title}. ${s.url}. Accessed: ${date}`;
63
+ })
64
+ .join('\n');
65
+
66
+ case 'bibliography':
67
+ return sources
68
+ .map(
69
+ (s) =>
70
+ `${s.title}. ${s.domain}. ${s.url}. Credibility: ${s.credibilityScore.toFixed(2)}`,
71
+ )
72
+ .join('\n');
73
+ }
74
+ }
75
+
76
+ clear(): void {
77
+ this.sources.clear();
78
+ this.findings = [];
79
+ }
80
+ }
@@ -0,0 +1,65 @@
1
+ import type { CredibilityFactors } from './types.js';
2
+
3
+ const KNOWN_DOMAINS = new Map<string, number>([
4
+ ['wikipedia.org', 0.95],
5
+ ['arxiv.org', 0.95],
6
+ ['nature.com', 0.95],
7
+ ['science.org', 0.95],
8
+ ['bbc.com', 0.9],
9
+ ['reuters.com', 0.9],
10
+ ['apnews.com', 0.9],
11
+ ['nytimes.com', 0.8],
12
+ ['washingtonpost.com', 0.8],
13
+ ['theguardian.com', 0.8],
14
+ ['techcrunch.com', 0.8],
15
+ ['medium.com', 0.7],
16
+ ['stackoverflow.com', 0.7],
17
+ ['github.com', 0.7],
18
+ ['reddit.com', 0.5],
19
+ ['quora.com', 0.5],
20
+ ['twitter.com', 0.3],
21
+ ['facebook.com', 0.3],
22
+ ]);
23
+
24
+ export class CredibilityScorer {
25
+ score(url: string, factors?: Partial<CredibilityFactors>): number {
26
+ const domain = this.extractDomain(url);
27
+ let score = this.getDomainReputation(domain);
28
+
29
+ if (factors?.isHttps) {
30
+ score += 0.05;
31
+ }
32
+ if (factors?.hasAuthor) {
33
+ score += 0.05;
34
+ }
35
+ if (factors?.hasDate) {
36
+ score += 0.05;
37
+ }
38
+ if (factors?.crossReferenced) {
39
+ score += 0.1;
40
+ }
41
+
42
+ return Math.min(Math.max(score, 0), 1);
43
+ }
44
+
45
+ getDomainReputation(domain: string): number {
46
+ const lookup = KNOWN_DOMAINS.get(domain);
47
+ if (lookup !== undefined) {
48
+ return lookup;
49
+ }
50
+
51
+ if (domain.endsWith('.gov')) {
52
+ return 0.9;
53
+ }
54
+ if (domain.endsWith('.edu')) {
55
+ return 0.9;
56
+ }
57
+
58
+ return 0.5;
59
+ }
60
+
61
+ extractDomain(url: string): string {
62
+ const parsed = new URL(url);
63
+ return parsed.hostname.replace(/^www\./, '');
64
+ }
65
+ }
package/src/engine.ts ADDED
@@ -0,0 +1,244 @@
1
+ import { nanoid } from 'nanoid';
2
+ import type { ResearchDepth, ResearchQuery, ResearchResult, Finding, Source, ResearchProvider } from './types.js';
3
+ import { BraveSearchClient } from './brave-search.js';
4
+ import { CredibilityScorer } from './credibility.js';
5
+ import { CitationTracker } from './citation.js';
6
+
7
+ export interface ResearchEngineConfig {
8
+ maxConcurrentSources?: number;
9
+ defaultDepth?: ResearchDepth;
10
+ braveApiKey?: string;
11
+ searchTimeout?: number;
12
+ fetchTimeout?: number;
13
+ provider?: ResearchProvider;
14
+ }
15
+
16
+ export class ResearchEngine {
17
+ private readonly config: Required<Omit<ResearchEngineConfig, 'provider' | 'braveApiKey'>>;
18
+ private readonly braveClient: BraveSearchClient;
19
+ private readonly provider?: ResearchProvider;
20
+ private readonly credibilityScorer = new CredibilityScorer();
21
+
22
+ constructor(config?: ResearchEngineConfig) {
23
+ const apiKey = config?.braveApiKey ?? process.env.AUXIORA_RESEARCH_BRAVE_API_KEY;
24
+ if (!apiKey) {
25
+ throw new Error('Research engine requires a Brave Search API key (braveApiKey or AUXIORA_RESEARCH_BRAVE_API_KEY)');
26
+ }
27
+
28
+ this.config = {
29
+ maxConcurrentSources: config?.maxConcurrentSources ?? 5,
30
+ defaultDepth: config?.defaultDepth ?? 'standard',
31
+ searchTimeout: config?.searchTimeout ?? 10_000,
32
+ fetchTimeout: config?.fetchTimeout ?? 15_000,
33
+ };
34
+
35
+ this.braveClient = new BraveSearchClient({
36
+ apiKey,
37
+ searchTimeout: this.config.searchTimeout,
38
+ fetchTimeout: this.config.fetchTimeout,
39
+ });
40
+
41
+ this.provider = config?.provider;
42
+ }
43
+
44
+ async research(query: ResearchQuery): Promise<ResearchResult> {
45
+ const startTime = Date.now();
46
+ const depth = query.depth ?? this.config.defaultDepth;
47
+ const maxSources = query.maxSources ?? this.getMaxSources(depth);
48
+ const searchQueries = this.planResearch(query.topic, depth, query.focusAreas);
49
+
50
+ const tracker = new CitationTracker();
51
+ const findings: Finding[] = [];
52
+
53
+ // Execute searches in parallel, limited to maxSources total results
54
+ const searchPromises = searchQueries.map((q) => this.braveClient.search(q, 3));
55
+ const searchResults = await Promise.all(searchPromises);
56
+
57
+ // Flatten and deduplicate by URL, cap at maxSources
58
+ const seenUrls = new Set<string>();
59
+ const uniqueResults: { query: string; url: string; title: string; snippet: string }[] = [];
60
+
61
+ for (let i = 0; i < searchResults.length; i++) {
62
+ for (const result of searchResults[i]) {
63
+ if (!seenUrls.has(result.url) && uniqueResults.length < maxSources) {
64
+ seenUrls.add(result.url);
65
+ uniqueResults.push({
66
+ query: searchQueries[i],
67
+ url: result.url,
68
+ title: result.title,
69
+ snippet: result.description,
70
+ });
71
+ }
72
+ }
73
+ }
74
+
75
+ // Fetch pages in parallel
76
+ const fetchPromises = uniqueResults.map((r) => this.braveClient.fetchPage(r.url));
77
+ const pageContents = await Promise.all(fetchPromises);
78
+
79
+ // Process each result
80
+ for (let i = 0; i < uniqueResults.length; i++) {
81
+ const r = uniqueResults[i];
82
+ const credibility = this.credibilityScorer.score(r.url, {
83
+ isHttps: r.url.startsWith('https'),
84
+ });
85
+ const source = tracker.addSource(r.url, r.title, credibility);
86
+
87
+ const content = pageContents[i];
88
+ if (this.provider && content) {
89
+ // AI-powered extraction
90
+ const extracted = await this.extractFindings(r.query, content, r.title);
91
+ for (const text of extracted) {
92
+ tracker.addFinding(text, source.id, credibility, 'general');
93
+ }
94
+ } else {
95
+ // Fallback: use search snippet + first paragraph from fetched content
96
+ let text = r.snippet;
97
+ if (content) {
98
+ const firstParagraph = content.split('\n\n').find((p) => p.trim().length > 50);
99
+ if (firstParagraph) {
100
+ text += ' ' + firstParagraph.trim();
101
+ }
102
+ }
103
+ tracker.addFinding(text, source.id, credibility, 'general');
104
+ }
105
+ }
106
+
107
+ const allFindings = tracker.getFindings();
108
+ const deduplicated = this.deduplicateFindings(allFindings);
109
+ const sources = tracker.getSources();
110
+
111
+ // Synthesize summary
112
+ let executiveSummary: string;
113
+ if (this.provider && deduplicated.length > 0) {
114
+ executiveSummary = await this.synthesizeWithAI(query.topic, deduplicated, sources);
115
+ } else {
116
+ executiveSummary = this.synthesize(deduplicated);
117
+ }
118
+
119
+ return {
120
+ id: nanoid(),
121
+ query,
122
+ findings: deduplicated,
123
+ executiveSummary,
124
+ sources,
125
+ confidence: Math.min(deduplicated.length / maxSources, 1),
126
+ generatedAt: Date.now(),
127
+ durationMs: Date.now() - startTime,
128
+ };
129
+ }
130
+
131
+ planResearch(topic: string, depth: ResearchDepth, focusAreas?: string[]): string[] {
132
+ let queries: string[];
133
+ switch (depth) {
134
+ case 'quick':
135
+ queries = [topic];
136
+ break;
137
+ case 'standard':
138
+ queries = [topic, `${topic} overview`, `${topic} analysis`];
139
+ break;
140
+ case 'deep':
141
+ queries = [
142
+ topic,
143
+ `${topic} overview`,
144
+ `${topic} analysis`,
145
+ `${topic} comparison`,
146
+ `${topic} best practices`,
147
+ `${topic} research papers`,
148
+ ];
149
+ break;
150
+ }
151
+
152
+ if (focusAreas?.length) {
153
+ for (const area of focusAreas) {
154
+ queries.push(`${topic} ${area}`);
155
+ }
156
+ }
157
+
158
+ return queries;
159
+ }
160
+
161
+ synthesize(findings: Finding[]): string {
162
+ const sourceIds = new Set(findings.map((f) => f.sourceId));
163
+ const topFindings = findings
164
+ .sort((a, b) => b.relevance - a.relevance)
165
+ .slice(0, 3)
166
+ .map((f) => f.content);
167
+
168
+ return `Based on ${findings.length} findings from ${sourceIds.size} sources: ${topFindings.join('. ')}`;
169
+ }
170
+
171
+ deduplicateFindings(findings: Finding[]): Finding[] {
172
+ const seen = new Set<string>();
173
+ return findings.filter((f) => {
174
+ if (seen.has(f.content)) {
175
+ return false;
176
+ }
177
+ seen.add(f.content);
178
+ return true;
179
+ });
180
+ }
181
+
182
+ private async extractFindings(query: string, content: string, title: string): Promise<string[]> {
183
+ if (!this.provider) return [];
184
+
185
+ try {
186
+ const result = await this.provider.complete(
187
+ [{ role: 'user', content: `Extract key findings from this source about "${query}".\n\nSource: ${title}\n\nContent:\n${content.slice(0, 8000)}` }],
188
+ {
189
+ systemPrompt: 'You are a research assistant. Extract 2-4 distinct factual findings from the source. Return each finding on a separate line, prefixed with "- ". Be concise and factual.',
190
+ maxTokens: 500,
191
+ temperature: 0.1,
192
+ },
193
+ );
194
+
195
+ return result.content
196
+ .split('\n')
197
+ .filter((line) => line.startsWith('- '))
198
+ .map((line) => line.slice(2).trim())
199
+ .filter((line) => line.length > 10);
200
+ } catch {
201
+ return [];
202
+ }
203
+ }
204
+
205
+ private async synthesizeWithAI(topic: string, findings: Finding[], sources: Source[]): Promise<string> {
206
+ if (!this.provider) return this.synthesize(findings);
207
+
208
+ try {
209
+ const findingsText = findings
210
+ .sort((a, b) => b.relevance - a.relevance)
211
+ .slice(0, 10)
212
+ .map((f, i) => `${i + 1}. ${f.content}`)
213
+ .join('\n');
214
+
215
+ const sourcesText = sources
216
+ .map((s) => `- ${s.title} (${s.domain}, credibility: ${s.credibilityScore.toFixed(2)})`)
217
+ .join('\n');
218
+
219
+ const result = await this.provider.complete(
220
+ [{ role: 'user', content: `Synthesize these research findings about "${topic}" into a concise executive summary.\n\nFindings:\n${findingsText}\n\nSources:\n${sourcesText}` }],
221
+ {
222
+ systemPrompt: 'You are a research analyst. Write a concise executive summary (2-4 paragraphs) synthesizing the findings. Mention source credibility where relevant. Be factual and balanced.',
223
+ maxTokens: 800,
224
+ temperature: 0.3,
225
+ },
226
+ );
227
+
228
+ return result.content;
229
+ } catch {
230
+ return this.synthesize(findings);
231
+ }
232
+ }
233
+
234
+ private getMaxSources(depth: ResearchDepth): number {
235
+ switch (depth) {
236
+ case 'quick':
237
+ return 3;
238
+ case 'standard':
239
+ return 5;
240
+ case 'deep':
241
+ return 10;
242
+ }
243
+ }
244
+ }
package/src/index.ts ADDED
@@ -0,0 +1,18 @@
1
+ export type {
2
+ ResearchDepth,
3
+ ResearchQuery,
4
+ ResearchResult,
5
+ Finding,
6
+ Source,
7
+ CredibilityFactors,
8
+ KnowledgeEntity,
9
+ KnowledgeRelation,
10
+ ResearchProvider,
11
+ BraveWebResult,
12
+ BraveSearchResponse,
13
+ } from './types.js';
14
+ export { ResearchEngine, type ResearchEngineConfig } from './engine.js';
15
+ export { BraveSearchClient, type BraveSearchOptions } from './brave-search.js';
16
+ export { CredibilityScorer } from './credibility.js';
17
+ export { CitationTracker } from './citation.js';
18
+ export { KnowledgeGraph } from './knowledge-graph.js';
@@ -0,0 +1,87 @@
1
+ import { nanoid } from 'nanoid';
2
+ import type { KnowledgeEntity, KnowledgeRelation } from './types.js';
3
+
4
+ export class KnowledgeGraph {
5
+ private entities = new Map<string, KnowledgeEntity>();
6
+ private relations: KnowledgeRelation[] = [];
7
+
8
+ addEntity(name: string, type: string, properties?: Record<string, string>): KnowledgeEntity {
9
+ const entity: KnowledgeEntity = {
10
+ id: nanoid(),
11
+ name,
12
+ type,
13
+ properties: properties ?? {},
14
+ };
15
+
16
+ this.entities.set(entity.id, entity);
17
+ return entity;
18
+ }
19
+
20
+ getEntity(id: string): KnowledgeEntity | undefined {
21
+ return this.entities.get(id);
22
+ }
23
+
24
+ findByName(name: string): KnowledgeEntity | undefined {
25
+ for (const entity of this.entities.values()) {
26
+ if (entity.name === name) {
27
+ return entity;
28
+ }
29
+ }
30
+ return undefined;
31
+ }
32
+
33
+ findByType(type: string): KnowledgeEntity[] {
34
+ const result: KnowledgeEntity[] = [];
35
+ for (const entity of this.entities.values()) {
36
+ if (entity.type === type) {
37
+ result.push(entity);
38
+ }
39
+ }
40
+ return result;
41
+ }
42
+
43
+ addRelation(fromId: string, toId: string, relation: string): void {
44
+ this.relations.push({ fromId, toId, relation });
45
+ }
46
+
47
+ getRelated(entityId: string): Array<{ entity: KnowledgeEntity; relation: string; direction: 'from' | 'to' }> {
48
+ const result: Array<{ entity: KnowledgeEntity; relation: string; direction: 'from' | 'to' }> = [];
49
+
50
+ for (const rel of this.relations) {
51
+ if (rel.fromId === entityId) {
52
+ const entity = this.entities.get(rel.toId);
53
+ if (entity) {
54
+ result.push({ entity, relation: rel.relation, direction: 'from' });
55
+ }
56
+ }
57
+ if (rel.toId === entityId) {
58
+ const entity = this.entities.get(rel.fromId);
59
+ if (entity) {
60
+ result.push({ entity, relation: rel.relation, direction: 'to' });
61
+ }
62
+ }
63
+ }
64
+
65
+ return result;
66
+ }
67
+
68
+ removeEntity(id: string): boolean {
69
+ const deleted = this.entities.delete(id);
70
+ if (deleted) {
71
+ this.relations = this.relations.filter((r) => r.fromId !== id && r.toId !== id);
72
+ }
73
+ return deleted;
74
+ }
75
+
76
+ toJSON(): { entities: KnowledgeEntity[]; relations: KnowledgeRelation[] } {
77
+ return {
78
+ entities: Array.from(this.entities.values()),
79
+ relations: [...this.relations],
80
+ };
81
+ }
82
+
83
+ clear(): void {
84
+ this.entities.clear();
85
+ this.relations = [];
86
+ }
87
+ }
package/src/types.ts ADDED
@@ -0,0 +1,78 @@
1
+ export type ResearchDepth = 'quick' | 'standard' | 'deep';
2
+
3
+ export interface ResearchQuery {
4
+ topic: string;
5
+ depth: ResearchDepth;
6
+ maxSources?: number;
7
+ focusAreas?: string[];
8
+ }
9
+
10
+ export interface ResearchResult {
11
+ id: string;
12
+ query: ResearchQuery;
13
+ findings: Finding[];
14
+ executiveSummary: string;
15
+ sources: Source[];
16
+ confidence: number; // 0-1
17
+ generatedAt: number;
18
+ durationMs: number;
19
+ }
20
+
21
+ export interface Finding {
22
+ id: string;
23
+ content: string;
24
+ sourceId: string;
25
+ relevance: number; // 0-1
26
+ category: string;
27
+ }
28
+
29
+ export interface Source {
30
+ id: string;
31
+ url: string;
32
+ title: string;
33
+ domain: string;
34
+ accessedAt: number;
35
+ credibilityScore: number; // 0-1
36
+ }
37
+
38
+ export interface CredibilityFactors {
39
+ domainReputation: number;
40
+ hasAuthor: boolean;
41
+ hasDate: boolean;
42
+ isHttps: boolean;
43
+ crossReferenced: boolean;
44
+ }
45
+
46
+ export interface KnowledgeEntity {
47
+ id: string;
48
+ name: string;
49
+ type: string;
50
+ properties: Record<string, string>;
51
+ }
52
+
53
+ export interface KnowledgeRelation {
54
+ fromId: string;
55
+ toId: string;
56
+ relation: string;
57
+ }
58
+
59
+ /** Minimal provider interface — avoids hard dep on @auxiora/providers */
60
+ export interface ResearchProvider {
61
+ complete(
62
+ messages: { role: 'user' | 'assistant' | 'system'; content: string }[],
63
+ options?: { systemPrompt?: string; maxTokens?: number; temperature?: number },
64
+ ): Promise<{ content: string }>;
65
+ }
66
+
67
+ export interface BraveWebResult {
68
+ title: string;
69
+ url: string;
70
+ description: string;
71
+ extra_snippets?: string[];
72
+ }
73
+
74
+ export interface BraveSearchResponse {
75
+ web?: {
76
+ results: BraveWebResult[];
77
+ };
78
+ }
@@ -0,0 +1,72 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { CitationTracker } from '../src/citation.js';
3
+
4
+ describe('CitationTracker', () => {
5
+ it('addSource creates source with ID', () => {
6
+ const tracker = new CitationTracker();
7
+ const source = tracker.addSource('https://example.com/article', 'Test Article');
8
+ expect(source.id).toBeDefined();
9
+ expect(source.title).toBe('Test Article');
10
+ expect(source.url).toBe('https://example.com/article');
11
+ });
12
+
13
+ it('addSource extracts domain', () => {
14
+ const tracker = new CitationTracker();
15
+ const source = tracker.addSource('https://www.example.com/article', 'Test');
16
+ expect(source.domain).toBe('example.com');
17
+ });
18
+
19
+ it('addFinding links to source', () => {
20
+ const tracker = new CitationTracker();
21
+ const source = tracker.addSource('https://example.com', 'Source');
22
+ const finding = tracker.addFinding('Important fact', source.id);
23
+ expect(finding.sourceId).toBe(source.id);
24
+ expect(finding.content).toBe('Important fact');
25
+ });
26
+
27
+ it('getFindings filters by sourceId', () => {
28
+ const tracker = new CitationTracker();
29
+ const s1 = tracker.addSource('https://a.com', 'A');
30
+ const s2 = tracker.addSource('https://b.com', 'B');
31
+ tracker.addFinding('Fact 1', s1.id);
32
+ tracker.addFinding('Fact 2', s2.id);
33
+ tracker.addFinding('Fact 3', s1.id);
34
+
35
+ const filtered = tracker.getFindings(s1.id);
36
+ expect(filtered).toHaveLength(2);
37
+ expect(filtered.every((f) => f.sourceId === s1.id)).toBe(true);
38
+ });
39
+
40
+ it('formatCitations inline format', () => {
41
+ const tracker = new CitationTracker();
42
+ tracker.addSource('https://example.com/page', 'Example Page', 0.8);
43
+ const output = tracker.formatCitations('inline');
44
+ expect(output).toContain('[1]');
45
+ expect(output).toContain('Example Page');
46
+ expect(output).toContain('example.com');
47
+ });
48
+
49
+ it('formatCitations bibliography format', () => {
50
+ const tracker = new CitationTracker();
51
+ tracker.addSource('https://example.com/page', 'Example Page', 0.8);
52
+ const output = tracker.formatCitations('bibliography');
53
+ expect(output).toContain('Example Page');
54
+ expect(output).toContain('Credibility: 0.80');
55
+ });
56
+
57
+ it('getSources returns all', () => {
58
+ const tracker = new CitationTracker();
59
+ tracker.addSource('https://a.com', 'A');
60
+ tracker.addSource('https://b.com', 'B');
61
+ expect(tracker.getSources()).toHaveLength(2);
62
+ });
63
+
64
+ it('clear empties everything', () => {
65
+ const tracker = new CitationTracker();
66
+ tracker.addSource('https://a.com', 'A');
67
+ tracker.addFinding('fact', 'some-id');
68
+ tracker.clear();
69
+ expect(tracker.getSources()).toHaveLength(0);
70
+ expect(tracker.getFindings()).toHaveLength(0);
71
+ });
72
+ });
@@ -0,0 +1,53 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { CredibilityScorer } from '../src/credibility.js';
3
+
4
+ describe('CredibilityScorer', () => {
5
+ const scorer = new CredibilityScorer();
6
+
7
+ it('wikipedia.org scores 0.95', () => {
8
+ const score = scorer.score('https://wikipedia.org/wiki/Test');
9
+ expect(score).toBe(0.95);
10
+ });
11
+
12
+ it('.gov domain scores 0.9', () => {
13
+ const score = scorer.score('https://data.gov/dataset');
14
+ expect(score).toBe(0.9);
15
+ });
16
+
17
+ it('.edu domain scores 0.9', () => {
18
+ const score = scorer.score('https://mit.edu/research');
19
+ expect(score).toBe(0.9);
20
+ });
21
+
22
+ it('unknown domain scores 0.5', () => {
23
+ const score = scorer.score('https://randomsite.xyz/page');
24
+ expect(score).toBe(0.5);
25
+ });
26
+
27
+ it('HTTPS bonus adds 0.05', () => {
28
+ const base = scorer.score('https://randomsite.xyz/page');
29
+ const withHttps = scorer.score('https://randomsite.xyz/page', { isHttps: true });
30
+ expect(withHttps - base).toBeCloseTo(0.05);
31
+ });
32
+
33
+ it('crossReferenced adds 0.1', () => {
34
+ const base = scorer.score('https://randomsite.xyz/page');
35
+ const withCross = scorer.score('https://randomsite.xyz/page', { crossReferenced: true });
36
+ expect(withCross - base).toBeCloseTo(0.1);
37
+ });
38
+
39
+ it('score clamped to max 1.0', () => {
40
+ const score = scorer.score('https://wikipedia.org/wiki/Test', {
41
+ isHttps: true,
42
+ hasAuthor: true,
43
+ hasDate: true,
44
+ crossReferenced: true,
45
+ });
46
+ expect(score).toBeLessThanOrEqual(1.0);
47
+ });
48
+
49
+ it('extractDomain handles full URLs', () => {
50
+ expect(scorer.extractDomain('https://www.example.com/path?q=1')).toBe('example.com');
51
+ expect(scorer.extractDomain('https://sub.domain.org/page')).toBe('sub.domain.org');
52
+ });
53
+ });