freshcontext-mcp 0.3.17 → 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/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/hackernews.js +16 -16
- package/dist/adapters/registry.js +232 -0
- 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/server.js +1 -1
- package/dist/tools/freshnessStamp.js +1 -137
- 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 -59
- package/server.json +27 -28
- 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
|
@@ -22,7 +22,7 @@ import { stampFreshness, formatForLLM } from "./tools/freshnessStamp.js";
|
|
|
22
22
|
import { formatSecurityError } from "./security.js";
|
|
23
23
|
const server = new McpServer({
|
|
24
24
|
name: "freshcontext-mcp",
|
|
25
|
-
version: "0.3.
|
|
25
|
+
version: "0.3.18",
|
|
26
26
|
});
|
|
27
27
|
// ─── Tool: extract_github ────────────────────────────────────────────────────
|
|
28
28
|
server.registerTool("extract_github", {
|
|
@@ -1,137 +1 @@
|
|
|
1
|
-
|
|
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