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.
Files changed (60) hide show
  1. package/.env.example +3 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE.md +17 -0
  4. package/README.md +395 -296
  5. package/SECURITY.md +34 -0
  6. package/TRADEMARKS.md +9 -0
  7. package/dist/adapters/arxiv.js +92 -48
  8. package/dist/adapters/finance.js +87 -101
  9. package/dist/adapters/gdelt.js +1 -1
  10. package/dist/adapters/gebiz.js +1 -1
  11. package/dist/adapters/hackernews.js +59 -29
  12. package/dist/adapters/productHunt.js +8 -4
  13. package/dist/adapters/registry.js +232 -0
  14. package/dist/adapters/repoSearch.js +1 -1
  15. package/dist/adapters/secFilings.js +1 -1
  16. package/dist/core/decay.js +61 -0
  17. package/dist/core/decision.js +176 -0
  18. package/dist/core/envelope.js +59 -0
  19. package/dist/core/explain.js +28 -0
  20. package/dist/core/guards.js +17 -0
  21. package/dist/core/index.js +11 -0
  22. package/dist/core/pipeline.js +101 -0
  23. package/dist/core/provenance.js +73 -0
  24. package/dist/core/rank.js +84 -0
  25. package/dist/core/signal.js +101 -0
  26. package/dist/core/sourceProfiles.js +126 -0
  27. package/dist/core/types.js +1 -0
  28. package/dist/core/utility.js +90 -0
  29. package/dist/rest/handler.js +126 -0
  30. package/dist/security.js +1 -1
  31. package/dist/server.js +10 -10
  32. package/dist/tools/freshnessStamp.js +1 -117
  33. package/dist/types.js +0 -1
  34. package/docs/API_DESIGN.md +434 -0
  35. package/docs/CODEX_MCP_USAGE.md +116 -0
  36. package/docs/CORE_API.md +224 -0
  37. package/docs/DEPENDENCY_DILIGENCE.md +63 -0
  38. package/docs/HA_PRI_V2_DESIGN.md +279 -0
  39. package/docs/OPERATIONAL_DEMO_RUNBOOK.md +458 -0
  40. package/docs/RELEASE_INTEGRITY.md +53 -0
  41. package/docs/RELEASE_NOTES.md +38 -0
  42. package/docs/SIGNAL_CONTRACT.md +89 -0
  43. package/docs/SOURCE_PROFILES.md +427 -0
  44. package/freshcontext.schema.json +103 -103
  45. package/package-script-guard.mjs +140 -0
  46. package/package.json +92 -52
  47. package/server.json +27 -28
  48. package/.github/workflows/publish.yml +0 -32
  49. package/RESEARCH.md +0 -487
  50. package/RISKS.md +0 -137
  51. package/cleanup.ps1 +0 -99
  52. package/demo/README.md +0 -70
  53. package/demo/data.json +0 -88
  54. package/demo/generate.mjs +0 -199
  55. package/demo/index.html +0 -513
  56. package/demo/logo-export.html +0 -61
  57. package/demo/logo.svg +0 -23
  58. package/dist/apify.js +0 -133
  59. package/freshcontext-validate.js +0 -196
  60. 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.1.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/1.0 contact@freshcontext.dev",
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";