freshcontext-mcp 0.3.17 → 0.3.19

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 (42) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +17 -0
  3. package/README.md +459 -296
  4. package/SECURITY.md +34 -0
  5. package/TRADEMARKS.md +9 -0
  6. package/dist/adapters/arxiv.js +92 -48
  7. package/dist/adapters/hackernews.js +16 -16
  8. package/dist/adapters/registry.js +232 -0
  9. package/dist/core/decay.js +61 -0
  10. package/dist/core/decision.js +176 -0
  11. package/dist/core/envelope.js +59 -0
  12. package/dist/core/explain.js +28 -0
  13. package/dist/core/guards.js +17 -0
  14. package/dist/core/index.js +11 -0
  15. package/dist/core/pipeline.js +101 -0
  16. package/dist/core/provenance.js +73 -0
  17. package/dist/core/rank.js +84 -0
  18. package/dist/core/signal.js +101 -0
  19. package/dist/core/sourceProfiles.js +126 -0
  20. package/dist/core/types.js +1 -0
  21. package/dist/core/utility.js +90 -0
  22. package/dist/rest/handler.js +126 -0
  23. package/dist/server.js +40 -2
  24. package/dist/tools/evaluateContext.js +127 -0
  25. package/dist/tools/freshnessStamp.js +1 -137
  26. package/dist/types.js +0 -1
  27. package/docs/API_DESIGN.md +434 -0
  28. package/docs/CODEX_MCP_USAGE.md +116 -0
  29. package/docs/CORE_API.md +226 -0
  30. package/docs/CORE_MCP_BOUNDARY.md +106 -0
  31. package/docs/DEPENDENCY_DILIGENCE.md +63 -0
  32. package/docs/FUTURE_LANES.md +173 -0
  33. package/docs/HA_PRI_V2_DESIGN.md +279 -0
  34. package/docs/RELEASE_INTEGRITY.md +55 -0
  35. package/docs/RELEASE_NOTES.md +55 -0
  36. package/docs/SIGNAL_CONTRACT.md +89 -0
  37. package/docs/SOURCE_PROFILES.md +427 -0
  38. package/freshcontext.schema.json +103 -103
  39. package/package-script-guard.mjs +141 -0
  40. package/package.json +94 -59
  41. package/server.json +27 -28
  42. package/dist/apify.js +0 -133
@@ -0,0 +1,126 @@
1
+ import { LAMBDA } from "./decay.js";
2
+ function halfLifeHours(lambda) {
3
+ return Number((Math.log(2) / lambda).toFixed(2));
4
+ }
5
+ function profile(input) {
6
+ return {
7
+ ...input,
8
+ half_life_hours: halfLifeHours(input.default_decay_lambda),
9
+ };
10
+ }
11
+ function copyProfile(profile) {
12
+ return {
13
+ ...profile,
14
+ source_types: [...profile.source_types],
15
+ recommended_surfaces: [...profile.recommended_surfaces],
16
+ };
17
+ }
18
+ export const BUILT_IN_SOURCE_PROFILES = Object.freeze({
19
+ official_docs: profile({
20
+ profile_id: "official_docs",
21
+ source_types: ["official_docs", "changelog", "packagetrends"],
22
+ purpose: "Official product docs, API docs, standards, changelogs, and canonical source material.",
23
+ default_decay_lambda: LAMBDA.github,
24
+ authority_hint: "high",
25
+ date_policy: "balanced",
26
+ failure_policy: "warn",
27
+ recommended_surfaces: ["rest", "sdk", "cli", "operator"],
28
+ }),
29
+ code_activity: profile({
30
+ profile_id: "code_activity",
31
+ source_types: ["github", "reposearch", "changelog", "packagetrends"],
32
+ purpose: "Repository activity, release cadence, dependency health, and implementation evidence.",
33
+ default_decay_lambda: LAMBDA.github,
34
+ authority_hint: "medium",
35
+ date_policy: "balanced",
36
+ failure_policy: "downgrade",
37
+ recommended_surfaces: ["mcp", "rest", "sdk", "cli", "operator"],
38
+ }),
39
+ social_pulse: profile({
40
+ profile_id: "social_pulse",
41
+ source_types: ["hackernews", "reddit", "producthunt"],
42
+ purpose: "Community awareness, social proof, launch momentum, and early-market signal.",
43
+ default_decay_lambda: LAMBDA.hackernews,
44
+ authority_hint: "medium",
45
+ date_policy: "strict",
46
+ failure_policy: "downgrade",
47
+ recommended_surfaces: ["mcp", "rest", "sdk", "operator"],
48
+ }),
49
+ academic_research: profile({
50
+ profile_id: "academic_research",
51
+ source_types: ["google_scholar", "arxiv"],
52
+ purpose: "Scholarly material, papers, research abstracts, and citation-oriented context.",
53
+ default_decay_lambda: LAMBDA.arxiv,
54
+ authority_hint: "high",
55
+ date_policy: "lenient",
56
+ failure_policy: "warn",
57
+ recommended_surfaces: ["mcp", "rest", "sdk", "cli", "operator"],
58
+ }),
59
+ market_finance: profile({
60
+ profile_id: "market_finance",
61
+ source_types: ["finance", "finance_landscape"],
62
+ purpose: "Market prices, quotes, financial movement, and finance-specific situational awareness.",
63
+ default_decay_lambda: LAMBDA.finance,
64
+ authority_hint: "medium",
65
+ date_policy: "strict",
66
+ failure_policy: "exclude",
67
+ recommended_surfaces: ["mcp", "rest", "sdk", "operator"],
68
+ }),
69
+ jobs_opportunities: profile({
70
+ profile_id: "jobs_opportunities",
71
+ source_types: ["jobs"],
72
+ purpose: "Job listings, openings, hiring signals, and opportunity windows.",
73
+ default_decay_lambda: LAMBDA.jobs,
74
+ authority_hint: "medium",
75
+ date_policy: "strict",
76
+ failure_policy: "downgrade",
77
+ recommended_surfaces: ["mcp", "rest", "sdk", "cli", "operator"],
78
+ }),
79
+ government_regulatory: profile({
80
+ profile_id: "government_regulatory",
81
+ source_types: ["govcontracts", "sec_filings", "gebiz", "gdelt", "gov_landscape"],
82
+ purpose: "Public-sector contracts, official filings, tenders, regulatory disclosures, and global news intelligence.",
83
+ default_decay_lambda: LAMBDA.govcontracts,
84
+ authority_hint: "high",
85
+ date_policy: "strict",
86
+ failure_policy: "warn",
87
+ recommended_surfaces: ["mcp", "rest", "sdk", "operator"],
88
+ }),
89
+ company_intel: profile({
90
+ profile_id: "company_intel",
91
+ source_types: ["yc", "company_landscape"],
92
+ purpose: "Company research, product velocity, ecosystem activity, and competitive context.",
93
+ default_decay_lambda: LAMBDA.company_landscape,
94
+ authority_hint: "medium",
95
+ date_policy: "balanced",
96
+ failure_policy: "downgrade",
97
+ recommended_surfaces: ["mcp", "rest", "sdk", "operator"],
98
+ }),
99
+ composite_landscape: profile({
100
+ profile_id: "composite_landscape",
101
+ source_types: ["landscape", "idea_landscape", "gov_landscape", "finance_landscape", "company_landscape"],
102
+ purpose: "Multi-source validation and idea, company, market, government, or finance landscape checks.",
103
+ default_decay_lambda: LAMBDA.landscape,
104
+ authority_hint: "medium",
105
+ date_policy: "balanced",
106
+ failure_policy: "warn",
107
+ recommended_surfaces: ["mcp", "rest", "sdk", "operator"],
108
+ }),
109
+ local_custom: profile({
110
+ profile_id: "local_custom",
111
+ source_types: ["local_custom", "user_provided", "custom"],
112
+ purpose: "User-provided content and custom signals supplied explicitly by a host or caller.",
113
+ default_decay_lambda: LAMBDA.default,
114
+ authority_hint: "medium",
115
+ date_policy: "balanced",
116
+ failure_policy: "warn",
117
+ recommended_surfaces: ["rest", "sdk", "cli", "operator"],
118
+ }),
119
+ });
120
+ export function listSourceProfiles() {
121
+ return Object.values(BUILT_IN_SOURCE_PROFILES).map(copyProfile);
122
+ }
123
+ export function getSourceProfile(profileId) {
124
+ const profile = BUILT_IN_SOURCE_PROFILES[profileId];
125
+ return profile ? copyProfile(profile) : undefined;
126
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,90 @@
1
+ const DATE_CONFIDENCE_FACTORS = {
2
+ high: 1.0,
3
+ medium: 0.75,
4
+ low: 0.4,
5
+ unknown: 0.0,
6
+ };
7
+ const STATUS_FACTORS = {
8
+ success: 1.0,
9
+ partial: 0.65,
10
+ stale: 0.4,
11
+ failed: 0.0,
12
+ unknown: 0.5,
13
+ };
14
+ function clampRelevance(value, reasons) {
15
+ if (!Number.isFinite(value)) {
16
+ reasons.push("contextual relevance was not finite; clamped to 0");
17
+ return 0;
18
+ }
19
+ if (value > 100) {
20
+ reasons.push("contextual relevance exceeded 100; clamped to 100");
21
+ return 100;
22
+ }
23
+ if (value < 0) {
24
+ reasons.push("contextual relevance was below 0; clamped to 0");
25
+ return 0;
26
+ }
27
+ return value;
28
+ }
29
+ function safeLambda(value, reasons) {
30
+ if (!Number.isFinite(value) || value < 0) {
31
+ reasons.push("lambda was invalid; clamped to 0");
32
+ return 0;
33
+ }
34
+ return value;
35
+ }
36
+ function safeAgeHours(value, reasons) {
37
+ if (!Number.isFinite(value)) {
38
+ reasons.push("ageHours was not finite; clamped to 0");
39
+ return 0;
40
+ }
41
+ if (value < 0) {
42
+ reasons.push("ageHours was negative; clamped to 0");
43
+ return 0;
44
+ }
45
+ return value;
46
+ }
47
+ export function calculateContextUtility(input) {
48
+ const reasons = [];
49
+ const contextualRelevance = clampRelevance(input.contextualRelevance, reasons);
50
+ const lambda = safeLambda(input.lambda, reasons);
51
+ const ageHours = safeAgeHours(input.ageHours, reasons);
52
+ const dateConfidence = input.dateConfidence ?? "unknown";
53
+ const status = input.status ?? "unknown";
54
+ const decayFactor = Math.exp(-lambda * ageHours);
55
+ const dateConfidenceFactor = DATE_CONFIDENCE_FACTORS[dateConfidence];
56
+ const statusFactor = STATUS_FACTORS[status];
57
+ if (dateConfidence === "medium") {
58
+ reasons.push("timestamp confidence is medium; utility reduced");
59
+ }
60
+ else if (dateConfidence === "low") {
61
+ reasons.push("timestamp confidence is low; utility reduced");
62
+ }
63
+ else if (dateConfidence === "unknown") {
64
+ reasons.push("timestamp confidence is unknown; utility reduced to zero");
65
+ }
66
+ if (status === "partial") {
67
+ reasons.push("signal status is partial; utility reduced");
68
+ }
69
+ else if (status === "stale") {
70
+ reasons.push("signal status is stale; utility reduced");
71
+ }
72
+ else if (status === "failed") {
73
+ reasons.push("signal status is failed; utility reduced to zero");
74
+ }
75
+ else if (status === "unknown") {
76
+ reasons.push("signal status is unknown; utility reduced");
77
+ }
78
+ const score = Math.min(100, Math.max(0, contextualRelevance * decayFactor * dateConfidenceFactor * statusFactor));
79
+ return {
80
+ score,
81
+ contextualRelevance,
82
+ decayFactor,
83
+ dateConfidenceFactor,
84
+ statusFactor,
85
+ lambda,
86
+ ageHours,
87
+ status,
88
+ reasons,
89
+ };
90
+ }
@@ -0,0 +1,126 @@
1
+ import { evaluateSignal, evaluateSignals } from "../core/index.js";
2
+ const SERVICE_VERSION = "0.1.0";
3
+ const JSON_CONTENT_TYPE = "application/json";
4
+ const MAX_BODY_BYTES = 256 * 1024;
5
+ function jsonResponse(body, status = 200) {
6
+ return new Response(JSON.stringify(body), {
7
+ status,
8
+ headers: { "Content-Type": JSON_CONTENT_TYPE },
9
+ });
10
+ }
11
+ function errorResponse(code, message, status, details = []) {
12
+ return jsonResponse({ error: { code, message, details } }, status);
13
+ }
14
+ function methodNotAllowed(allowed) {
15
+ return new Response(JSON.stringify({
16
+ error: {
17
+ code: "method_not_allowed",
18
+ message: `Method not allowed. Use ${allowed}.`,
19
+ details: [],
20
+ },
21
+ }), {
22
+ status: 405,
23
+ headers: {
24
+ "Content-Type": JSON_CONTENT_TYPE,
25
+ "Allow": allowed,
26
+ },
27
+ });
28
+ }
29
+ function isJsonContentType(request) {
30
+ return (request.headers.get("Content-Type") ?? "").toLowerCase().includes(JSON_CONTENT_TYPE);
31
+ }
32
+ function isRecord(value) {
33
+ return typeof value === "object" && value !== null && !Array.isArray(value);
34
+ }
35
+ async function readJsonBody(request) {
36
+ if (!isJsonContentType(request)) {
37
+ return {
38
+ ok: false,
39
+ response: errorResponse("unsupported_media_type", "POST requests require Content-Type: application/json.", 415),
40
+ };
41
+ }
42
+ const contentLength = request.headers.get("Content-Length");
43
+ if (contentLength !== null && Number(contentLength) > MAX_BODY_BYTES) {
44
+ return {
45
+ ok: false,
46
+ response: errorResponse("payload_too_large", `Request body exceeds ${MAX_BODY_BYTES} bytes.`, 413),
47
+ };
48
+ }
49
+ const text = await request.text();
50
+ if (new TextEncoder().encode(text).length > MAX_BODY_BYTES) {
51
+ return {
52
+ ok: false,
53
+ response: errorResponse("payload_too_large", `Request body exceeds ${MAX_BODY_BYTES} bytes.`, 413),
54
+ };
55
+ }
56
+ try {
57
+ const parsed = JSON.parse(text);
58
+ if (!isRecord(parsed)) {
59
+ return {
60
+ ok: false,
61
+ response: errorResponse("invalid_request", "Request body must be a JSON object.", 400),
62
+ };
63
+ }
64
+ return { ok: true, body: parsed };
65
+ }
66
+ catch {
67
+ return {
68
+ ok: false,
69
+ response: errorResponse("invalid_request", "Request body must be valid JSON.", 400),
70
+ };
71
+ }
72
+ }
73
+ function optionsFromBody(body) {
74
+ if (body.options === undefined)
75
+ return undefined;
76
+ return isRecord(body.options) ? body.options : {};
77
+ }
78
+ async function handleEvaluate(request) {
79
+ if (request.method !== "POST")
80
+ return methodNotAllowed("POST");
81
+ const parsed = await readJsonBody(request);
82
+ if (!parsed.ok)
83
+ return parsed.response;
84
+ if (!isRecord(parsed.body.signal)) {
85
+ return errorResponse("invalid_request", "Request body must include signal.", 400);
86
+ }
87
+ const result = evaluateSignal(parsed.body.signal, optionsFromBody(parsed.body));
88
+ return jsonResponse(result);
89
+ }
90
+ async function handleEvaluateBatch(request) {
91
+ if (request.method !== "POST")
92
+ return methodNotAllowed("POST");
93
+ const parsed = await readJsonBody(request);
94
+ if (!parsed.ok)
95
+ return parsed.response;
96
+ if (!Array.isArray(parsed.body.signals)) {
97
+ return errorResponse("invalid_request", "Request body must include signals array.", 400);
98
+ }
99
+ const result = evaluateSignals(parsed.body.signals, optionsFromBody(parsed.body));
100
+ return jsonResponse({ evaluations: result });
101
+ }
102
+ function handleHealth(request) {
103
+ if (request.method !== "GET")
104
+ return methodNotAllowed("GET");
105
+ return jsonResponse({
106
+ ok: true,
107
+ service: "freshcontext-rest",
108
+ version: SERVICE_VERSION,
109
+ core_available: true,
110
+ });
111
+ }
112
+ export async function handleRestRequest(request) {
113
+ const url = new URL(request.url);
114
+ try {
115
+ if (url.pathname === "/v1/health")
116
+ return handleHealth(request);
117
+ if (url.pathname === "/v1/evaluate")
118
+ return handleEvaluate(request);
119
+ if (url.pathname === "/v1/evaluate-batch")
120
+ return handleEvaluateBatch(request);
121
+ return errorResponse("not_found", `Not found: ${url.pathname}.`, 404);
122
+ }
123
+ catch {
124
+ return errorResponse("internal_error", "Unexpected REST host error.", 500);
125
+ }
126
+ }
package/dist/server.js CHANGED
@@ -19,12 +19,50 @@ import { secFilingsAdapter } from "./adapters/secFilings.js";
19
19
  import { gdeltAdapter } from "./adapters/gdelt.js";
20
20
  import { gebizAdapter } from "./adapters/gebiz.js";
21
21
  import { stampFreshness, formatForLLM } from "./tools/freshnessStamp.js";
22
+ import { EvaluateContextInputError, evaluateContextInput, formatEvaluateContextResult, } from "./tools/evaluateContext.js";
22
23
  import { formatSecurityError } from "./security.js";
23
24
  const server = new McpServer({
24
25
  name: "freshcontext-mcp",
25
- version: "0.3.17",
26
+ version: "0.3.19",
26
27
  });
27
- // ─── Tool: extract_github ────────────────────────────────────────────────────
28
+ const signalInputSchema = z.object({
29
+ id: z.string().optional(),
30
+ source: z.string().min(1).describe("Source URL, URI, document id, or stable source label."),
31
+ source_type: z.string().optional().describe("Source type such as arxiv, jobs, official_docs, custom, or user_provided."),
32
+ title: z.string().optional(),
33
+ content: z.string().optional(),
34
+ published_at: z.string().nullable().optional(),
35
+ content_date: z.string().nullable().optional(),
36
+ retrieved_at: z.string().nullable().optional(),
37
+ semantic_score: z.number().optional().describe("Optional relevance score from 0..1. Core clamps out-of-range values."),
38
+ date_confidence: z.enum(["high", "medium", "low", "unknown"]).optional(),
39
+ freshness_confidence: z.enum(["high", "medium", "low"]).optional(),
40
+ status: z.enum(["success", "partial", "stale", "failed", "unknown"]).optional(),
41
+ metadata: z.record(z.unknown()).optional(),
42
+ }).passthrough();
43
+ // Tool: evaluate_context
44
+ server.registerTool("evaluate_context", {
45
+ description: "Evaluate caller-provided candidate context and return decision-ready output. This is the primary FreshContext judgment path: it does not fetch, crawl, scrape, browse, read folders, or call adapters.",
46
+ inputSchema: z.object({
47
+ profile: z.string().min(1).describe("Source Profile id, e.g. academic_research, jobs_opportunities, market_finance, official_docs, local_custom."),
48
+ intent: z.string().min(1).describe("Intent Profile id, e.g. citation_check, student_research, developer_adoption, job_search, market_watch, business_due_diligence, medical_literature_triage."),
49
+ signals: z.array(signalInputSchema).min(1).describe("Candidate context items provided by the caller. FreshContext evaluates these; it does not retrieve them."),
50
+ now: z.string().optional().describe("Optional ISO timestamp for deterministic evaluation."),
51
+ }),
52
+ annotations: { readOnlyHint: true, openWorldHint: false },
53
+ }, async ({ profile, intent, signals, now }) => {
54
+ try {
55
+ const result = evaluateContextInput({ profile, intent, signals, now });
56
+ return { content: [{ type: "text", text: formatEvaluateContextResult(result) }] };
57
+ }
58
+ catch (err) {
59
+ if (err instanceof EvaluateContextInputError) {
60
+ return { content: [{ type: "text", text: `[FreshContext evaluate_context error]\n${err.message}` }] };
61
+ }
62
+ return { content: [{ type: "text", text: formatSecurityError(err) }] };
63
+ }
64
+ });
65
+ // ─── Reference adapter: extract_github ───────────────────────────────────────
28
66
  server.registerTool("extract_github", {
29
67
  description: "Extract real-time data from a GitHub repository — README, stars, forks, language, topics, last commit. Returns timestamped freshcontext.",
30
68
  inputSchema: z.object({
@@ -0,0 +1,127 @@
1
+ import { evaluateSignals, getSourceProfile, interpretEvaluations, } from "../core/index.js";
2
+ const SUPPORTED_INTENTS = [
3
+ "citation_check",
4
+ "student_research",
5
+ "developer_adoption",
6
+ "job_search",
7
+ "market_watch",
8
+ "business_due_diligence",
9
+ "medical_literature_triage",
10
+ ];
11
+ export class EvaluateContextInputError extends Error {
12
+ constructor(message) {
13
+ super(message);
14
+ this.name = "EvaluateContextInputError";
15
+ }
16
+ }
17
+ function isRecord(value) {
18
+ return typeof value === "object" && value !== null && !Array.isArray(value);
19
+ }
20
+ function isIntentProfileId(value) {
21
+ return SUPPORTED_INTENTS.includes(value);
22
+ }
23
+ function validateSignal(value, index) {
24
+ if (!isRecord(value)) {
25
+ throw new EvaluateContextInputError(`signals[${index}] must be an object.`);
26
+ }
27
+ if (typeof value.source !== "string" || value.source.trim().length === 0) {
28
+ throw new EvaluateContextInputError(`signals[${index}].source must be a non-empty string.`);
29
+ }
30
+ if ((typeof value.title !== "string" || value.title.trim().length === 0)
31
+ && (typeof value.content !== "string" || value.content.trim().length === 0)) {
32
+ throw new EvaluateContextInputError(`signals[${index}] must include title or content.`);
33
+ }
34
+ return {
35
+ ...value,
36
+ source: value.source,
37
+ title: typeof value.title === "string" ? value.title : undefined,
38
+ content: typeof value.content === "string" ? value.content : undefined,
39
+ };
40
+ }
41
+ export function evaluateContextInput(input) {
42
+ const profile = getSourceProfile(input.profile);
43
+ if (!profile) {
44
+ throw new EvaluateContextInputError(`Unknown source profile: ${input.profile}.`);
45
+ }
46
+ if (!isIntentProfileId(input.intent)) {
47
+ throw new EvaluateContextInputError(`Unsupported intent profile: ${input.intent}.`);
48
+ }
49
+ if (!Array.isArray(input.signals)) {
50
+ throw new EvaluateContextInputError("signals must be an array.");
51
+ }
52
+ if (input.signals.length === 0) {
53
+ throw new EvaluateContextInputError("signals must contain at least one candidate context item.");
54
+ }
55
+ const signals = input.signals.map(validateSignal);
56
+ const options = input.now ? { now: input.now } : {};
57
+ const evaluations = evaluateSignals(signals, options);
58
+ const decisions = interpretEvaluations(evaluations, {
59
+ sourceProfile: profile,
60
+ intentProfile: input.intent,
61
+ });
62
+ return {
63
+ profile,
64
+ intent: input.intent,
65
+ items: evaluations.map((evaluation, index) => ({
66
+ evaluation,
67
+ decision: decisions[index],
68
+ })),
69
+ };
70
+ }
71
+ function sourceTitle(evaluation) {
72
+ if (evaluation.signal.title)
73
+ return evaluation.signal.title;
74
+ if (evaluation.signal.content)
75
+ return evaluation.signal.content.slice(0, 80);
76
+ return evaluation.signal.source;
77
+ }
78
+ function formatFreshness(score) {
79
+ return score === null ? "unknown" : `${Math.round(score)}/100`;
80
+ }
81
+ function formatRank(score) {
82
+ return score.toFixed(3);
83
+ }
84
+ function formatUtility(score) {
85
+ return `${Number(score.toFixed(1))}/100`;
86
+ }
87
+ function formatList(values) {
88
+ return values.length > 0 ? values.join("; ") : "None";
89
+ }
90
+ export function formatEvaluateContextResult(result) {
91
+ const lines = [
92
+ "FreshContext evaluate_context",
93
+ "Candidate context -> Core evaluation -> decision-ready context",
94
+ "",
95
+ `Profile: ${result.profile.profile_id}`,
96
+ `Purpose: ${result.profile.purpose}`,
97
+ `Intent: ${result.intent}`,
98
+ "",
99
+ ];
100
+ result.items.forEach((item, index) => {
101
+ const { evaluation, decision } = item;
102
+ lines.push(`${index + 1}. ${sourceTitle(evaluation)}`, ` Decision: ${decision.label}`, ` Meaning: ${decision.meaning}`, ` Action: ${decision.action}`, ` Warnings: ${formatList(decision.warnings)}`, ` Source: ${evaluation.signal.source}`, ` Freshness: ${formatFreshness(evaluation.freshness_score)}`, ` Rank score: ${formatRank(evaluation.ranked.final_score)}`, ` Utility: ${formatUtility(evaluation.utility.score)}`, ` Confidence: ${evaluation.ranked.confidence}`, ` Why: ${evaluation.explanation}`, "");
103
+ });
104
+ const structured = {
105
+ profile: result.profile.profile_id,
106
+ intent: result.intent,
107
+ results: result.items.map((item, index) => ({
108
+ index: index + 1,
109
+ title: sourceTitle(item.evaluation),
110
+ source: item.evaluation.signal.source,
111
+ source_type: item.evaluation.signal.source_type,
112
+ decision: item.decision.decision,
113
+ label: item.decision.label,
114
+ meaning: item.decision.meaning,
115
+ action: item.decision.action,
116
+ warnings: item.decision.warnings,
117
+ reasons: item.decision.reasons,
118
+ freshness_score: item.evaluation.freshness_score,
119
+ rank_score: item.evaluation.ranked.final_score,
120
+ utility_score: item.evaluation.utility.score,
121
+ confidence: item.evaluation.ranked.confidence,
122
+ why: item.evaluation.explanation,
123
+ })),
124
+ };
125
+ lines.push("[FRESHCONTEXT_EVALUATION_JSON]", JSON.stringify(structured, null, 2), "[/FRESHCONTEXT_EVALUATION_JSON]");
126
+ return lines.join("\n");
127
+ }
@@ -1,137 +1 @@
1
- // ─── Decay rates per adapter ──────────────────────────────────────────────────
2
- // Spec-compliant exponential DAR model.
3
- // Higher lambda = data goes stale faster. Half-life formula: t½ = ln(2) / λ.
4
- // Lambda is measured per hour and mirrors the Worker/D1 intelligence engine.
5
- export const LAMBDA = {
6
- hackernews: 0.050,
7
- reddit: 0.010,
8
- producthunt: 0.010,
9
- jobs: 0.005,
10
- finance: 0.001,
11
- yc: 0.001,
12
- packagetrends: 0.0005,
13
- github: 0.0002,
14
- reposearch: 0.0002,
15
- google_scholar: 0.00005,
16
- arxiv: 0.00005,
17
- changelog: 0.0005,
18
- gdelt: 0.020,
19
- gebiz: 0.003,
20
- govcontracts: 0.001,
21
- sec_filings: 0.005,
22
- landscape: 0.050,
23
- gov_landscape: 0.001,
24
- finance_landscape: 0.001,
25
- company_landscape: 0.005,
26
- idea_landscape: 0.050,
27
- default: 0.001,
28
- };
29
- // ─── Score calculation ────────────────────────────────────────────────────────
30
- // Returns null when content_date is unknown — we can't calculate age without a date.
31
- // Returns a clamped 0-100 exponential freshness score.
32
- function calculateFreshnessScore(content_date, retrieved_at, adapter) {
33
- if (!content_date)
34
- return null;
35
- const published = new Date(content_date).getTime();
36
- const retrieved = new Date(retrieved_at).getTime();
37
- // Guard against unparseable dates
38
- if (isNaN(published) || isNaN(retrieved))
39
- return null;
40
- const hoursSinceRetrieved = Math.max(0, (retrieved - published) / (1000 * 60 * 60));
41
- const lambda = LAMBDA[adapter] ?? LAMBDA.default;
42
- return Math.max(0, Math.round(100 * Math.exp(-lambda * hoursSinceRetrieved)));
43
- }
44
- // ─── Score label ──────────────────────────────────────────────────────────────
45
- // Human-readable interpretation alongside the number, per the spec.
46
- function scoreLabel(score) {
47
- if (score === null)
48
- return "unknown";
49
- if (score >= 90)
50
- return "current";
51
- if (score >= 70)
52
- return "reliable";
53
- if (score >= 50)
54
- return "verify before acting";
55
- return "use with caution";
56
- }
57
- function looksLikeFailedAdapterContent(raw) {
58
- const trimmed = raw.trim();
59
- if (!trimmed)
60
- return true;
61
- if (/^\[(?:error|security)\]/i.test(trimmed))
62
- return true;
63
- if (/^(?:error|failed|upstream|timeout)\b/i.test(trimmed))
64
- return true;
65
- const meaningful = trimmed
66
- .split(/\r?\n/)
67
- .map((line) => line.trim())
68
- .filter(Boolean);
69
- if (!meaningful.length)
70
- return true;
71
- const failureLines = meaningful.filter((line) => /\b(?:error|failed|failure|timeout|401|403|404|429|5\d\d)\b/i.test(line));
72
- return failureLines.length === meaningful.length;
73
- }
74
- // ─── Main stamp function ──────────────────────────────────────────────────────
75
- export function stampFreshness(result, options, adapter) {
76
- const retrieved_at = new Date().toISOString();
77
- const failedContent = looksLikeFailedAdapterContent(result.raw);
78
- const content_date = failedContent ? null : result.content_date;
79
- const freshness_confidence = failedContent ? "low" : result.freshness_confidence;
80
- const freshness_score = calculateFreshnessScore(content_date, retrieved_at, adapter);
81
- return {
82
- content: result.raw.slice(0, options.maxLength ?? 8000),
83
- source_url: options.url,
84
- content_date,
85
- retrieved_at,
86
- freshness_confidence,
87
- freshness_score,
88
- adapter,
89
- };
90
- }
91
- // ─── Structured JSON form ─────────────────────────────────────────────────────
92
- // Returns the spec-compliant JSON object defined in FRESHCONTEXT_SPEC.md.
93
- // Programmatic consumers can parse this without touching the text envelope.
94
- export function toStructuredJSON(ctx) {
95
- return {
96
- freshcontext: {
97
- source_url: ctx.source_url,
98
- content_date: ctx.content_date,
99
- retrieved_at: ctx.retrieved_at,
100
- freshness_confidence: ctx.freshness_confidence,
101
- freshness_score: ctx.freshness_score,
102
- adapter: ctx.adapter,
103
- },
104
- content: ctx.content,
105
- };
106
- }
107
- // ─── Text envelope formatter ──────────────────────────────────────────────────
108
- // Produces the [FRESHCONTEXT]...[/FRESHCONTEXT] envelope defined in the spec,
109
- // followed by a [FRESHCONTEXT_JSON]...[/FRESHCONTEXT_JSON] block so both the
110
- // human-readable envelope and the machine-parseable JSON travel together.
111
- export function formatForLLM(ctx) {
112
- const dateInfo = ctx.content_date
113
- ? `Published: ${ctx.content_date}`
114
- : "Publish date: unknown";
115
- const scoreLine = ctx.freshness_score !== null
116
- ? `Score: ${ctx.freshness_score}/100 (${scoreLabel(ctx.freshness_score)})`
117
- : `Score: unknown`;
118
- const textEnvelope = [
119
- `[FRESHCONTEXT]`,
120
- `Source: ${ctx.source_url}`,
121
- `${dateInfo}`,
122
- `Retrieved: ${ctx.retrieved_at}`,
123
- `Confidence: ${ctx.freshness_confidence}`,
124
- `${scoreLine}`,
125
- `---`,
126
- ctx.content,
127
- `[/FRESHCONTEXT]`,
128
- ].join("\n");
129
- // Append the structured JSON block so programmatic consumers
130
- // can extract metadata without parsing the text envelope.
131
- const jsonBlock = [
132
- `[FRESHCONTEXT_JSON]`,
133
- JSON.stringify(toStructuredJSON(ctx), null, 2),
134
- `[/FRESHCONTEXT_JSON]`,
135
- ].join("\n");
136
- return `${textEnvelope}\n\n${jsonBlock}`;
137
- }
1
+ export { LAMBDA, stampFreshness, toStructuredJSON, formatForLLM, } from "../core/index.js";
package/dist/types.js CHANGED
@@ -1,2 +1 @@
1
- // Core data types for freshcontext-mcp
2
1
  export {};