@asterxsk/kiln 0.4.0 → 0.4.2
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.
- package/README.md +155 -155
- package/agent/AGENTS.md +67 -67
- package/agent/README.md +5 -5
- package/agent/extensions/ask-user/index.ts +418 -418
- package/agent/extensions/ask-user/install.ps1 +27 -27
- package/agent/extensions/ask-user/install.sh +24 -24
- package/agent/extensions/ask-user/package-lock.json +769 -769
- package/agent/extensions/ask-user/package.json +19 -19
- package/agent/extensions/ask-user/prompt.ts +45 -45
- package/agent/extensions/ask-user/tsconfig.json +7 -7
- package/agent/extensions/background-terminals/docs/implementation-guide.md +942 -942
- package/agent/extensions/background-terminals/index.ts +627 -627
- package/agent/extensions/background-terminals/install.ps1 +27 -27
- package/agent/extensions/background-terminals/install.sh +24 -24
- package/agent/extensions/background-terminals/manager.test.ts +735 -735
- package/agent/extensions/background-terminals/output.test.ts +109 -109
- package/agent/extensions/background-terminals/package-lock.json +769 -769
- package/agent/extensions/background-terminals/package.json +17 -17
- package/agent/extensions/background-terminals/prompt.test.ts +125 -125
- package/agent/extensions/background-terminals/ps.test.ts +82 -82
- package/agent/extensions/background-terminals/result-delivery.test.ts +44 -44
- package/agent/extensions/background-terminals/src/domain.ts +87 -87
- package/agent/extensions/background-terminals/src/manager.ts +907 -907
- package/agent/extensions/background-terminals/src/output.ts +84 -84
- package/agent/extensions/background-terminals/src/prompt.ts +142 -142
- package/agent/extensions/background-terminals/src/result-delivery.ts +27 -27
- package/agent/extensions/background-terminals/src/runtime.ts +36 -36
- package/agent/extensions/background-terminals/src/ui/output-view.ts +79 -79
- package/agent/extensions/background-terminals/src/ui/ps.ts +621 -621
- package/agent/extensions/background-terminals/tsconfig.json +7 -7
- package/agent/extensions/destructive/README.md +31 -31
- package/agent/extensions/destructive/index.ts +88 -88
- package/agent/extensions/destructive/install.ps1 +27 -27
- package/agent/extensions/destructive/install.sh +24 -24
- package/agent/extensions/file-search/index.spec.ts +443 -443
- package/agent/extensions/file-search/index.ts +459 -459
- package/agent/extensions/file-search/install.ps1 +27 -27
- package/agent/extensions/file-search/install.sh +24 -24
- package/agent/extensions/file-search/package-lock.json +2253 -2253
- package/agent/extensions/file-search/package.json +23 -23
- package/agent/extensions/file-search/src/args.ts +122 -122
- package/agent/extensions/file-search/src/binaries.ts +422 -422
- package/agent/extensions/file-search/src/output.ts +126 -126
- package/agent/extensions/file-search/src/process.ts +146 -146
- package/agent/extensions/file-search/src/prompt.ts +52 -52
- package/agent/extensions/file-search/tsconfig.json +7 -7
- package/agent/extensions/goal/README.md +50 -50
- package/agent/extensions/goal/index.ts +155 -155
- package/agent/extensions/goal/install.ps1 +27 -27
- package/agent/extensions/goal/install.sh +24 -24
- package/agent/extensions/kiln-update/README.md +38 -25
- package/agent/extensions/kiln-update/index.ts +177 -99
- package/agent/extensions/modelconf/PLAN.md +915 -915
- package/agent/extensions/modelconf/README.md +66 -66
- package/agent/extensions/modelconf/index.ts +296 -296
- package/agent/extensions/modelconf/install.ps1 +27 -27
- package/agent/extensions/modelconf/install.sh +24 -24
- package/agent/extensions/modelconf/package-lock.json +1809 -1809
- package/agent/extensions/modelconf/package.json +13 -13
- package/agent/extensions/modelconf/src/fuzzy.ts +26 -26
- package/agent/extensions/modelconf/src/glob.ts +13 -13
- package/agent/extensions/modelconf/src/persistence.test.ts +46 -46
- package/agent/extensions/modelconf/src/persistence.ts +164 -164
- package/agent/extensions/modelconf/src/ui/ModelConfView.test.ts +75 -75
- package/agent/extensions/modelconf/src/ui/ModelConfView.ts +1101 -1101
- package/agent/extensions/modelconf/tsconfig.json +15 -15
- package/agent/extensions/pi-web-access/CHANGELOG.md +690 -690
- package/agent/extensions/pi-web-access/LICENSE +21 -21
- package/agent/extensions/pi-web-access/README.md +470 -470
- package/agent/extensions/pi-web-access/SECURITY.md +5 -5
- package/agent/extensions/pi-web-access/activity.ts +101 -101
- package/agent/extensions/pi-web-access/auth-fetch.ts +148 -148
- package/agent/extensions/pi-web-access/brightdata-unlocker.ts +272 -272
- package/agent/extensions/pi-web-access/chrome-cookies.ts +669 -669
- package/agent/extensions/pi-web-access/content-find.ts +139 -139
- package/agent/extensions/pi-web-access/credential-source.ts +191 -191
- package/agent/extensions/pi-web-access/data-uri-sanitize.ts +406 -406
- package/agent/extensions/pi-web-access/datalab-pdf-extract.ts +568 -568
- package/agent/extensions/pi-web-access/declared-web-links.ts +173 -173
- package/agent/extensions/pi-web-access/evidence/CONTRACT-EVIDENCE.md +496 -496
- package/agent/extensions/pi-web-access/evidence/contract-probe.mjs +140 -140
- package/agent/extensions/pi-web-access/exa.ts +526 -526
- package/agent/extensions/pi-web-access/extract.ts +1196 -1196
- package/agent/extensions/pi-web-access/feature-config.ts +29 -29
- package/agent/extensions/pi-web-access/fetch-params.ts +111 -111
- package/agent/extensions/pi-web-access/gemini-adc.ts +298 -298
- package/agent/extensions/pi-web-access/gemini-api.ts +353 -353
- package/agent/extensions/pi-web-access/gemini-pdf-extract.ts +108 -108
- package/agent/extensions/pi-web-access/gemini-search.ts +21 -21
- package/agent/extensions/pi-web-access/gemini-url-context.ts +128 -128
- package/agent/extensions/pi-web-access/gemini-web-config.ts +101 -101
- package/agent/extensions/pi-web-access/gemini-web.ts +487 -487
- package/agent/extensions/pi-web-access/github-api.ts +197 -197
- package/agent/extensions/pi-web-access/github-extract.ts +746 -746
- package/agent/extensions/pi-web-access/github-issue-pr.ts +700 -700
- package/agent/extensions/pi-web-access/index.ts +1737 -1737
- package/agent/extensions/pi-web-access/package-lock.json +5808 -5808
- package/agent/extensions/pi-web-access/package.json +64 -64
- package/agent/extensions/pi-web-access/page-query.ts +96 -96
- package/agent/extensions/pi-web-access/pdf-extract.ts +409 -409
- package/agent/extensions/pi-web-access/promise-try.d.ts +7 -7
- package/agent/extensions/pi-web-access/query-rewrite.ts +51 -51
- package/agent/extensions/pi-web-access/render-search-error.ts +170 -170
- package/agent/extensions/pi-web-access/rsc-extract.ts +338 -338
- package/agent/extensions/pi-web-access/search-types.ts +20 -20
- package/agent/extensions/pi-web-access/source-check.ts +282 -282
- package/agent/extensions/pi-web-access/ssrf-protection.ts +526 -526
- package/agent/extensions/pi-web-access/storage.ts +521 -521
- package/agent/extensions/pi-web-access/summary-model-scope.ts +125 -125
- package/agent/extensions/pi-web-access/test/auth-fetch.test.mjs +208 -208
- package/agent/extensions/pi-web-access/test/brightdata-unlocker.test.mjs +840 -840
- package/agent/extensions/pi-web-access/test/chrome-cookie-extraction.test.mjs +441 -441
- package/agent/extensions/pi-web-access/test/config-path.test.mjs +283 -283
- package/agent/extensions/pi-web-access/test/content-find.test.mjs +25 -25
- package/agent/extensions/pi-web-access/test/credential-source.test.mjs +118 -118
- package/agent/extensions/pi-web-access/test/data-uri-sanitize.test.mjs +210 -210
- package/agent/extensions/pi-web-access/test/datalab-pdf-extract.test.mjs +552 -552
- package/agent/extensions/pi-web-access/test/declared-web-links.test.mjs +212 -212
- package/agent/extensions/pi-web-access/test/fetch-answer-storage.test.mjs +40 -40
- package/agent/extensions/pi-web-access/test/fetch-cache-storage.test.mjs +334 -334
- package/agent/extensions/pi-web-access/test/fetch-content-domain-policy.test.mjs +95 -95
- package/agent/extensions/pi-web-access/test/fetch-modes.test.mjs +53 -53
- package/agent/extensions/pi-web-access/test/fetch-not-found-guidance.test.mjs +92 -92
- package/agent/extensions/pi-web-access/test/fetch-params.test.mjs +86 -86
- package/agent/extensions/pi-web-access/test/fetch-render-call.test.mjs +34 -34
- package/agent/extensions/pi-web-access/test/fetch-routing.test.mjs +173 -173
- package/agent/extensions/pi-web-access/test/gemini-adc-auth.test.mjs +257 -257
- package/agent/extensions/pi-web-access/test/gemini-api-transport.test.mjs +170 -170
- package/agent/extensions/pi-web-access/test/gemini-pdf-extract.test.mjs +133 -133
- package/agent/extensions/pi-web-access/test/gemini-web-cookie-opt-in.test.mjs +178 -178
- package/agent/extensions/pi-web-access/test/gemini-web-header-overflow.test.mjs +148 -148
- package/agent/extensions/pi-web-access/test/get-search-content.test.mjs +223 -223
- package/agent/extensions/pi-web-access/test/github-extract.test.mjs +378 -378
- package/agent/extensions/pi-web-access/test/github-issue-pr.test.mjs +565 -565
- package/agent/extensions/pi-web-access/test/inline-content-config.test.mjs +99 -99
- package/agent/extensions/pi-web-access/test/lazy-extract-load.test.mjs +118 -118
- package/agent/extensions/pi-web-access/test/local-video-oversize.test.mjs +52 -52
- package/agent/extensions/pi-web-access/test/package-typebox-dependency.test.mjs +50 -50
- package/agent/extensions/pi-web-access/test/page-query.test.mjs +51 -51
- package/agent/extensions/pi-web-access/test/pdf-config.test.mjs +140 -140
- package/agent/extensions/pi-web-access/test/pdf-extract.test.mjs +500 -500
- package/agent/extensions/pi-web-access/test/proxy-transport.test.mjs +286 -286
- package/agent/extensions/pi-web-access/test/query-rewrite.test.mjs +52 -52
- package/agent/extensions/pi-web-access/test/rsc-fallback.test.mjs +102 -102
- package/agent/extensions/pi-web-access/test/search-error-render.test.mjs +152 -152
- package/agent/extensions/pi-web-access/test/search-providers.test.mjs +274 -274
- package/agent/extensions/pi-web-access/test/source-check.test.mjs +179 -179
- package/agent/extensions/pi-web-access/test/ssrf-allow-ranges-config.test.mjs +205 -205
- package/agent/extensions/pi-web-access/test/ssrf-protection.test.mjs +456 -456
- package/agent/extensions/pi-web-access/test/summary-model-scope.test.mjs +106 -106
- package/agent/extensions/pi-web-access/test/tool-registration-config.test.mjs +182 -182
- package/agent/extensions/pi-web-access/test/web-search-answer-render.test.mjs +66 -66
- package/agent/extensions/pi-web-access/test/youtube-extract-errors.test.mjs +64 -64
- package/agent/extensions/pi-web-access/tsconfig.json +11 -11
- package/agent/extensions/pi-web-access/utils.ts +451 -451
- package/agent/extensions/pi-web-access/video-extract.ts +392 -392
- package/agent/extensions/pi-web-access/youtube-extract.ts +328 -328
- package/agent/extensions/shared/activity-status.ts +31 -31
- package/agent/extensions/shared/child-session.test.ts +270 -270
- package/agent/extensions/shared/child-session.ts +148 -148
- package/agent/extensions/shared/context-utilization.test.ts +48 -48
- package/agent/extensions/shared/context-utilization.ts +47 -47
- package/agent/extensions/shared/dashboard-state.ts +99 -99
- package/agent/extensions/shared/install.ps1 +27 -27
- package/agent/extensions/shared/install.sh +24 -24
- package/agent/extensions/shared/tool-call-timeout.test.ts +117 -117
- package/agent/extensions/shared/tool-call-timeout.ts +104 -104
- package/agent/extensions/skillsconf/README.md +74 -74
- package/agent/extensions/skillsconf/index.ts +92 -92
- package/agent/extensions/skillsconf/install.ps1 +27 -27
- package/agent/extensions/skillsconf/install.sh +24 -24
- package/agent/extensions/skillsconf/package-lock.json +1809 -1809
- package/agent/extensions/skillsconf/package.json +18 -18
- package/agent/extensions/skillsconf/src/delete-skill.test.ts +64 -64
- package/agent/extensions/skillsconf/src/delete-skill.ts +45 -45
- package/agent/extensions/skillsconf/src/filter.test.ts +80 -80
- package/agent/extensions/skillsconf/src/filter.ts +74 -74
- package/agent/extensions/skillsconf/src/fuzzy.ts +26 -26
- package/agent/extensions/skillsconf/src/persistence.test.ts +72 -72
- package/agent/extensions/skillsconf/src/persistence.ts +169 -169
- package/agent/extensions/skillsconf/src/ui/SkillConfView.test.ts +337 -337
- package/agent/extensions/skillsconf/src/ui/SkillConfView.ts +758 -758
- package/agent/extensions/skillsconf/src/ui/text-input.ts +91 -91
- package/agent/extensions/skillsconf/src/ui/tui-helpers.ts +144 -144
- package/agent/extensions/skillsconf/tsconfig.json +15 -15
- package/agent/extensions/statusline/index.ts +273 -282
- package/agent/extensions/statusline/install.ps1 +27 -27
- package/agent/extensions/statusline/install.sh +24 -24
- package/agent/extensions/subagents/by-the-way.test.ts +29 -29
- package/agent/extensions/subagents/claude.test.ts +119 -119
- package/agent/extensions/subagents/codex.test.ts +102 -102
- package/agent/extensions/subagents/context-usage.test.ts +107 -107
- package/agent/extensions/subagents/docs/design-plan.md +568 -568
- package/agent/extensions/subagents/docs/effect-v4-extension-guide.md +354 -354
- package/agent/extensions/subagents/docs/effect-v4-notes.md +571 -571
- package/agent/extensions/subagents/index.ts +779 -779
- package/agent/extensions/subagents/install.ps1 +27 -27
- package/agent/extensions/subagents/install.sh +24 -24
- package/agent/extensions/subagents/manager.test.ts +276 -276
- package/agent/extensions/subagents/package-lock.json +2244 -2244
- package/agent/extensions/subagents/package.json +19 -19
- package/agent/extensions/subagents/result-delivery.test.ts +27 -27
- package/agent/extensions/subagents/src/backend.ts +73 -73
- package/agent/extensions/subagents/src/backends/claude.ts +701 -701
- package/agent/extensions/subagents/src/backends/codex.ts +1060 -1060
- package/agent/extensions/subagents/src/backends/pi.ts +575 -575
- package/agent/extensions/subagents/src/backends/stub.ts +300 -300
- package/agent/extensions/subagents/src/by-the-way.ts +21 -21
- package/agent/extensions/subagents/src/domain.ts +253 -253
- package/agent/extensions/subagents/src/format.ts +74 -74
- package/agent/extensions/subagents/src/manager.ts +736 -736
- package/agent/extensions/subagents/src/prompt.ts +92 -92
- package/agent/extensions/subagents/src/result-delivery.ts +20 -20
- package/agent/extensions/subagents/src/runtime.ts +53 -53
- package/agent/extensions/subagents/src/ui/takeover.ts +583 -583
- package/agent/extensions/subagents/src/ui/transcript.ts +201 -201
- package/agent/extensions/subagents/takeover.test.ts +29 -29
- package/agent/extensions/subagents/tsconfig.json +7 -7
- package/agent/extensions/todo/AGENTS.md +38 -38
- package/agent/extensions/todo/LICENSE +21 -21
- package/agent/extensions/todo/config.ts +55 -55
- package/agent/extensions/todo/index.ts +151 -151
- package/agent/extensions/todo/install.ps1 +27 -27
- package/agent/extensions/todo/install.sh +24 -24
- package/agent/extensions/todo/locales/de.json +17 -17
- package/agent/extensions/todo/locales/en.json +15 -15
- package/agent/extensions/todo/locales/es.json +17 -17
- package/agent/extensions/todo/locales/fr.json +17 -17
- package/agent/extensions/todo/locales/pt-BR.json +17 -17
- package/agent/extensions/todo/locales/pt.json +17 -17
- package/agent/extensions/todo/locales/ru.json +17 -17
- package/agent/extensions/todo/locales/uk.json +17 -17
- package/agent/extensions/todo/locales/zh.json +17 -17
- package/agent/extensions/todo/package-lock.json +3358 -3358
- package/agent/extensions/todo/package.json +67 -67
- package/agent/extensions/todo/state/i18n-bridge.ts +64 -64
- package/agent/extensions/todo/state/invariants.ts +20 -20
- package/agent/extensions/todo/state/replay.ts +38 -38
- package/agent/extensions/todo/state/selectors.ts +107 -107
- package/agent/extensions/todo/state/state-reducer.ts +326 -326
- package/agent/extensions/todo/state/state.ts +18 -18
- package/agent/extensions/todo/state/store.ts +82 -82
- package/agent/extensions/todo/state/task-graph.ts +57 -57
- package/agent/extensions/todo/todo-overlay.ts +200 -200
- package/agent/extensions/todo/todo.ts +155 -155
- package/agent/extensions/todo/tool/response-envelope.ts +109 -109
- package/agent/extensions/todo/tool/types.ts +206 -206
- package/agent/extensions/todo/view/format.ts +177 -177
- package/agent/extensions/trim-context/README.md +54 -54
- package/agent/extensions/trim-context/index.ts +487 -487
- package/agent/extensions/trim-context/install.ps1 +27 -27
- package/agent/extensions/trim-context/install.sh +24 -24
- package/agent/keybindings.json +7 -7
- package/agent/version.txt +1 -1
- package/bin/kiln.js +20 -13
- package/package.json +1 -1
- package/agent/extensions/taste/index.ts +0 -443
- package/agent/extensions/taste/install.ps1 +0 -27
- package/agent/extensions/taste/install.sh +0 -24
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import type { ExtractedContent } from "./extract.ts";
|
|
2
|
-
|
|
3
|
-
export interface SearchResult {
|
|
4
|
-
title: string;
|
|
5
|
-
url: string;
|
|
6
|
-
snippet: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface SearchResponse {
|
|
10
|
-
answer: string;
|
|
11
|
-
results: SearchResult[];
|
|
12
|
-
inlineContent?: ExtractedContent[];
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface SearchOptions {
|
|
16
|
-
numResults?: number;
|
|
17
|
-
recencyFilter?: "day" | "week" | "month" | "year";
|
|
18
|
-
domainFilter?: string[];
|
|
19
|
-
signal?: AbortSignal;
|
|
20
|
-
}
|
|
1
|
+
import type { ExtractedContent } from "./extract.ts";
|
|
2
|
+
|
|
3
|
+
export interface SearchResult {
|
|
4
|
+
title: string;
|
|
5
|
+
url: string;
|
|
6
|
+
snippet: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface SearchResponse {
|
|
10
|
+
answer: string;
|
|
11
|
+
results: SearchResult[];
|
|
12
|
+
inlineContent?: ExtractedContent[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SearchOptions {
|
|
16
|
+
numResults?: number;
|
|
17
|
+
recencyFilter?: "day" | "week" | "month" | "year";
|
|
18
|
+
domainFilter?: string[];
|
|
19
|
+
signal?: AbortSignal;
|
|
20
|
+
}
|
|
@@ -1,282 +1,282 @@
|
|
|
1
|
-
// Structured source checking and machine-readable research artifacts.
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
|
-
import { generateId, getResult, storeResult } from "./storage.ts";
|
|
4
|
-
import type { SearchResult } from "./search-types.ts";
|
|
5
|
-
import type { ExtractedContent } from "./extract.ts";
|
|
6
|
-
|
|
7
|
-
export type SourceQuality = "official_docs" | "vendor_docs" | "repo_issue" | "blog" | "forum" | "news" | "unknown";
|
|
8
|
-
export type ClaimStatus = "supported" | "contradicted" | "unclear" | "missing-evidence";
|
|
9
|
-
export type RecencyFilter = "day" | "week" | "month" | "year";
|
|
10
|
-
|
|
11
|
-
export interface ResearchSource {
|
|
12
|
-
rank: number;
|
|
13
|
-
url: string;
|
|
14
|
-
title: string;
|
|
15
|
-
snippet?: string;
|
|
16
|
-
fetch_timestamp?: number;
|
|
17
|
-
content_hash?: string;
|
|
18
|
-
quality: SourceQuality;
|
|
19
|
-
fetched?: boolean;
|
|
20
|
-
fetch_error?: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface ResearchPassage {
|
|
24
|
-
passage_id: string;
|
|
25
|
-
source_url: string;
|
|
26
|
-
source_rank: number;
|
|
27
|
-
text: string;
|
|
28
|
-
extraction_span?: { start: number; end: number };
|
|
29
|
-
content_hash?: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface ClaimAssessment {
|
|
33
|
-
claim: string;
|
|
34
|
-
status: ClaimStatus;
|
|
35
|
-
supporting_passages: string[];
|
|
36
|
-
contradicting_passages: string[];
|
|
37
|
-
rationale: string;
|
|
38
|
-
confidence: number;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface ResearchArtifact {
|
|
42
|
-
id: string;
|
|
43
|
-
type: "research";
|
|
44
|
-
timestamp: number;
|
|
45
|
-
query: string;
|
|
46
|
-
sources: ResearchSource[];
|
|
47
|
-
passages: ResearchPassage[];
|
|
48
|
-
claims?: ClaimAssessment[];
|
|
49
|
-
provider?: string;
|
|
50
|
-
summary?: string;
|
|
51
|
-
content_hash?: string;
|
|
52
|
-
filters?: { recency?: RecencyFilter; domain_include?: string[]; domain_exclude?: string[] };
|
|
53
|
-
errors?: Array<{ query: string; error: string }>;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export interface ResearchSearchRequest {
|
|
57
|
-
query: string;
|
|
58
|
-
numResults: number;
|
|
59
|
-
recencyFilter?: RecencyFilter;
|
|
60
|
-
domainFilter?: string[];
|
|
61
|
-
}
|
|
62
|
-
export interface ResearchSearchResult { url: string; title: string; snippet: string; rank: number }
|
|
63
|
-
export interface ResearchSearchResponse { provider: string; results: ResearchSearchResult[]; summary?: string }
|
|
64
|
-
export interface ResearchProvider { name: string; search(req: ResearchSearchRequest): Promise<ResearchSearchResponse> }
|
|
65
|
-
|
|
66
|
-
const OFFICIAL_DOCS_HOSTS = /^(developers\.|docs\.|learn\.|reference\.)|\.github\.io$/i;
|
|
67
|
-
const OFFICIAL_DOCS_PATHS = /\/(docs?|reference)(\/|\b)/i;
|
|
68
|
-
const VENDOR_DOCS_PATHS = /\/(documentation|docs?)\//i;
|
|
69
|
-
const REPO_ISSUE_PATHS = /\/(issues|pull|pulls)\//i;
|
|
70
|
-
const BLOG_HOSTS = /(medium\.com|substack\.com|dev\.to|hashnode\.)/i;
|
|
71
|
-
const BLOG_PATHS = /\/blogs?\//i;
|
|
72
|
-
const FORUM_HOSTS = /(stackoverflow\.com|serverfault\.com|superuser\.com|discourse\.|community\.)/i;
|
|
73
|
-
const FORUM_PATHS = /\/(forum|forums|threads)\//i;
|
|
74
|
-
const NEWS_HOSTS = /(reuters\.com|bloomberg\.com|techcrunch\.com|theverge\.com|arstechnica\.com|wired\.com|cnet\.com|zdnet\.com)/i;
|
|
75
|
-
const NEWS_PATHS = /\/news(\/|$)/i;
|
|
76
|
-
|
|
77
|
-
export function classifySource(url: string): SourceQuality {
|
|
78
|
-
let host = "";
|
|
79
|
-
let path = "";
|
|
80
|
-
try {
|
|
81
|
-
const parsed = new URL(url);
|
|
82
|
-
host = parsed.hostname;
|
|
83
|
-
path = parsed.pathname;
|
|
84
|
-
} catch {
|
|
85
|
-
return "unknown";
|
|
86
|
-
}
|
|
87
|
-
if (REPO_ISSUE_PATHS.test(path)) return "repo_issue";
|
|
88
|
-
if (OFFICIAL_DOCS_HOSTS.test(host) || OFFICIAL_DOCS_PATHS.test(path)) return "official_docs";
|
|
89
|
-
if (VENDOR_DOCS_PATHS.test(path)) return "vendor_docs";
|
|
90
|
-
if (NEWS_HOSTS.test(host) || NEWS_PATHS.test(path)) return "news";
|
|
91
|
-
if (FORUM_HOSTS.test(host) || FORUM_PATHS.test(path)) return "forum";
|
|
92
|
-
if (BLOG_HOSTS.test(host) || BLOG_PATHS.test(path)) return "blog";
|
|
93
|
-
return "unknown";
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function hashContent(text: string): string {
|
|
97
|
-
return `sha256:${createHash("sha256").update(text, "utf8").digest("hex")}`;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
interface Span { text: string; start: number; end: number }
|
|
101
|
-
|
|
102
|
-
function tokenize(value: string): string[] {
|
|
103
|
-
return [...new Set(value.toLowerCase().split(/[^a-z0-9]+/).filter((term) => term.length > 3))];
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function extractRelevantSpans(content: string, hint: string): Span[] {
|
|
107
|
-
const sentences: Span[] = [];
|
|
108
|
-
const sentencePattern = /[^.!?]+(?:[.!?]+(?=\s|$)|$)/g;
|
|
109
|
-
for (const match of content.matchAll(sentencePattern)) {
|
|
110
|
-
const raw = match[0];
|
|
111
|
-
const text = raw.trim();
|
|
112
|
-
if (text.length > 0 && text.length <= 400) {
|
|
113
|
-
const start = (match.index ?? 0) + raw.indexOf(text);
|
|
114
|
-
sentences.push({ text, start, end: start + text.length });
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
const terms = tokenize(hint);
|
|
118
|
-
if (terms.length === 0) return [];
|
|
119
|
-
return sentences
|
|
120
|
-
.map((sentence, index) => ({ sentence, index, score: terms.filter((term) => sentence.text.toLowerCase().includes(term)).length }))
|
|
121
|
-
.filter((item) => item.score > 0)
|
|
122
|
-
.sort((a, b) => b.score - a.score || a.index - b.index)
|
|
123
|
-
.slice(0, 3)
|
|
124
|
-
.map(({ sentence }) => sentence);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function passageId(sourceRank: number, index: number): string {
|
|
128
|
-
return `p-${sourceRank}-${index}`;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export function buildPassages(sources: ResearchSource[], fetched: ExtractedContent[] = [], hint = ""): ResearchPassage[] {
|
|
132
|
-
const passages: ResearchPassage[] = [];
|
|
133
|
-
const fetchedByUrl = new Map(fetched.map((item) => [item.url, item]));
|
|
134
|
-
for (const source of sources) {
|
|
135
|
-
if (source.snippet) {
|
|
136
|
-
passages.push({
|
|
137
|
-
passage_id: passageId(source.rank, 0),
|
|
138
|
-
source_url: source.url,
|
|
139
|
-
source_rank: source.rank,
|
|
140
|
-
text: source.snippet,
|
|
141
|
-
content_hash: hashContent(source.snippet),
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
const page = fetchedByUrl.get(source.url);
|
|
145
|
-
if (page && !page.error && page.content) {
|
|
146
|
-
const passageHint = source.snippet?.trim() || hint;
|
|
147
|
-
for (const [index, span] of extractRelevantSpans(page.content, passageHint).entries()) {
|
|
148
|
-
passages.push({
|
|
149
|
-
passage_id: passageId(source.rank, index + 1),
|
|
150
|
-
source_url: source.url,
|
|
151
|
-
source_rank: source.rank,
|
|
152
|
-
text: span.text,
|
|
153
|
-
extraction_span: { start: span.start, end: span.end },
|
|
154
|
-
content_hash: hashContent(span.text),
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
return passages;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const CONTRADICTION_MARKERS = ["not true", "false", "incorrect", "debunked", "retracted", "no longer", "never", "denied", "contrary", "misleading"];
|
|
163
|
-
const SUPPORT_MARKERS = ["yes", "true", "correct", "confirmed", "according to", "shows that", "demonstrates", "reported", "verified", "established"];
|
|
164
|
-
|
|
165
|
-
function containsPhrase(value: string, phrase: string): boolean {
|
|
166
|
-
const escaped = phrase.trim().toLowerCase().split(/\s+/).map((term) => term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\s+");
|
|
167
|
-
return new RegExp(`(?:^|[^a-z0-9])${escaped}(?=$|[^a-z0-9])`, "i").test(value);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function markerIsNegated(value: string, marker: string): boolean {
|
|
171
|
-
const escaped = marker.trim().toLowerCase().split(/\s+/).map((term) => term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\s+");
|
|
172
|
-
const markerPattern = new RegExp(`(?:^|[^a-z0-9])${escaped}(?=$|[^a-z0-9])`, "i");
|
|
173
|
-
const match = markerPattern.exec(value);
|
|
174
|
-
if (!match || match.index === undefined) return false;
|
|
175
|
-
const matchedMarker = match[0].replace(/^[^a-z0-9]+/i, "");
|
|
176
|
-
const beforeMarker = value.slice(0, match.index + match[0].length - matchedMarker.length);
|
|
177
|
-
return /(?:^|[^a-z0-9])(?:not|no|never|without)\s+$/i.test(beforeMarker);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function hasPolarityMarker(value: string, markers: string[], allowNegated = false): boolean {
|
|
181
|
-
return markers.some((marker) => containsPhrase(value, marker) && (allowNegated || !markerIsNegated(value, marker)));
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
export function assessClaim(claim: string, passages: ResearchPassage[]): ClaimAssessment {
|
|
185
|
-
const terms = tokenize(claim);
|
|
186
|
-
if (terms.length === 0 || passages.length === 0) {
|
|
187
|
-
return { claim, status: "missing-evidence", supporting_passages: [], contradicting_passages: [], rationale: "No passages available that discuss the claim's terms.", confidence: 0.2 };
|
|
188
|
-
}
|
|
189
|
-
const supporting: string[] = [];
|
|
190
|
-
const contradicting: string[] = [];
|
|
191
|
-
for (const passage of passages) {
|
|
192
|
-
const lower = passage.text.toLowerCase();
|
|
193
|
-
const overlap = terms.filter((term) => containsPhrase(lower, term)).length;
|
|
194
|
-
if (overlap < Math.max(2, Math.ceil(terms.length / 4))) continue;
|
|
195
|
-
const contra = hasPolarityMarker(lower, CONTRADICTION_MARKERS);
|
|
196
|
-
const support = hasPolarityMarker(lower, SUPPORT_MARKERS);
|
|
197
|
-
if (contra && !support) contradicting.push(passage.passage_id);
|
|
198
|
-
else if (support && !contra) supporting.push(passage.passage_id);
|
|
199
|
-
}
|
|
200
|
-
if (contradicting.length > 0 && supporting.length === 0) {
|
|
201
|
-
return { claim, status: "contradicted", supporting_passages: [], contradicting_passages: contradicting, rationale: `${contradicting.length} passage(s) contradict the claim; none support it.`, confidence: Math.min(0.85, 0.5 + contradicting.length * 0.1) };
|
|
202
|
-
}
|
|
203
|
-
if (supporting.length > 0 && contradicting.length === 0) {
|
|
204
|
-
return { claim, status: "supported", supporting_passages: supporting, contradicting_passages: [], rationale: `${supporting.length} passage(s) support the claim; none contradict it.`, confidence: Math.min(0.85, 0.5 + supporting.length * 0.1) };
|
|
205
|
-
}
|
|
206
|
-
if (supporting.length > 0 || contradicting.length > 0) {
|
|
207
|
-
return { claim, status: "unclear", supporting_passages: supporting, contradicting_passages: contradicting, rationale: `${supporting.length} supporting and ${contradicting.length} contradicting passage(s); evidence is mixed.`, confidence: 0.4 };
|
|
208
|
-
}
|
|
209
|
-
return { claim, status: "unclear", supporting_passages: [], contradicting_passages: [], rationale: "Passages mention the claim's terms but contain no clear support or contradiction markers.", confidence: 0.3 };
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
interface RankedSearchResult extends SearchResult {
|
|
213
|
-
rank?: number;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
export interface BuildArtifactInput {
|
|
217
|
-
query: string;
|
|
218
|
-
provider?: string;
|
|
219
|
-
summary?: string;
|
|
220
|
-
results: RankedSearchResult[];
|
|
221
|
-
fetched?: ExtractedContent[];
|
|
222
|
-
recency?: RecencyFilter;
|
|
223
|
-
domainFilter?: string[];
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
export function buildResearchArtifact(input: BuildArtifactInput): ResearchArtifact {
|
|
227
|
-
const filters = input.domainFilter ?? [];
|
|
228
|
-
const fetchedByUrl = new Map((input.fetched ?? []).map((page) => [page.url, page]));
|
|
229
|
-
const sources: ResearchSource[] = [];
|
|
230
|
-
const seen = new Set<string>();
|
|
231
|
-
for (const [index, result] of input.results.entries()) {
|
|
232
|
-
if (seen.has(result.url)) continue;
|
|
233
|
-
seen.add(result.url);
|
|
234
|
-
const page = fetchedByUrl.get(result.url);
|
|
235
|
-
const fetched = Boolean(page && !page.error);
|
|
236
|
-
sources.push({
|
|
237
|
-
rank: result.rank ?? index + 1,
|
|
238
|
-
url: result.url,
|
|
239
|
-
title: result.title,
|
|
240
|
-
snippet: result.snippet,
|
|
241
|
-
quality: classifySource(result.url),
|
|
242
|
-
fetched,
|
|
243
|
-
...(page ? { fetch_timestamp: Date.now() } : {}),
|
|
244
|
-
...(page && !page.error ? { content_hash: hashContent(page.content) } : {}),
|
|
245
|
-
...(page?.error ? { fetch_error: page.error } : {}),
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
const passages = buildPassages(sources, input.fetched, input.query);
|
|
249
|
-
const domainInclude = filters.filter((domain) => !domain.startsWith("-"));
|
|
250
|
-
const domainExclude = filters.filter((domain) => domain.startsWith("-")).map((domain) => domain.slice(1));
|
|
251
|
-
return {
|
|
252
|
-
id: generateId(),
|
|
253
|
-
type: "research",
|
|
254
|
-
timestamp: Date.now(),
|
|
255
|
-
query: input.query,
|
|
256
|
-
sources,
|
|
257
|
-
passages,
|
|
258
|
-
...(input.provider !== undefined ? { provider: input.provider } : {}),
|
|
259
|
-
...(input.summary !== undefined ? { summary: input.summary } : {}),
|
|
260
|
-
...(passages.length > 0 ? { content_hash: hashContent(passages.map((passage) => passage.text).join("\n")) } : {}),
|
|
261
|
-
filters: {
|
|
262
|
-
...(input.recency !== undefined ? { recency: input.recency } : {}),
|
|
263
|
-
domain_include: domainInclude,
|
|
264
|
-
domain_exclude: domainExclude,
|
|
265
|
-
},
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
export function withClaimAssessment(artifact: ResearchArtifact, claims: string[]): ResearchArtifact {
|
|
270
|
-
return { ...artifact, claims: claims.map((claim) => assessClaim(claim, artifact.passages)) };
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
export function storeResearchArtifact(artifact: ResearchArtifact): void {
|
|
274
|
-
if (!artifact.id) throw new Error("Research artifact id must not be empty");
|
|
275
|
-
storeResult(artifact.id, { id: artifact.id, type: "research", timestamp: artifact.timestamp, artifact });
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
export function getResearchArtifact(id: string): ResearchArtifact | null {
|
|
279
|
-
const data = getResult(id);
|
|
280
|
-
if (!data || data.type !== "research" || !data.artifact || typeof data.artifact !== "object") return null;
|
|
281
|
-
return data.artifact as ResearchArtifact;
|
|
282
|
-
}
|
|
1
|
+
// Structured source checking and machine-readable research artifacts.
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { generateId, getResult, storeResult } from "./storage.ts";
|
|
4
|
+
import type { SearchResult } from "./search-types.ts";
|
|
5
|
+
import type { ExtractedContent } from "./extract.ts";
|
|
6
|
+
|
|
7
|
+
export type SourceQuality = "official_docs" | "vendor_docs" | "repo_issue" | "blog" | "forum" | "news" | "unknown";
|
|
8
|
+
export type ClaimStatus = "supported" | "contradicted" | "unclear" | "missing-evidence";
|
|
9
|
+
export type RecencyFilter = "day" | "week" | "month" | "year";
|
|
10
|
+
|
|
11
|
+
export interface ResearchSource {
|
|
12
|
+
rank: number;
|
|
13
|
+
url: string;
|
|
14
|
+
title: string;
|
|
15
|
+
snippet?: string;
|
|
16
|
+
fetch_timestamp?: number;
|
|
17
|
+
content_hash?: string;
|
|
18
|
+
quality: SourceQuality;
|
|
19
|
+
fetched?: boolean;
|
|
20
|
+
fetch_error?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ResearchPassage {
|
|
24
|
+
passage_id: string;
|
|
25
|
+
source_url: string;
|
|
26
|
+
source_rank: number;
|
|
27
|
+
text: string;
|
|
28
|
+
extraction_span?: { start: number; end: number };
|
|
29
|
+
content_hash?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ClaimAssessment {
|
|
33
|
+
claim: string;
|
|
34
|
+
status: ClaimStatus;
|
|
35
|
+
supporting_passages: string[];
|
|
36
|
+
contradicting_passages: string[];
|
|
37
|
+
rationale: string;
|
|
38
|
+
confidence: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ResearchArtifact {
|
|
42
|
+
id: string;
|
|
43
|
+
type: "research";
|
|
44
|
+
timestamp: number;
|
|
45
|
+
query: string;
|
|
46
|
+
sources: ResearchSource[];
|
|
47
|
+
passages: ResearchPassage[];
|
|
48
|
+
claims?: ClaimAssessment[];
|
|
49
|
+
provider?: string;
|
|
50
|
+
summary?: string;
|
|
51
|
+
content_hash?: string;
|
|
52
|
+
filters?: { recency?: RecencyFilter; domain_include?: string[]; domain_exclude?: string[] };
|
|
53
|
+
errors?: Array<{ query: string; error: string }>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ResearchSearchRequest {
|
|
57
|
+
query: string;
|
|
58
|
+
numResults: number;
|
|
59
|
+
recencyFilter?: RecencyFilter;
|
|
60
|
+
domainFilter?: string[];
|
|
61
|
+
}
|
|
62
|
+
export interface ResearchSearchResult { url: string; title: string; snippet: string; rank: number }
|
|
63
|
+
export interface ResearchSearchResponse { provider: string; results: ResearchSearchResult[]; summary?: string }
|
|
64
|
+
export interface ResearchProvider { name: string; search(req: ResearchSearchRequest): Promise<ResearchSearchResponse> }
|
|
65
|
+
|
|
66
|
+
const OFFICIAL_DOCS_HOSTS = /^(developers\.|docs\.|learn\.|reference\.)|\.github\.io$/i;
|
|
67
|
+
const OFFICIAL_DOCS_PATHS = /\/(docs?|reference)(\/|\b)/i;
|
|
68
|
+
const VENDOR_DOCS_PATHS = /\/(documentation|docs?)\//i;
|
|
69
|
+
const REPO_ISSUE_PATHS = /\/(issues|pull|pulls)\//i;
|
|
70
|
+
const BLOG_HOSTS = /(medium\.com|substack\.com|dev\.to|hashnode\.)/i;
|
|
71
|
+
const BLOG_PATHS = /\/blogs?\//i;
|
|
72
|
+
const FORUM_HOSTS = /(stackoverflow\.com|serverfault\.com|superuser\.com|discourse\.|community\.)/i;
|
|
73
|
+
const FORUM_PATHS = /\/(forum|forums|threads)\//i;
|
|
74
|
+
const NEWS_HOSTS = /(reuters\.com|bloomberg\.com|techcrunch\.com|theverge\.com|arstechnica\.com|wired\.com|cnet\.com|zdnet\.com)/i;
|
|
75
|
+
const NEWS_PATHS = /\/news(\/|$)/i;
|
|
76
|
+
|
|
77
|
+
export function classifySource(url: string): SourceQuality {
|
|
78
|
+
let host = "";
|
|
79
|
+
let path = "";
|
|
80
|
+
try {
|
|
81
|
+
const parsed = new URL(url);
|
|
82
|
+
host = parsed.hostname;
|
|
83
|
+
path = parsed.pathname;
|
|
84
|
+
} catch {
|
|
85
|
+
return "unknown";
|
|
86
|
+
}
|
|
87
|
+
if (REPO_ISSUE_PATHS.test(path)) return "repo_issue";
|
|
88
|
+
if (OFFICIAL_DOCS_HOSTS.test(host) || OFFICIAL_DOCS_PATHS.test(path)) return "official_docs";
|
|
89
|
+
if (VENDOR_DOCS_PATHS.test(path)) return "vendor_docs";
|
|
90
|
+
if (NEWS_HOSTS.test(host) || NEWS_PATHS.test(path)) return "news";
|
|
91
|
+
if (FORUM_HOSTS.test(host) || FORUM_PATHS.test(path)) return "forum";
|
|
92
|
+
if (BLOG_HOSTS.test(host) || BLOG_PATHS.test(path)) return "blog";
|
|
93
|
+
return "unknown";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function hashContent(text: string): string {
|
|
97
|
+
return `sha256:${createHash("sha256").update(text, "utf8").digest("hex")}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface Span { text: string; start: number; end: number }
|
|
101
|
+
|
|
102
|
+
function tokenize(value: string): string[] {
|
|
103
|
+
return [...new Set(value.toLowerCase().split(/[^a-z0-9]+/).filter((term) => term.length > 3))];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function extractRelevantSpans(content: string, hint: string): Span[] {
|
|
107
|
+
const sentences: Span[] = [];
|
|
108
|
+
const sentencePattern = /[^.!?]+(?:[.!?]+(?=\s|$)|$)/g;
|
|
109
|
+
for (const match of content.matchAll(sentencePattern)) {
|
|
110
|
+
const raw = match[0];
|
|
111
|
+
const text = raw.trim();
|
|
112
|
+
if (text.length > 0 && text.length <= 400) {
|
|
113
|
+
const start = (match.index ?? 0) + raw.indexOf(text);
|
|
114
|
+
sentences.push({ text, start, end: start + text.length });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const terms = tokenize(hint);
|
|
118
|
+
if (terms.length === 0) return [];
|
|
119
|
+
return sentences
|
|
120
|
+
.map((sentence, index) => ({ sentence, index, score: terms.filter((term) => sentence.text.toLowerCase().includes(term)).length }))
|
|
121
|
+
.filter((item) => item.score > 0)
|
|
122
|
+
.sort((a, b) => b.score - a.score || a.index - b.index)
|
|
123
|
+
.slice(0, 3)
|
|
124
|
+
.map(({ sentence }) => sentence);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function passageId(sourceRank: number, index: number): string {
|
|
128
|
+
return `p-${sourceRank}-${index}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function buildPassages(sources: ResearchSource[], fetched: ExtractedContent[] = [], hint = ""): ResearchPassage[] {
|
|
132
|
+
const passages: ResearchPassage[] = [];
|
|
133
|
+
const fetchedByUrl = new Map(fetched.map((item) => [item.url, item]));
|
|
134
|
+
for (const source of sources) {
|
|
135
|
+
if (source.snippet) {
|
|
136
|
+
passages.push({
|
|
137
|
+
passage_id: passageId(source.rank, 0),
|
|
138
|
+
source_url: source.url,
|
|
139
|
+
source_rank: source.rank,
|
|
140
|
+
text: source.snippet,
|
|
141
|
+
content_hash: hashContent(source.snippet),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
const page = fetchedByUrl.get(source.url);
|
|
145
|
+
if (page && !page.error && page.content) {
|
|
146
|
+
const passageHint = source.snippet?.trim() || hint;
|
|
147
|
+
for (const [index, span] of extractRelevantSpans(page.content, passageHint).entries()) {
|
|
148
|
+
passages.push({
|
|
149
|
+
passage_id: passageId(source.rank, index + 1),
|
|
150
|
+
source_url: source.url,
|
|
151
|
+
source_rank: source.rank,
|
|
152
|
+
text: span.text,
|
|
153
|
+
extraction_span: { start: span.start, end: span.end },
|
|
154
|
+
content_hash: hashContent(span.text),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return passages;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const CONTRADICTION_MARKERS = ["not true", "false", "incorrect", "debunked", "retracted", "no longer", "never", "denied", "contrary", "misleading"];
|
|
163
|
+
const SUPPORT_MARKERS = ["yes", "true", "correct", "confirmed", "according to", "shows that", "demonstrates", "reported", "verified", "established"];
|
|
164
|
+
|
|
165
|
+
function containsPhrase(value: string, phrase: string): boolean {
|
|
166
|
+
const escaped = phrase.trim().toLowerCase().split(/\s+/).map((term) => term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\s+");
|
|
167
|
+
return new RegExp(`(?:^|[^a-z0-9])${escaped}(?=$|[^a-z0-9])`, "i").test(value);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function markerIsNegated(value: string, marker: string): boolean {
|
|
171
|
+
const escaped = marker.trim().toLowerCase().split(/\s+/).map((term) => term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\s+");
|
|
172
|
+
const markerPattern = new RegExp(`(?:^|[^a-z0-9])${escaped}(?=$|[^a-z0-9])`, "i");
|
|
173
|
+
const match = markerPattern.exec(value);
|
|
174
|
+
if (!match || match.index === undefined) return false;
|
|
175
|
+
const matchedMarker = match[0].replace(/^[^a-z0-9]+/i, "");
|
|
176
|
+
const beforeMarker = value.slice(0, match.index + match[0].length - matchedMarker.length);
|
|
177
|
+
return /(?:^|[^a-z0-9])(?:not|no|never|without)\s+$/i.test(beforeMarker);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function hasPolarityMarker(value: string, markers: string[], allowNegated = false): boolean {
|
|
181
|
+
return markers.some((marker) => containsPhrase(value, marker) && (allowNegated || !markerIsNegated(value, marker)));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function assessClaim(claim: string, passages: ResearchPassage[]): ClaimAssessment {
|
|
185
|
+
const terms = tokenize(claim);
|
|
186
|
+
if (terms.length === 0 || passages.length === 0) {
|
|
187
|
+
return { claim, status: "missing-evidence", supporting_passages: [], contradicting_passages: [], rationale: "No passages available that discuss the claim's terms.", confidence: 0.2 };
|
|
188
|
+
}
|
|
189
|
+
const supporting: string[] = [];
|
|
190
|
+
const contradicting: string[] = [];
|
|
191
|
+
for (const passage of passages) {
|
|
192
|
+
const lower = passage.text.toLowerCase();
|
|
193
|
+
const overlap = terms.filter((term) => containsPhrase(lower, term)).length;
|
|
194
|
+
if (overlap < Math.max(2, Math.ceil(terms.length / 4))) continue;
|
|
195
|
+
const contra = hasPolarityMarker(lower, CONTRADICTION_MARKERS);
|
|
196
|
+
const support = hasPolarityMarker(lower, SUPPORT_MARKERS);
|
|
197
|
+
if (contra && !support) contradicting.push(passage.passage_id);
|
|
198
|
+
else if (support && !contra) supporting.push(passage.passage_id);
|
|
199
|
+
}
|
|
200
|
+
if (contradicting.length > 0 && supporting.length === 0) {
|
|
201
|
+
return { claim, status: "contradicted", supporting_passages: [], contradicting_passages: contradicting, rationale: `${contradicting.length} passage(s) contradict the claim; none support it.`, confidence: Math.min(0.85, 0.5 + contradicting.length * 0.1) };
|
|
202
|
+
}
|
|
203
|
+
if (supporting.length > 0 && contradicting.length === 0) {
|
|
204
|
+
return { claim, status: "supported", supporting_passages: supporting, contradicting_passages: [], rationale: `${supporting.length} passage(s) support the claim; none contradict it.`, confidence: Math.min(0.85, 0.5 + supporting.length * 0.1) };
|
|
205
|
+
}
|
|
206
|
+
if (supporting.length > 0 || contradicting.length > 0) {
|
|
207
|
+
return { claim, status: "unclear", supporting_passages: supporting, contradicting_passages: contradicting, rationale: `${supporting.length} supporting and ${contradicting.length} contradicting passage(s); evidence is mixed.`, confidence: 0.4 };
|
|
208
|
+
}
|
|
209
|
+
return { claim, status: "unclear", supporting_passages: [], contradicting_passages: [], rationale: "Passages mention the claim's terms but contain no clear support or contradiction markers.", confidence: 0.3 };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
interface RankedSearchResult extends SearchResult {
|
|
213
|
+
rank?: number;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export interface BuildArtifactInput {
|
|
217
|
+
query: string;
|
|
218
|
+
provider?: string;
|
|
219
|
+
summary?: string;
|
|
220
|
+
results: RankedSearchResult[];
|
|
221
|
+
fetched?: ExtractedContent[];
|
|
222
|
+
recency?: RecencyFilter;
|
|
223
|
+
domainFilter?: string[];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function buildResearchArtifact(input: BuildArtifactInput): ResearchArtifact {
|
|
227
|
+
const filters = input.domainFilter ?? [];
|
|
228
|
+
const fetchedByUrl = new Map((input.fetched ?? []).map((page) => [page.url, page]));
|
|
229
|
+
const sources: ResearchSource[] = [];
|
|
230
|
+
const seen = new Set<string>();
|
|
231
|
+
for (const [index, result] of input.results.entries()) {
|
|
232
|
+
if (seen.has(result.url)) continue;
|
|
233
|
+
seen.add(result.url);
|
|
234
|
+
const page = fetchedByUrl.get(result.url);
|
|
235
|
+
const fetched = Boolean(page && !page.error);
|
|
236
|
+
sources.push({
|
|
237
|
+
rank: result.rank ?? index + 1,
|
|
238
|
+
url: result.url,
|
|
239
|
+
title: result.title,
|
|
240
|
+
snippet: result.snippet,
|
|
241
|
+
quality: classifySource(result.url),
|
|
242
|
+
fetched,
|
|
243
|
+
...(page ? { fetch_timestamp: Date.now() } : {}),
|
|
244
|
+
...(page && !page.error ? { content_hash: hashContent(page.content) } : {}),
|
|
245
|
+
...(page?.error ? { fetch_error: page.error } : {}),
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
const passages = buildPassages(sources, input.fetched, input.query);
|
|
249
|
+
const domainInclude = filters.filter((domain) => !domain.startsWith("-"));
|
|
250
|
+
const domainExclude = filters.filter((domain) => domain.startsWith("-")).map((domain) => domain.slice(1));
|
|
251
|
+
return {
|
|
252
|
+
id: generateId(),
|
|
253
|
+
type: "research",
|
|
254
|
+
timestamp: Date.now(),
|
|
255
|
+
query: input.query,
|
|
256
|
+
sources,
|
|
257
|
+
passages,
|
|
258
|
+
...(input.provider !== undefined ? { provider: input.provider } : {}),
|
|
259
|
+
...(input.summary !== undefined ? { summary: input.summary } : {}),
|
|
260
|
+
...(passages.length > 0 ? { content_hash: hashContent(passages.map((passage) => passage.text).join("\n")) } : {}),
|
|
261
|
+
filters: {
|
|
262
|
+
...(input.recency !== undefined ? { recency: input.recency } : {}),
|
|
263
|
+
domain_include: domainInclude,
|
|
264
|
+
domain_exclude: domainExclude,
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function withClaimAssessment(artifact: ResearchArtifact, claims: string[]): ResearchArtifact {
|
|
270
|
+
return { ...artifact, claims: claims.map((claim) => assessClaim(claim, artifact.passages)) };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function storeResearchArtifact(artifact: ResearchArtifact): void {
|
|
274
|
+
if (!artifact.id) throw new Error("Research artifact id must not be empty");
|
|
275
|
+
storeResult(artifact.id, { id: artifact.id, type: "research", timestamp: artifact.timestamp, artifact });
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function getResearchArtifact(id: string): ResearchArtifact | null {
|
|
279
|
+
const data = getResult(id);
|
|
280
|
+
if (!data || data.type !== "research" || !data.artifact || typeof data.artifact !== "object") return null;
|
|
281
|
+
return data.artifact as ResearchArtifact;
|
|
282
|
+
}
|