freshcontext-mcp 0.3.16 → 0.3.18
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/.env.example +3 -0
- package/LICENSE +21 -0
- package/NOTICE.md +17 -0
- package/README.md +395 -296
- package/SECURITY.md +34 -0
- package/TRADEMARKS.md +9 -0
- package/dist/adapters/arxiv.js +92 -48
- package/dist/adapters/finance.js +87 -101
- package/dist/adapters/gdelt.js +1 -1
- package/dist/adapters/gebiz.js +1 -1
- package/dist/adapters/hackernews.js +59 -29
- package/dist/adapters/productHunt.js +8 -4
- package/dist/adapters/registry.js +232 -0
- package/dist/adapters/repoSearch.js +1 -1
- package/dist/adapters/secFilings.js +1 -1
- package/dist/core/decay.js +61 -0
- package/dist/core/decision.js +176 -0
- package/dist/core/envelope.js +59 -0
- package/dist/core/explain.js +28 -0
- package/dist/core/guards.js +17 -0
- package/dist/core/index.js +11 -0
- package/dist/core/pipeline.js +101 -0
- package/dist/core/provenance.js +73 -0
- package/dist/core/rank.js +84 -0
- package/dist/core/signal.js +101 -0
- package/dist/core/sourceProfiles.js +126 -0
- package/dist/core/types.js +1 -0
- package/dist/core/utility.js +90 -0
- package/dist/rest/handler.js +126 -0
- package/dist/security.js +1 -1
- package/dist/server.js +10 -10
- package/dist/tools/freshnessStamp.js +1 -117
- package/dist/types.js +0 -1
- package/docs/API_DESIGN.md +434 -0
- package/docs/CODEX_MCP_USAGE.md +116 -0
- package/docs/CORE_API.md +224 -0
- package/docs/DEPENDENCY_DILIGENCE.md +63 -0
- package/docs/HA_PRI_V2_DESIGN.md +279 -0
- package/docs/OPERATIONAL_DEMO_RUNBOOK.md +458 -0
- package/docs/RELEASE_INTEGRITY.md +53 -0
- package/docs/RELEASE_NOTES.md +38 -0
- package/docs/SIGNAL_CONTRACT.md +89 -0
- package/docs/SOURCE_PROFILES.md +427 -0
- package/freshcontext.schema.json +103 -103
- package/package-script-guard.mjs +140 -0
- package/package.json +92 -52
- package/server.json +27 -28
- package/.github/workflows/publish.yml +0 -32
- package/RESEARCH.md +0 -487
- package/RISKS.md +0 -137
- package/cleanup.ps1 +0 -99
- package/demo/README.md +0 -70
- package/demo/data.json +0 -88
- package/demo/generate.mjs +0 -199
- package/demo/index.html +0 -513
- package/demo/logo-export.html +0 -61
- package/demo/logo.svg +0 -23
- package/dist/apify.js +0 -133
- package/freshcontext-validate.js +0 -196
- package/time-check.ps1 +0 -46
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
function descriptor(input) {
|
|
2
|
+
return Object.freeze({
|
|
3
|
+
...input,
|
|
4
|
+
secondary_source_profiles: input.secondary_source_profiles
|
|
5
|
+
? Object.freeze([...input.secondary_source_profiles])
|
|
6
|
+
: undefined,
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
function copyDescriptor(descriptor) {
|
|
10
|
+
return {
|
|
11
|
+
...descriptor,
|
|
12
|
+
secondary_source_profiles: descriptor.secondary_source_profiles
|
|
13
|
+
? [...descriptor.secondary_source_profiles]
|
|
14
|
+
: undefined,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export const BUILT_IN_ADAPTER_REGISTRY = Object.freeze([
|
|
18
|
+
descriptor({
|
|
19
|
+
adapter_id: "github",
|
|
20
|
+
tool_name: "extract_github",
|
|
21
|
+
source_profile: "code_activity",
|
|
22
|
+
output_mode: "single",
|
|
23
|
+
runtime_kind: "browser",
|
|
24
|
+
risk: "medium",
|
|
25
|
+
notes: "Repository page extraction uses browser automation; keep behavior compatibility pinned before signal extraction.",
|
|
26
|
+
}),
|
|
27
|
+
descriptor({
|
|
28
|
+
adapter_id: "google_scholar",
|
|
29
|
+
tool_name: "extract_scholar",
|
|
30
|
+
source_profile: "academic_research",
|
|
31
|
+
output_mode: "batch",
|
|
32
|
+
runtime_kind: "browser",
|
|
33
|
+
risk: "medium",
|
|
34
|
+
notes: "Scholar extraction is browser-backed and date precision is usually year-level.",
|
|
35
|
+
}),
|
|
36
|
+
descriptor({
|
|
37
|
+
adapter_id: "hackernews",
|
|
38
|
+
tool_name: "extract_hackernews",
|
|
39
|
+
source_profile: "social_pulse",
|
|
40
|
+
output_mode: "batch",
|
|
41
|
+
runtime_kind: "mixed",
|
|
42
|
+
risk: "medium",
|
|
43
|
+
notes: "Plain query path uses Algolia API; URL extraction can use browser automation.",
|
|
44
|
+
}),
|
|
45
|
+
descriptor({
|
|
46
|
+
adapter_id: "yc",
|
|
47
|
+
tool_name: "extract_yc",
|
|
48
|
+
source_profile: "company_intel",
|
|
49
|
+
output_mode: "batch",
|
|
50
|
+
runtime_kind: "browser",
|
|
51
|
+
risk: "medium",
|
|
52
|
+
notes: "YC company listing extraction is browser-backed.",
|
|
53
|
+
}),
|
|
54
|
+
descriptor({
|
|
55
|
+
adapter_id: "reposearch",
|
|
56
|
+
tool_name: "search_repos",
|
|
57
|
+
source_profile: "code_activity",
|
|
58
|
+
output_mode: "batch",
|
|
59
|
+
runtime_kind: "api",
|
|
60
|
+
risk: "low",
|
|
61
|
+
notes: "GitHub repository search API result set; good early signal-output candidate.",
|
|
62
|
+
}),
|
|
63
|
+
descriptor({
|
|
64
|
+
adapter_id: "packagetrends",
|
|
65
|
+
tool_name: "package_trends",
|
|
66
|
+
source_profile: "code_activity",
|
|
67
|
+
secondary_source_profiles: ["official_docs"],
|
|
68
|
+
output_mode: "batch",
|
|
69
|
+
runtime_kind: "api",
|
|
70
|
+
risk: "low",
|
|
71
|
+
notes: "Registry metadata for npm and PyPI packages.",
|
|
72
|
+
}),
|
|
73
|
+
descriptor({
|
|
74
|
+
adapter_id: "arxiv",
|
|
75
|
+
tool_name: "extract_arxiv",
|
|
76
|
+
source_profile: "academic_research",
|
|
77
|
+
output_mode: "batch",
|
|
78
|
+
runtime_kind: "api",
|
|
79
|
+
risk: "low",
|
|
80
|
+
notes: "Official API with clear paper timestamps; recommended first extraction target.",
|
|
81
|
+
}),
|
|
82
|
+
descriptor({
|
|
83
|
+
adapter_id: "finance",
|
|
84
|
+
tool_name: "extract_finance",
|
|
85
|
+
source_profile: "market_finance",
|
|
86
|
+
output_mode: "batch",
|
|
87
|
+
runtime_kind: "api",
|
|
88
|
+
risk: "medium",
|
|
89
|
+
notes: "Quote freshness and partial-failure semantics need careful compatibility coverage.",
|
|
90
|
+
}),
|
|
91
|
+
descriptor({
|
|
92
|
+
adapter_id: "reddit",
|
|
93
|
+
tool_name: "extract_reddit",
|
|
94
|
+
source_profile: "social_pulse",
|
|
95
|
+
output_mode: "batch",
|
|
96
|
+
runtime_kind: "api",
|
|
97
|
+
risk: "medium",
|
|
98
|
+
notes: "Public JSON API with community-content volatility.",
|
|
99
|
+
}),
|
|
100
|
+
descriptor({
|
|
101
|
+
adapter_id: "producthunt",
|
|
102
|
+
tool_name: "extract_producthunt",
|
|
103
|
+
source_profile: "social_pulse",
|
|
104
|
+
output_mode: "batch",
|
|
105
|
+
runtime_kind: "mixed",
|
|
106
|
+
risk: "medium",
|
|
107
|
+
notes: "Uses optional API path with browser fallback.",
|
|
108
|
+
}),
|
|
109
|
+
descriptor({
|
|
110
|
+
adapter_id: "landscape",
|
|
111
|
+
tool_name: "extract_landscape",
|
|
112
|
+
source_profile: "composite_landscape",
|
|
113
|
+
secondary_source_profiles: ["company_intel", "code_activity", "social_pulse"],
|
|
114
|
+
output_mode: "composite",
|
|
115
|
+
runtime_kind: "composite",
|
|
116
|
+
risk: "high",
|
|
117
|
+
notes: "Composite report should preserve section-level source profiles before extraction.",
|
|
118
|
+
}),
|
|
119
|
+
descriptor({
|
|
120
|
+
adapter_id: "jobs",
|
|
121
|
+
tool_name: "search_jobs",
|
|
122
|
+
source_profile: "jobs_opportunities",
|
|
123
|
+
output_mode: "batch",
|
|
124
|
+
runtime_kind: "api",
|
|
125
|
+
risk: "medium",
|
|
126
|
+
notes: "Multi-source job aggregation with filters and strict recency expectations.",
|
|
127
|
+
}),
|
|
128
|
+
descriptor({
|
|
129
|
+
adapter_id: "changelog",
|
|
130
|
+
tool_name: "extract_changelog",
|
|
131
|
+
source_profile: "official_docs",
|
|
132
|
+
secondary_source_profiles: ["code_activity"],
|
|
133
|
+
output_mode: "batch",
|
|
134
|
+
runtime_kind: "mixed",
|
|
135
|
+
risk: "medium",
|
|
136
|
+
notes: "GitHub releases and registry paths are API-backed; website discovery can use browser automation.",
|
|
137
|
+
}),
|
|
138
|
+
descriptor({
|
|
139
|
+
adapter_id: "govcontracts",
|
|
140
|
+
tool_name: "extract_govcontracts",
|
|
141
|
+
source_profile: "government_regulatory",
|
|
142
|
+
output_mode: "batch",
|
|
143
|
+
runtime_kind: "api",
|
|
144
|
+
risk: "medium",
|
|
145
|
+
notes: "Official API; direct API URL compatibility and award-date semantics need coverage.",
|
|
146
|
+
}),
|
|
147
|
+
descriptor({
|
|
148
|
+
adapter_id: "gov_landscape",
|
|
149
|
+
tool_name: "extract_gov_landscape",
|
|
150
|
+
source_profile: "composite_landscape",
|
|
151
|
+
secondary_source_profiles: ["government_regulatory", "code_activity", "social_pulse", "official_docs"],
|
|
152
|
+
output_mode: "composite",
|
|
153
|
+
runtime_kind: "composite",
|
|
154
|
+
risk: "high",
|
|
155
|
+
notes: "Composite government report stitches multiple source profiles.",
|
|
156
|
+
}),
|
|
157
|
+
descriptor({
|
|
158
|
+
adapter_id: "finance_landscape",
|
|
159
|
+
tool_name: "extract_finance_landscape",
|
|
160
|
+
source_profile: "composite_landscape",
|
|
161
|
+
secondary_source_profiles: ["market_finance", "social_pulse", "code_activity", "official_docs"],
|
|
162
|
+
output_mode: "composite",
|
|
163
|
+
runtime_kind: "composite",
|
|
164
|
+
risk: "high",
|
|
165
|
+
notes: "Composite finance report must not collapse market and social freshness into one policy.",
|
|
166
|
+
}),
|
|
167
|
+
descriptor({
|
|
168
|
+
adapter_id: "sec_filings",
|
|
169
|
+
tool_name: "extract_sec_filings",
|
|
170
|
+
source_profile: "government_regulatory",
|
|
171
|
+
output_mode: "batch",
|
|
172
|
+
runtime_kind: "api",
|
|
173
|
+
risk: "low",
|
|
174
|
+
notes: "Official SEC API with clear filing dates.",
|
|
175
|
+
}),
|
|
176
|
+
descriptor({
|
|
177
|
+
adapter_id: "gdelt",
|
|
178
|
+
tool_name: "extract_gdelt",
|
|
179
|
+
source_profile: "government_regulatory",
|
|
180
|
+
secondary_source_profiles: ["company_intel"],
|
|
181
|
+
output_mode: "batch",
|
|
182
|
+
runtime_kind: "api",
|
|
183
|
+
risk: "medium",
|
|
184
|
+
notes: "Global news intelligence has fast-moving timestamps and broad source variance.",
|
|
185
|
+
}),
|
|
186
|
+
descriptor({
|
|
187
|
+
adapter_id: "company_landscape",
|
|
188
|
+
tool_name: "extract_company_landscape",
|
|
189
|
+
source_profile: "composite_landscape",
|
|
190
|
+
secondary_source_profiles: ["company_intel", "government_regulatory", "market_finance", "official_docs"],
|
|
191
|
+
output_mode: "composite",
|
|
192
|
+
runtime_kind: "composite",
|
|
193
|
+
risk: "high",
|
|
194
|
+
notes: "Composite company report combines official, market, news, and product velocity signals.",
|
|
195
|
+
}),
|
|
196
|
+
descriptor({
|
|
197
|
+
adapter_id: "gebiz",
|
|
198
|
+
tool_name: "extract_gebiz",
|
|
199
|
+
source_profile: "government_regulatory",
|
|
200
|
+
output_mode: "batch",
|
|
201
|
+
runtime_kind: "api",
|
|
202
|
+
risk: "low",
|
|
203
|
+
notes: "Official data.gov.sg procurement dataset.",
|
|
204
|
+
}),
|
|
205
|
+
descriptor({
|
|
206
|
+
adapter_id: "idea_landscape",
|
|
207
|
+
tool_name: "extract_idea_landscape",
|
|
208
|
+
source_profile: "composite_landscape",
|
|
209
|
+
secondary_source_profiles: ["social_pulse", "company_intel", "code_activity", "jobs_opportunities"],
|
|
210
|
+
output_mode: "composite",
|
|
211
|
+
runtime_kind: "composite",
|
|
212
|
+
risk: "high",
|
|
213
|
+
notes: "Composite idea validation report stitches social, funding, code, jobs, package, and launch signals.",
|
|
214
|
+
}),
|
|
215
|
+
]);
|
|
216
|
+
export function listAdapterDescriptors() {
|
|
217
|
+
return BUILT_IN_ADAPTER_REGISTRY.map(copyDescriptor);
|
|
218
|
+
}
|
|
219
|
+
export function getAdapterDescriptor(adapterIdOrToolName) {
|
|
220
|
+
const descriptor = BUILT_IN_ADAPTER_REGISTRY.find((item) => item.adapter_id === adapterIdOrToolName || item.tool_name === adapterIdOrToolName);
|
|
221
|
+
return descriptor ? copyDescriptor(descriptor) : undefined;
|
|
222
|
+
}
|
|
223
|
+
export function listAdaptersBySourceProfile(profileId) {
|
|
224
|
+
return BUILT_IN_ADAPTER_REGISTRY
|
|
225
|
+
.filter((item) => item.source_profile === profileId || item.secondary_source_profiles?.includes(profileId))
|
|
226
|
+
.map(copyDescriptor);
|
|
227
|
+
}
|
|
228
|
+
export function listAdaptersByRisk(risk) {
|
|
229
|
+
return BUILT_IN_ADAPTER_REGISTRY
|
|
230
|
+
.filter((item) => item.risk === risk)
|
|
231
|
+
.map(copyDescriptor);
|
|
232
|
+
}
|
|
@@ -22,7 +22,7 @@ export async function repoSearchAdapter(options) {
|
|
|
22
22
|
const res = await fetch(apiUrl, {
|
|
23
23
|
headers: {
|
|
24
24
|
Accept: "application/vnd.github.v3+json",
|
|
25
|
-
"User-Agent": "freshcontext-mcp/0.
|
|
25
|
+
"User-Agent": "freshcontext-mcp/0.3.17 (https://github.com/PrinceGabriel-lgtm/freshcontext-mcp)",
|
|
26
26
|
},
|
|
27
27
|
});
|
|
28
28
|
if (!res.ok) {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
const HEADERS = {
|
|
14
14
|
"Accept": "application/json",
|
|
15
|
-
"User-Agent": "freshcontext-mcp/
|
|
15
|
+
"User-Agent": "freshcontext-mcp/0.3.17 (https://github.com/PrinceGabriel-lgtm/freshcontext-mcp)",
|
|
16
16
|
};
|
|
17
17
|
async function fetchSecFilings(query, maxResults = 10) {
|
|
18
18
|
const today = new Date().toISOString().slice(0, 10);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Spec-compliant exponential DAR model.
|
|
2
|
+
// Higher lambda = data goes stale faster. Half-life formula: t1/2 = ln(2) / lambda.
|
|
3
|
+
// Lambda is measured per hour and mirrors the Worker/D1 intelligence engine.
|
|
4
|
+
export const LAMBDA = {
|
|
5
|
+
hackernews: 0.050,
|
|
6
|
+
reddit: 0.010,
|
|
7
|
+
producthunt: 0.010,
|
|
8
|
+
jobs: 0.005,
|
|
9
|
+
finance: 0.001,
|
|
10
|
+
yc: 0.001,
|
|
11
|
+
packagetrends: 0.0005,
|
|
12
|
+
github: 0.0002,
|
|
13
|
+
reposearch: 0.0002,
|
|
14
|
+
google_scholar: 0.00005,
|
|
15
|
+
arxiv: 0.00005,
|
|
16
|
+
changelog: 0.0005,
|
|
17
|
+
gdelt: 0.020,
|
|
18
|
+
gebiz: 0.003,
|
|
19
|
+
govcontracts: 0.001,
|
|
20
|
+
sec_filings: 0.005,
|
|
21
|
+
landscape: 0.050,
|
|
22
|
+
gov_landscape: 0.001,
|
|
23
|
+
finance_landscape: 0.001,
|
|
24
|
+
company_landscape: 0.005,
|
|
25
|
+
idea_landscape: 0.050,
|
|
26
|
+
default: 0.001,
|
|
27
|
+
};
|
|
28
|
+
export const FUTURE_CLOCK_SKEW_TOLERANCE_MS = 5 * 60 * 1000;
|
|
29
|
+
export function isMeaningfullyFutureDate(content_date, retrieved_at) {
|
|
30
|
+
if (!content_date)
|
|
31
|
+
return false;
|
|
32
|
+
const published = new Date(content_date).getTime();
|
|
33
|
+
const retrieved = new Date(retrieved_at).getTime();
|
|
34
|
+
if (isNaN(published) || isNaN(retrieved))
|
|
35
|
+
return false;
|
|
36
|
+
return published - retrieved > FUTURE_CLOCK_SKEW_TOLERANCE_MS;
|
|
37
|
+
}
|
|
38
|
+
export function calculateFreshnessScore(content_date, retrieved_at, adapter) {
|
|
39
|
+
if (!content_date)
|
|
40
|
+
return null;
|
|
41
|
+
const published = new Date(content_date).getTime();
|
|
42
|
+
const retrieved = new Date(retrieved_at).getTime();
|
|
43
|
+
if (isNaN(published) || isNaN(retrieved))
|
|
44
|
+
return null;
|
|
45
|
+
if (published - retrieved > FUTURE_CLOCK_SKEW_TOLERANCE_MS)
|
|
46
|
+
return null;
|
|
47
|
+
const hoursSinceRetrieved = Math.max(0, (retrieved - published) / (1000 * 60 * 60));
|
|
48
|
+
const lambda = LAMBDA[adapter] ?? LAMBDA.default;
|
|
49
|
+
return Math.max(0, Math.round(100 * Math.exp(-lambda * hoursSinceRetrieved)));
|
|
50
|
+
}
|
|
51
|
+
export function scoreLabel(score) {
|
|
52
|
+
if (score === null)
|
|
53
|
+
return "unknown";
|
|
54
|
+
if (score >= 90)
|
|
55
|
+
return "current";
|
|
56
|
+
if (score >= 70)
|
|
57
|
+
return "reliable";
|
|
58
|
+
if (score >= 50)
|
|
59
|
+
return "verify before acting";
|
|
60
|
+
return "use with caution";
|
|
61
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { getSourceProfile } from "./sourceProfiles.js";
|
|
2
|
+
const CITATION_INTENTS = new Set(["citation_check", "student_research"]);
|
|
3
|
+
const STRICT_REFRESH_PROFILES = new Set(["market_finance", "jobs_opportunities"]);
|
|
4
|
+
function resolveSourceProfile(profile) {
|
|
5
|
+
if (!profile)
|
|
6
|
+
return undefined;
|
|
7
|
+
return typeof profile === "string" ? getSourceProfile(profile) : profile;
|
|
8
|
+
}
|
|
9
|
+
function profileId(profile) {
|
|
10
|
+
return profile?.profile_id;
|
|
11
|
+
}
|
|
12
|
+
function unique(values) {
|
|
13
|
+
return [...new Set(values.filter(Boolean))];
|
|
14
|
+
}
|
|
15
|
+
function hasFailureReason(evaluation) {
|
|
16
|
+
return evaluation.reasons.some((reason) => /\b(?:failed|failure|timeout|error|blocked|upstream)\b/i.test(reason));
|
|
17
|
+
}
|
|
18
|
+
function isCitationIntent(intent) {
|
|
19
|
+
return intent !== undefined && CITATION_INTENTS.has(intent);
|
|
20
|
+
}
|
|
21
|
+
function nonAdviceWarnings(intent) {
|
|
22
|
+
switch (intent) {
|
|
23
|
+
case "citation_check":
|
|
24
|
+
case "student_research":
|
|
25
|
+
return ["FreshContext judges citation readiness and context usefulness; it does not certify truth."];
|
|
26
|
+
case "medical_literature_triage":
|
|
27
|
+
return ["FreshContext provides literature triage only; it is not medical advice."];
|
|
28
|
+
case "market_watch":
|
|
29
|
+
return ["FreshContext provides market signal triage only; it is not investment advice."];
|
|
30
|
+
case "business_due_diligence":
|
|
31
|
+
return ["FreshContext supports context triage only; it is not legal, tax, or investment advice."];
|
|
32
|
+
case "job_search":
|
|
33
|
+
return ["FreshContext provides opportunity triage only; it is not employment or legal advice."];
|
|
34
|
+
default:
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function decisionResult(decision, reasons, warnings) {
|
|
39
|
+
const copyReasons = unique(reasons);
|
|
40
|
+
const copyWarnings = unique(warnings);
|
|
41
|
+
switch (decision) {
|
|
42
|
+
case "use_first":
|
|
43
|
+
return {
|
|
44
|
+
decision,
|
|
45
|
+
label: "Use first",
|
|
46
|
+
meaning: "This is strong, current context for the task.",
|
|
47
|
+
action: "Use this near the top of the context bundle.",
|
|
48
|
+
reasons: copyReasons,
|
|
49
|
+
warnings: copyWarnings,
|
|
50
|
+
};
|
|
51
|
+
case "cite_as_primary":
|
|
52
|
+
return {
|
|
53
|
+
decision,
|
|
54
|
+
label: "Cite as primary",
|
|
55
|
+
meaning: "This source is relevant, current, and traceable enough to use as main evidence.",
|
|
56
|
+
action: "Use it as primary citation evidence, while keeping normal source-review standards.",
|
|
57
|
+
reasons: copyReasons,
|
|
58
|
+
warnings: copyWarnings,
|
|
59
|
+
};
|
|
60
|
+
case "cite_as_supporting":
|
|
61
|
+
return {
|
|
62
|
+
decision,
|
|
63
|
+
label: "Cite as supporting",
|
|
64
|
+
meaning: "This source is useful evidence, but should not be the only or latest support.",
|
|
65
|
+
action: "Use it as supporting evidence and pair it with stronger or newer sources.",
|
|
66
|
+
reasons: copyReasons,
|
|
67
|
+
warnings: copyWarnings,
|
|
68
|
+
};
|
|
69
|
+
case "use_as_background":
|
|
70
|
+
return {
|
|
71
|
+
decision,
|
|
72
|
+
label: "Use as background",
|
|
73
|
+
meaning: "This source is relevant context, but not strong enough for latest-evidence claims.",
|
|
74
|
+
action: "Use it for framing, history, or background rather than as the main current source.",
|
|
75
|
+
reasons: copyReasons,
|
|
76
|
+
warnings: copyWarnings,
|
|
77
|
+
};
|
|
78
|
+
case "needs_verification":
|
|
79
|
+
return {
|
|
80
|
+
decision,
|
|
81
|
+
label: "Needs verification",
|
|
82
|
+
meaning: "This source may be useful, but its date, confidence, or traceability is uncertain.",
|
|
83
|
+
action: "Verify the source details before citing it, acting on it, or sending it to a model as trusted context.",
|
|
84
|
+
reasons: copyReasons,
|
|
85
|
+
warnings: copyWarnings,
|
|
86
|
+
};
|
|
87
|
+
case "needs_refresh":
|
|
88
|
+
return {
|
|
89
|
+
decision,
|
|
90
|
+
label: "Needs refresh",
|
|
91
|
+
meaning: "This source may be useful, but it is too stale or date-uncertain for this source type.",
|
|
92
|
+
action: "Refresh or re-query this source before relying on it as current context.",
|
|
93
|
+
reasons: copyReasons,
|
|
94
|
+
warnings: copyWarnings,
|
|
95
|
+
};
|
|
96
|
+
case "watch_only":
|
|
97
|
+
return {
|
|
98
|
+
decision,
|
|
99
|
+
label: "Watch only",
|
|
100
|
+
meaning: "This is an interesting signal, but not strong enough to prioritize.",
|
|
101
|
+
action: "Monitor it or keep it as a weak signal; do not use it as main evidence.",
|
|
102
|
+
reasons: copyReasons,
|
|
103
|
+
warnings: copyWarnings,
|
|
104
|
+
};
|
|
105
|
+
case "exclude":
|
|
106
|
+
return {
|
|
107
|
+
decision,
|
|
108
|
+
label: "Exclude",
|
|
109
|
+
meaning: "This source is failed, too weak, or unsafe to include as useful context.",
|
|
110
|
+
action: "Keep it out of the final context bundle unless a human explicitly reviews it.",
|
|
111
|
+
reasons: copyReasons,
|
|
112
|
+
warnings: copyWarnings,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export function interpretEvaluation(evaluation, options = {}) {
|
|
117
|
+
const sourceProfile = resolveSourceProfile(options.sourceProfile);
|
|
118
|
+
const sourceProfileId = profileId(sourceProfile);
|
|
119
|
+
const intentProfile = options.intentProfile;
|
|
120
|
+
const reasons = unique([
|
|
121
|
+
evaluation.explanation,
|
|
122
|
+
...evaluation.signal.reasons,
|
|
123
|
+
...evaluation.utility.reasons,
|
|
124
|
+
...evaluation.reasons,
|
|
125
|
+
]);
|
|
126
|
+
const warnings = [...nonAdviceWarnings(intentProfile)];
|
|
127
|
+
const finalScore = evaluation.ranked.final_score;
|
|
128
|
+
const utilityScore = evaluation.utility.score;
|
|
129
|
+
const freshnessScore = evaluation.freshness_score;
|
|
130
|
+
const confidence = evaluation.ranked.confidence;
|
|
131
|
+
const isFailed = evaluation.signal.status === "failed"
|
|
132
|
+
|| (confidence === "low" && hasFailureReason(evaluation));
|
|
133
|
+
if (sourceProfile) {
|
|
134
|
+
reasons.push(`source profile ${sourceProfile.profile_id} uses ${sourceProfile.date_policy} date policy`);
|
|
135
|
+
}
|
|
136
|
+
if (intentProfile) {
|
|
137
|
+
reasons.push(`intent profile ${intentProfile} selected`);
|
|
138
|
+
}
|
|
139
|
+
if (isFailed) {
|
|
140
|
+
return decisionResult("exclude", reasons, warnings);
|
|
141
|
+
}
|
|
142
|
+
if (sourceProfileId
|
|
143
|
+
&& STRICT_REFRESH_PROFILES.has(sourceProfileId)
|
|
144
|
+
&& (freshnessScore === null || freshnessScore < 50)) {
|
|
145
|
+
return decisionResult("needs_refresh", reasons, warnings);
|
|
146
|
+
}
|
|
147
|
+
if (evaluation.signal.date_confidence === "unknown") {
|
|
148
|
+
if (sourceProfileId === "academic_research" && finalScore >= 0.75) {
|
|
149
|
+
return decisionResult(isCitationIntent(intentProfile) ? "cite_as_supporting" : "use_as_background", reasons, warnings);
|
|
150
|
+
}
|
|
151
|
+
return decisionResult("needs_verification", reasons, warnings);
|
|
152
|
+
}
|
|
153
|
+
if (finalScore >= 0.85
|
|
154
|
+
&& freshnessScore !== null
|
|
155
|
+
&& freshnessScore >= 70
|
|
156
|
+
&& utilityScore >= 60
|
|
157
|
+
&& confidence === "high") {
|
|
158
|
+
if (sourceProfile?.authority_hint === "high" && isCitationIntent(intentProfile)) {
|
|
159
|
+
return decisionResult("cite_as_primary", reasons, warnings);
|
|
160
|
+
}
|
|
161
|
+
return decisionResult("use_first", reasons, warnings);
|
|
162
|
+
}
|
|
163
|
+
if (finalScore >= 0.55 && freshnessScore !== null && freshnessScore < 50) {
|
|
164
|
+
return decisionResult(isCitationIntent(intentProfile) ? "cite_as_supporting" : "use_as_background", reasons, warnings);
|
|
165
|
+
}
|
|
166
|
+
if (finalScore < 0.35 && utilityScore < 30) {
|
|
167
|
+
return decisionResult(confidence === "low" ? "exclude" : "watch_only", reasons, warnings);
|
|
168
|
+
}
|
|
169
|
+
if (finalScore >= 0.55) {
|
|
170
|
+
return decisionResult("use_as_background", reasons, warnings);
|
|
171
|
+
}
|
|
172
|
+
return decisionResult("watch_only", reasons, warnings);
|
|
173
|
+
}
|
|
174
|
+
export function interpretEvaluations(evaluations, options = {}) {
|
|
175
|
+
return evaluations.map((evaluation) => interpretEvaluation(evaluation, options));
|
|
176
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { calculateFreshnessScore, isMeaningfullyFutureDate, scoreLabel } from "./decay.js";
|
|
2
|
+
import { looksLikeFailedAdapterContent } from "./guards.js";
|
|
3
|
+
export function stampFreshness(result, options, adapter) {
|
|
4
|
+
const retrieved_at = new Date().toISOString();
|
|
5
|
+
const failedContent = looksLikeFailedAdapterContent(result.raw);
|
|
6
|
+
const content_date = failedContent ? null : result.content_date;
|
|
7
|
+
const futureDated = !failedContent && isMeaningfullyFutureDate(content_date, retrieved_at);
|
|
8
|
+
const freshness_confidence = failedContent || futureDated ? "low" : result.freshness_confidence;
|
|
9
|
+
const freshness_score = calculateFreshnessScore(content_date, retrieved_at, adapter);
|
|
10
|
+
return {
|
|
11
|
+
content: result.raw.slice(0, options.maxLength ?? 8000),
|
|
12
|
+
source_url: options.url,
|
|
13
|
+
content_date,
|
|
14
|
+
retrieved_at,
|
|
15
|
+
freshness_confidence,
|
|
16
|
+
freshness_score,
|
|
17
|
+
adapter,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function toStructuredJSON(ctx) {
|
|
21
|
+
return {
|
|
22
|
+
freshcontext: {
|
|
23
|
+
source_url: ctx.source_url,
|
|
24
|
+
content_date: ctx.content_date,
|
|
25
|
+
retrieved_at: ctx.retrieved_at,
|
|
26
|
+
freshness_confidence: ctx.freshness_confidence,
|
|
27
|
+
freshness_score: ctx.freshness_score,
|
|
28
|
+
adapter: ctx.adapter,
|
|
29
|
+
},
|
|
30
|
+
content: ctx.content,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function formatForLLM(ctx, options = {}) {
|
|
34
|
+
const publishedLabel = options.publishedLabel ?? "Published";
|
|
35
|
+
const unknownDateText = options.unknownDateText ?? "Publish date: unknown";
|
|
36
|
+
const dateInfo = ctx.content_date
|
|
37
|
+
? `${publishedLabel}: ${ctx.content_date}`
|
|
38
|
+
: unknownDateText;
|
|
39
|
+
const scoreLine = ctx.freshness_score !== null
|
|
40
|
+
? `Score: ${ctx.freshness_score}/100 (${scoreLabel(ctx.freshness_score)})`
|
|
41
|
+
: `Score: unknown`;
|
|
42
|
+
const textEnvelope = [
|
|
43
|
+
`[FRESHCONTEXT]`,
|
|
44
|
+
`Source: ${ctx.source_url}`,
|
|
45
|
+
`${dateInfo}`,
|
|
46
|
+
`Retrieved: ${ctx.retrieved_at}`,
|
|
47
|
+
`Confidence: ${ctx.freshness_confidence}`,
|
|
48
|
+
`${scoreLine}`,
|
|
49
|
+
`---`,
|
|
50
|
+
ctx.content,
|
|
51
|
+
`[/FRESHCONTEXT]`,
|
|
52
|
+
].join("\n");
|
|
53
|
+
const jsonBlock = [
|
|
54
|
+
`[FRESHCONTEXT_JSON]`,
|
|
55
|
+
JSON.stringify(toStructuredJSON(ctx), null, 2),
|
|
56
|
+
`[/FRESHCONTEXT_JSON]`,
|
|
57
|
+
].join("\n");
|
|
58
|
+
return `${textEnvelope}\n\n${jsonBlock}`;
|
|
59
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
function sourceLabel(input) {
|
|
2
|
+
return input.source_type || input.source || "source";
|
|
3
|
+
}
|
|
4
|
+
export function explainSignal(input) {
|
|
5
|
+
const source = sourceLabel(input);
|
|
6
|
+
if (input.freshness_score === null) {
|
|
7
|
+
if (input.semantic_score < 0.5) {
|
|
8
|
+
return `Low confidence: weak semantic match and missing freshness data for ${source}.`;
|
|
9
|
+
}
|
|
10
|
+
return `Missing freshness data for ${source}; ranked mostly by semantic relevance.`;
|
|
11
|
+
}
|
|
12
|
+
if (input.semantic_score < 0.5) {
|
|
13
|
+
if (input.freshness_score >= 70) {
|
|
14
|
+
return `Fresh signal from ${source}, but semantic relevance is weak.`;
|
|
15
|
+
}
|
|
16
|
+
return `Weak semantic match with limited freshness for ${source}.`;
|
|
17
|
+
}
|
|
18
|
+
if (input.freshness_score >= 90) {
|
|
19
|
+
return `Strong semantic match and current freshness for ${source}.`;
|
|
20
|
+
}
|
|
21
|
+
if (input.freshness_score >= 70) {
|
|
22
|
+
return `Relevant signal with reliable freshness for ${source}.`;
|
|
23
|
+
}
|
|
24
|
+
if (input.freshness_score >= 50) {
|
|
25
|
+
return `Relevant signal, but freshness should be verified for ${source}.`;
|
|
26
|
+
}
|
|
27
|
+
return `Relevant signal, but stale for ${source}.`;
|
|
28
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function looksLikeFailedAdapterContent(raw) {
|
|
2
|
+
const trimmed = raw.trim();
|
|
3
|
+
if (!trimmed)
|
|
4
|
+
return true;
|
|
5
|
+
if (/^\[(?:error|security)\]/i.test(trimmed))
|
|
6
|
+
return true;
|
|
7
|
+
if (/^(?:error|failed|upstream|timeout)\b/i.test(trimmed))
|
|
8
|
+
return true;
|
|
9
|
+
const meaningful = trimmed
|
|
10
|
+
.split(/\r?\n/)
|
|
11
|
+
.map((line) => line.trim())
|
|
12
|
+
.filter(Boolean);
|
|
13
|
+
if (!meaningful.length)
|
|
14
|
+
return true;
|
|
15
|
+
const failureLines = meaningful.filter((line) => /\b(?:error|failed|failure|timeout|401|403|404|429|5\d\d)\b/i.test(line));
|
|
16
|
+
return failureLines.length === meaningful.length;
|
|
17
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { LAMBDA, calculateFreshnessScore, scoreLabel } from "./decay.js";
|
|
2
|
+
export { looksLikeFailedAdapterContent } from "./guards.js";
|
|
3
|
+
export { stampFreshness, toStructuredJSON, formatForLLM } from "./envelope.js";
|
|
4
|
+
export { explainSignal } from "./explain.js";
|
|
5
|
+
export { rankSignals, rankSignal, clampScore } from "./rank.js";
|
|
6
|
+
export { calculateContextUtility } from "./utility.js";
|
|
7
|
+
export { SIGNAL_CONTRACT_VERSION, normalizeSignal } from "./signal.js";
|
|
8
|
+
export { evaluateSignal, evaluateSignals } from "./pipeline.js";
|
|
9
|
+
export { interpretEvaluation, interpretEvaluations } from "./decision.js";
|
|
10
|
+
export { BUILT_IN_SOURCE_PROFILES, getSourceProfile, listSourceProfiles } from "./sourceProfiles.js";
|
|
11
|
+
export { canonicalizeHaPriContent, sha256Hex, calculateHaPriV2, verifyHaPriV2, } from "./provenance.js";
|