freshcontext-mcp 0.3.23 → 0.4.0
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/FRESHCONTEXT_SPEC.md +1 -1
- package/METHODOLOGY.md +455 -381
- package/README.md +1 -1
- package/dist/adapters/arxiv.js +1 -1
- package/dist/adapters/finance.js +1 -1
- package/dist/adapters/gdelt.js +1 -1
- package/dist/adapters/gebiz.js +1 -1
- package/dist/adapters/jobs.js +6 -6
- package/dist/adapters/reddit.js +1 -1
- package/dist/adapters/repoSearch.js +1 -1
- package/dist/adapters/secFilings.js +1 -1
- package/dist/core/decay.d.ts +3 -0
- package/dist/core/decay.js +40 -1
- package/dist/core/decision.d.ts +17 -1
- package/dist/core/decision.js +92 -11
- package/dist/core/edge.d.ts +3 -0
- package/dist/core/edge.js +18 -0
- package/dist/core/envelope.js +11 -1
- package/dist/core/index.d.ts +5 -4
- package/dist/core/index.js +3 -3
- package/dist/core/pipeline.js +10 -1
- package/dist/core/provenance.d.ts +3 -1
- package/dist/core/provenance.js +47 -0
- package/dist/core/types.d.ts +48 -0
- package/dist/rest/handler.d.ts +1 -1
- package/dist/rest/handler.js +53 -2
- package/dist/server.js +1 -1
- package/docs/CLIENT_SETUP.md +4 -4
- package/docs/CODEX_MCP_USAGE.md +4 -4
- package/docs/CORE_MCP_BOUNDARY.md +5 -5
- package/docs/FUTURE_LANES.md +1 -1
- package/docs/RELEASE_NOTES.md +129 -50
- package/package-script-guard.mjs +9 -0
- package/package.json +119 -114
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -170,7 +170,7 @@ FreshContext does not certify truth. It records why context was used, supported,
|
|
|
170
170
|
|
|
171
171
|
`evaluate_context` does not fetch URLs, crawl, scrape, browse, read folders, or call adapters. It only evaluates candidate context the caller provides.
|
|
172
172
|
|
|
173
|
-
Current boundary: `evaluate_context` is part of the npm/local stdio MCP server prepared for `0.
|
|
173
|
+
Current boundary: `evaluate_context` is part of the npm/local stdio MCP server prepared for `0.4.0`. The hosted Cloudflare Worker MCP endpoint was last verified separately at `0.4.0 / 22 tools`. The Worker remains a separate deployment surface, so future package interfaces should be re-verified remotely before being claimed live.
|
|
174
174
|
|
|
175
175
|
### Network Boundary
|
|
176
176
|
|
package/dist/adapters/arxiv.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { validateUrl } from "../security.js";
|
|
2
|
-
const USER_AGENT = "freshcontext-mcp/0.
|
|
2
|
+
const USER_AGENT = "freshcontext-mcp/0.4.0 (https://github.com/PrinceGabriel-lgtm/freshcontext-mcp)";
|
|
3
3
|
const DEFAULT_ARXIV_SIGNAL_SCORE = 0.8;
|
|
4
4
|
function buildArxivApiUrl(input, maxResults = 10) {
|
|
5
5
|
const trimmed = input.trim();
|
package/dist/adapters/finance.js
CHANGED
|
@@ -30,7 +30,7 @@ async function fetchStooqQuote(ticker) {
|
|
|
30
30
|
const url = `https://stooq.com/q/l/?s=${encodeURIComponent(stooqSymbol.toLowerCase())}&f=sd2t2ohlcv&h&e=json`;
|
|
31
31
|
const res = await fetch(url, {
|
|
32
32
|
headers: {
|
|
33
|
-
"User-Agent": "freshcontext-mcp/0.
|
|
33
|
+
"User-Agent": "freshcontext-mcp/0.4.0",
|
|
34
34
|
"Accept": "application/json",
|
|
35
35
|
},
|
|
36
36
|
});
|
package/dist/adapters/gdelt.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
const HEADERS = {
|
|
13
13
|
"Accept": "application/json",
|
|
14
|
-
"User-Agent": "freshcontext-mcp/0.
|
|
14
|
+
"User-Agent": "freshcontext-mcp/0.4.0 (https://github.com/PrinceGabriel-lgtm/freshcontext-mcp)",
|
|
15
15
|
};
|
|
16
16
|
function parseGdeltDate(raw) {
|
|
17
17
|
if (!raw)
|
package/dist/adapters/gebiz.js
CHANGED
|
@@ -20,7 +20,7 @@ const DATASET_ID = "d_acde1106003906a75c3fa052592f2fcb";
|
|
|
20
20
|
const BASE_URL = "https://data.gov.sg/api/action/datastore_search";
|
|
21
21
|
const HEADERS = {
|
|
22
22
|
"Accept": "application/json",
|
|
23
|
-
"User-Agent": "freshcontext-mcp/0.
|
|
23
|
+
"User-Agent": "freshcontext-mcp/0.4.0 (https://github.com/PrinceGabriel-lgtm/freshcontext-mcp)",
|
|
24
24
|
};
|
|
25
25
|
function formatDate(raw) {
|
|
26
26
|
if (!raw)
|
package/dist/adapters/jobs.js
CHANGED
|
@@ -131,7 +131,7 @@ export async function jobsAdapter(options) {
|
|
|
131
131
|
async function fetchRemotive(query, location, maxAgeDays, keywords) {
|
|
132
132
|
const url = `https://remotive.com/api/remote-jobs?search=${encodeURIComponent(query)}&limit=15`;
|
|
133
133
|
const res = await fetch(url, {
|
|
134
|
-
headers: { "User-Agent": "freshcontext-mcp/0.
|
|
134
|
+
headers: { "User-Agent": "freshcontext-mcp/0.4.0", "Accept": "application/json" },
|
|
135
135
|
});
|
|
136
136
|
if (!res.ok)
|
|
137
137
|
throw new Error(`Remotive ${res.status}`);
|
|
@@ -161,7 +161,7 @@ async function fetchRemoteOK(query, location, maxAgeDays, keywords) {
|
|
|
161
161
|
const tag = query.toLowerCase().replace(/\s+/g, "-");
|
|
162
162
|
const url = `https://remoteok.com/api?tag=${encodeURIComponent(tag)}`;
|
|
163
163
|
const res = await fetch(url, {
|
|
164
|
-
headers: { "User-Agent": "freshcontext-mcp/0.
|
|
164
|
+
headers: { "User-Agent": "freshcontext-mcp/0.4.0", "Accept": "application/json" },
|
|
165
165
|
});
|
|
166
166
|
if (!res.ok)
|
|
167
167
|
throw new Error(`RemoteOK ${res.status}`);
|
|
@@ -199,7 +199,7 @@ async function fetchArbeitnow(query, location, maxAgeDays, keywords, remoteOnly)
|
|
|
199
199
|
params.set("remote", "true");
|
|
200
200
|
const url = `https://arbeitnow.com/api/job-board-api?${params.toString()}`;
|
|
201
201
|
const res = await fetch(url, {
|
|
202
|
-
headers: { "User-Agent": "freshcontext-mcp/0.
|
|
202
|
+
headers: { "User-Agent": "freshcontext-mcp/0.4.0", "Accept": "application/json" },
|
|
203
203
|
});
|
|
204
204
|
if (!res.ok)
|
|
205
205
|
throw new Error(`Arbeitnow ${res.status}`);
|
|
@@ -233,7 +233,7 @@ async function fetchMuse(query, location, maxAgeDays, keywords) {
|
|
|
233
233
|
: "&location=Flexible%20%2F%20Remote";
|
|
234
234
|
const url = `https://www.themuse.com/api/public/jobs?name=${encodeURIComponent(query)}${locParam}&page=0&descending=true`;
|
|
235
235
|
const res = await fetch(url, {
|
|
236
|
-
headers: { "User-Agent": "freshcontext-mcp/0.
|
|
236
|
+
headers: { "User-Agent": "freshcontext-mcp/0.4.0", "Accept": "application/json" },
|
|
237
237
|
});
|
|
238
238
|
if (!res.ok)
|
|
239
239
|
throw new Error(`The Muse ${res.status}`);
|
|
@@ -263,7 +263,7 @@ async function fetchMuse(query, location, maxAgeDays, keywords) {
|
|
|
263
263
|
// instead of all HN comments. Uses the parent_id filter to target the thread.
|
|
264
264
|
async function fetchHNHiring(query, location, maxAgeDays, keywords) {
|
|
265
265
|
// Step 1: Find the most recent "Ask HN: Who is hiring?" thread
|
|
266
|
-
const threadRes = await fetch(`https://hn.algolia.com/api/v1/search?query=Ask+HN+Who+is+hiring&tags=story&hitsPerPage=5`, { headers: { "User-Agent": "freshcontext-mcp/0.
|
|
266
|
+
const threadRes = await fetch(`https://hn.algolia.com/api/v1/search?query=Ask+HN+Who+is+hiring&tags=story&hitsPerPage=5`, { headers: { "User-Agent": "freshcontext-mcp/0.4.0" } });
|
|
267
267
|
if (!threadRes.ok)
|
|
268
268
|
throw new Error(`HN thread search ${threadRes.status}`);
|
|
269
269
|
const threadData = await threadRes.json();
|
|
@@ -273,7 +273,7 @@ async function fetchHNHiring(query, location, maxAgeDays, keywords) {
|
|
|
273
273
|
throw new Error("HN hiring thread not found");
|
|
274
274
|
// Step 2: Search comments within that thread for the query
|
|
275
275
|
const searchTerms = [query, location].filter(Boolean).join(" ");
|
|
276
|
-
const commentsRes = await fetch(`https://hn.algolia.com/api/v1/search?query=${encodeURIComponent(searchTerms)}&tags=comment,story_${hiringThread.objectID}&hitsPerPage=10`, { headers: { "User-Agent": "freshcontext-mcp/0.
|
|
276
|
+
const commentsRes = await fetch(`https://hn.algolia.com/api/v1/search?query=${encodeURIComponent(searchTerms)}&tags=comment,story_${hiringThread.objectID}&hitsPerPage=10`, { headers: { "User-Agent": "freshcontext-mcp/0.4.0" } });
|
|
277
277
|
if (!commentsRes.ok)
|
|
278
278
|
throw new Error(`HN comments ${commentsRes.status}`);
|
|
279
279
|
const commentsData = await commentsRes.json();
|
package/dist/adapters/reddit.js
CHANGED
|
@@ -28,7 +28,7 @@ export async function redditAdapter(options) {
|
|
|
28
28
|
const safeUrl = validateUrl(apiUrl, "reddit");
|
|
29
29
|
const res = await fetch(safeUrl, {
|
|
30
30
|
headers: {
|
|
31
|
-
"User-Agent": "freshcontext-mcp/0.
|
|
31
|
+
"User-Agent": "freshcontext-mcp/0.4.0 (https://github.com/PrinceGabriel-lgtm/freshcontext-mcp)",
|
|
32
32
|
"Accept": "application/json",
|
|
33
33
|
},
|
|
34
34
|
});
|
|
@@ -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.4.0 (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/0.
|
|
15
|
+
"User-Agent": "freshcontext-mcp/0.4.0 (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);
|
package/dist/core/decay.d.ts
CHANGED
|
@@ -3,3 +3,6 @@ export declare const FUTURE_CLOCK_SKEW_TOLERANCE_MS: number;
|
|
|
3
3
|
export declare function isMeaningfullyFutureDate(content_date: string | null, retrieved_at: string): boolean;
|
|
4
4
|
export declare function calculateFreshnessScore(content_date: string | null, retrieved_at: string, adapter: string): number | null;
|
|
5
5
|
export declare function scoreLabel(score: number | null): string;
|
|
6
|
+
export type StalenessVerdict = "fresh" | "aging" | "stale" | "unknown";
|
|
7
|
+
export declare function stalenessVerdict(score: number | null): StalenessVerdict;
|
|
8
|
+
export declare function computeRevalidateAfter(content_date: string | null, retrieved_at: string, adapter: string): string | null;
|
package/dist/core/decay.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
// Spec-compliant exponential DAR model.
|
|
2
2
|
// Higher lambda = data goes stale faster. Half-life formula: t1/2 = ln(2) / lambda.
|
|
3
|
-
// Lambda is measured per hour
|
|
3
|
+
// Lambda is measured per hour.
|
|
4
|
+
//
|
|
5
|
+
// CANONICAL SOURCE OF DECAY POLICY. This is the single definition of LAMBDA in the
|
|
6
|
+
// codebase. Core (pipeline, sourceProfiles, freshness scoring) and the Worker
|
|
7
|
+
// intelligence module (via the core/edge boundary) both derive from it — neither
|
|
8
|
+
// keeps its own copy. To tune a source's decay, change it here and only here. The
|
|
9
|
+
// single-source guard (tests/lambdaSingleSource.test.ts) goes red if a second
|
|
10
|
+
// `const LAMBDA` table is ever reintroduced anywhere in runtime source.
|
|
4
11
|
export const LAMBDA = {
|
|
5
12
|
hackernews: 0.050,
|
|
6
13
|
reddit: 0.010,
|
|
@@ -59,3 +66,35 @@ export function scoreLabel(score) {
|
|
|
59
66
|
return "verify before acting";
|
|
60
67
|
return "use with caution";
|
|
61
68
|
}
|
|
69
|
+
// Derived from the SAME score buckets as scoreLabel, not a second decay system.
|
|
70
|
+
// "fresh" merges scoreLabel's "current"/"reliable" (score >= 70); "aging" matches
|
|
71
|
+
// "verify before acting" (>= 50); "stale" matches "use with caution" (< 50). The
|
|
72
|
+
// 50-point line is also where computeRevalidateAfter is anchored below — crossing
|
|
73
|
+
// 50 is the one staleness boundary, expressed two ways (a verdict and a timestamp).
|
|
74
|
+
export function stalenessVerdict(score) {
|
|
75
|
+
if (score === null)
|
|
76
|
+
return "unknown";
|
|
77
|
+
if (score >= 70)
|
|
78
|
+
return "fresh";
|
|
79
|
+
if (score >= 50)
|
|
80
|
+
return "aging";
|
|
81
|
+
return "stale";
|
|
82
|
+
}
|
|
83
|
+
// The timestamp at which this content's freshness_score would cross the staleness
|
|
84
|
+
// boundary (score = 50). Since 100 * e^(-lambda*t) = 50 solves to t = ln(2)/lambda
|
|
85
|
+
// — exactly one half-life — revalidate_after is always content_date + half-life.
|
|
86
|
+
// Uses the same null/future-date guards as calculateFreshnessScore so the two
|
|
87
|
+
// stay in lockstep: staleness === "unknown" if and only if revalidate_after === null.
|
|
88
|
+
export function computeRevalidateAfter(content_date, retrieved_at, adapter) {
|
|
89
|
+
if (!content_date)
|
|
90
|
+
return null;
|
|
91
|
+
const published = new Date(content_date).getTime();
|
|
92
|
+
const retrieved = new Date(retrieved_at).getTime();
|
|
93
|
+
if (isNaN(published) || isNaN(retrieved))
|
|
94
|
+
return null;
|
|
95
|
+
if (published - retrieved > FUTURE_CLOCK_SKEW_TOLERANCE_MS)
|
|
96
|
+
return null;
|
|
97
|
+
const lambda = LAMBDA[adapter] ?? LAMBDA.default;
|
|
98
|
+
const halfLifeHours = Math.log(2) / lambda;
|
|
99
|
+
return new Date(published + halfLifeHours * 60 * 60 * 1000).toISOString();
|
|
100
|
+
}
|
package/dist/core/decision.d.ts
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
-
import type { ContextDecisionOptions, ContextDecisionResult, CoreSignalEvaluationResult } from "./types.js";
|
|
1
|
+
import type { ContextDecision, ContextDecisionOptions, ContextDecisionResult, CoreSignalEvaluationResult, IntentProfileId, SourceProfileId } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Deterministic identity for a single decision (a "verdict").
|
|
4
|
+
*
|
|
5
|
+
* Derived only from the inputs that determine the decision itself: signal
|
|
6
|
+
* identity, source, the decision label, and profile selection. Deliberately
|
|
7
|
+
* EXCLUDES utility and provenance_readiness, which must stay free to vary
|
|
8
|
+
* without changing what verdict this is — mirrors the existing
|
|
9
|
+
* "utility/provenance_readiness does not control decision labels" rule.
|
|
10
|
+
*
|
|
11
|
+
* Re-evaluating identical inputs always yields the same verdict_id. It is a
|
|
12
|
+
* fingerprint, not a random id, so any caller can recompute and recognize it
|
|
13
|
+
* later without FreshContext having to store an id-issuing table. This is
|
|
14
|
+
* the id a future verification event would reference when resolving a
|
|
15
|
+
* needs_verification (or similar) decision into a cleared state.
|
|
16
|
+
*/
|
|
17
|
+
export declare function computeVerdictId(evaluation: CoreSignalEvaluationResult, decision: ContextDecision, sourceProfileId: SourceProfileId | undefined, intentProfile: IntentProfileId | undefined): string;
|
|
2
18
|
export declare function interpretEvaluation(evaluation: CoreSignalEvaluationResult, options?: ContextDecisionOptions): ContextDecisionResult;
|
|
3
19
|
export declare function interpretEvaluations(evaluations: CoreSignalEvaluationResult[], options?: ContextDecisionOptions): ContextDecisionResult[];
|
package/dist/core/decision.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { sha256Hex } from "./provenance.js";
|
|
1
2
|
import { getSourceProfile } from "./sourceProfiles.js";
|
|
2
3
|
const CITATION_INTENTS = new Set(["citation_check", "student_research"]);
|
|
3
4
|
const STRICT_REFRESH_PROFILES = new Set(["market_finance", "jobs_opportunities"]);
|
|
5
|
+
const VERDICT_ID_VERSION = "FRESHCONTEXT_VERDICT_V1";
|
|
4
6
|
function resolveSourceProfile(profile) {
|
|
5
7
|
if (!profile)
|
|
6
8
|
return undefined;
|
|
@@ -18,6 +20,26 @@ function hasFailureReason(evaluation) {
|
|
|
18
20
|
function isCitationIntent(intent) {
|
|
19
21
|
return intent !== undefined && CITATION_INTENTS.has(intent);
|
|
20
22
|
}
|
|
23
|
+
// Pass 21: decision-time clock. Honors options.now for deterministic verdicts,
|
|
24
|
+
// falls back to wall time. Never recomputed at read.
|
|
25
|
+
function computeEvaluatedAt(now) {
|
|
26
|
+
if (now === undefined)
|
|
27
|
+
return new Date().toISOString();
|
|
28
|
+
const date = typeof now === "string" ? new Date(now) : now;
|
|
29
|
+
return date.toISOString();
|
|
30
|
+
}
|
|
31
|
+
// Pass 21: revalidate_after = evaluated_at + 1.0 × source profile half-life.
|
|
32
|
+
// The half-life is the literal inflection where the source has lost half its
|
|
33
|
+
// freshness signal value, the one number on the decay curve the source profile
|
|
34
|
+
// is calibrated to express. Explicit null when no source profile basis exists
|
|
35
|
+
// — never a fabricated timestamp.
|
|
36
|
+
function computeRevalidateAfter(evaluatedAt, sourceProfile) {
|
|
37
|
+
if (!sourceProfile)
|
|
38
|
+
return null;
|
|
39
|
+
const baseMs = new Date(evaluatedAt).getTime();
|
|
40
|
+
const halfLifeMs = sourceProfile.half_life_hours * 60 * 60 * 1000;
|
|
41
|
+
return new Date(baseMs + halfLifeMs).toISOString();
|
|
42
|
+
}
|
|
21
43
|
function nonAdviceWarnings(intent) {
|
|
22
44
|
switch (intent) {
|
|
23
45
|
case "citation_check":
|
|
@@ -35,13 +57,46 @@ function nonAdviceWarnings(intent) {
|
|
|
35
57
|
return [];
|
|
36
58
|
}
|
|
37
59
|
}
|
|
38
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Deterministic identity for a single decision (a "verdict").
|
|
62
|
+
*
|
|
63
|
+
* Derived only from the inputs that determine the decision itself: signal
|
|
64
|
+
* identity, source, the decision label, and profile selection. Deliberately
|
|
65
|
+
* EXCLUDES utility and provenance_readiness, which must stay free to vary
|
|
66
|
+
* without changing what verdict this is — mirrors the existing
|
|
67
|
+
* "utility/provenance_readiness does not control decision labels" rule.
|
|
68
|
+
*
|
|
69
|
+
* Re-evaluating identical inputs always yields the same verdict_id. It is a
|
|
70
|
+
* fingerprint, not a random id, so any caller can recompute and recognize it
|
|
71
|
+
* later without FreshContext having to store an id-issuing table. This is
|
|
72
|
+
* the id a future verification event would reference when resolving a
|
|
73
|
+
* needs_verification (or similar) decision into a cleared state.
|
|
74
|
+
*/
|
|
75
|
+
export function computeVerdictId(evaluation, decision, sourceProfileId, intentProfile) {
|
|
76
|
+
const signal = evaluation.signal;
|
|
77
|
+
const basis = [
|
|
78
|
+
VERDICT_ID_VERSION,
|
|
79
|
+
`signal_id=${signal.id ?? "null"}`,
|
|
80
|
+
`source=${signal.source}`,
|
|
81
|
+
`source_type=${signal.source_type}`,
|
|
82
|
+
`published_at=${signal.published_at ?? "null"}`,
|
|
83
|
+
`decision=${decision}`,
|
|
84
|
+
`source_profile=${sourceProfileId ?? "null"}`,
|
|
85
|
+
`intent_profile=${intentProfile ?? "null"}`,
|
|
86
|
+
].join("\n");
|
|
87
|
+
return sha256Hex(basis);
|
|
88
|
+
}
|
|
89
|
+
function decisionResult(decision, reasons, warnings, evaluation, sourceProfileId, intentProfile, evaluatedAt, revalidateAfter) {
|
|
39
90
|
const copyReasons = unique(reasons);
|
|
40
91
|
const copyWarnings = unique(warnings);
|
|
92
|
+
const verdict_id = computeVerdictId(evaluation, decision, sourceProfileId, intentProfile);
|
|
41
93
|
switch (decision) {
|
|
42
94
|
case "use_first":
|
|
43
95
|
return {
|
|
44
96
|
decision,
|
|
97
|
+
verdict_id,
|
|
98
|
+
evaluated_at: evaluatedAt,
|
|
99
|
+
revalidate_after: revalidateAfter,
|
|
45
100
|
label: "Use first",
|
|
46
101
|
meaning: "This is strong, current context for the task.",
|
|
47
102
|
action: "Use this near the top of the context bundle.",
|
|
@@ -51,6 +106,9 @@ function decisionResult(decision, reasons, warnings) {
|
|
|
51
106
|
case "cite_as_primary":
|
|
52
107
|
return {
|
|
53
108
|
decision,
|
|
109
|
+
verdict_id,
|
|
110
|
+
evaluated_at: evaluatedAt,
|
|
111
|
+
revalidate_after: revalidateAfter,
|
|
54
112
|
label: "Cite as primary",
|
|
55
113
|
meaning: "This source is relevant, current, and traceable enough to use as main evidence.",
|
|
56
114
|
action: "Use it as primary citation evidence, while keeping normal source-review standards.",
|
|
@@ -60,6 +118,9 @@ function decisionResult(decision, reasons, warnings) {
|
|
|
60
118
|
case "cite_as_supporting":
|
|
61
119
|
return {
|
|
62
120
|
decision,
|
|
121
|
+
verdict_id,
|
|
122
|
+
evaluated_at: evaluatedAt,
|
|
123
|
+
revalidate_after: revalidateAfter,
|
|
63
124
|
label: "Cite as supporting",
|
|
64
125
|
meaning: "This source is useful evidence, but should not be the only or latest support.",
|
|
65
126
|
action: "Use it as supporting evidence and pair it with stronger or newer sources.",
|
|
@@ -69,6 +130,9 @@ function decisionResult(decision, reasons, warnings) {
|
|
|
69
130
|
case "use_as_background":
|
|
70
131
|
return {
|
|
71
132
|
decision,
|
|
133
|
+
verdict_id,
|
|
134
|
+
evaluated_at: evaluatedAt,
|
|
135
|
+
revalidate_after: revalidateAfter,
|
|
72
136
|
label: "Use as background",
|
|
73
137
|
meaning: "This source is relevant context, but not strong enough for latest-evidence claims.",
|
|
74
138
|
action: "Use it for framing, history, or background rather than as the main current source.",
|
|
@@ -78,6 +142,9 @@ function decisionResult(decision, reasons, warnings) {
|
|
|
78
142
|
case "needs_verification":
|
|
79
143
|
return {
|
|
80
144
|
decision,
|
|
145
|
+
verdict_id,
|
|
146
|
+
evaluated_at: evaluatedAt,
|
|
147
|
+
revalidate_after: revalidateAfter,
|
|
81
148
|
label: "Needs verification",
|
|
82
149
|
meaning: "This source may be useful, but its date, confidence, or traceability is uncertain.",
|
|
83
150
|
action: "Verify the source details before citing it, acting on it, or sending it to a model as trusted context.",
|
|
@@ -87,6 +154,9 @@ function decisionResult(decision, reasons, warnings) {
|
|
|
87
154
|
case "needs_refresh":
|
|
88
155
|
return {
|
|
89
156
|
decision,
|
|
157
|
+
verdict_id,
|
|
158
|
+
evaluated_at: evaluatedAt,
|
|
159
|
+
revalidate_after: revalidateAfter,
|
|
90
160
|
label: "Needs refresh",
|
|
91
161
|
meaning: "This source may be useful, but it is too stale or date-uncertain for this source type.",
|
|
92
162
|
action: "Refresh or re-query this source before relying on it as current context.",
|
|
@@ -96,6 +166,9 @@ function decisionResult(decision, reasons, warnings) {
|
|
|
96
166
|
case "watch_only":
|
|
97
167
|
return {
|
|
98
168
|
decision,
|
|
169
|
+
verdict_id,
|
|
170
|
+
evaluated_at: evaluatedAt,
|
|
171
|
+
revalidate_after: revalidateAfter,
|
|
99
172
|
label: "Watch only",
|
|
100
173
|
meaning: "This is an interesting signal, but not strong enough to prioritize.",
|
|
101
174
|
action: "Monitor it or keep it as a weak signal; do not use it as main evidence.",
|
|
@@ -105,6 +178,9 @@ function decisionResult(decision, reasons, warnings) {
|
|
|
105
178
|
case "exclude":
|
|
106
179
|
return {
|
|
107
180
|
decision,
|
|
181
|
+
verdict_id,
|
|
182
|
+
evaluated_at: evaluatedAt,
|
|
183
|
+
revalidate_after: revalidateAfter,
|
|
108
184
|
label: "Exclude",
|
|
109
185
|
meaning: "This source is failed, too weak, or unsafe to include as useful context.",
|
|
110
186
|
action: "Keep it out of the final context bundle unless a human explicitly reviews it.",
|
|
@@ -117,6 +193,11 @@ export function interpretEvaluation(evaluation, options = {}) {
|
|
|
117
193
|
const sourceProfile = resolveSourceProfile(options.sourceProfile);
|
|
118
194
|
const sourceProfileId = profileId(sourceProfile);
|
|
119
195
|
const intentProfile = options.intentProfile;
|
|
196
|
+
// Pass 21: decision-time clock. Computed once here and passed through to the
|
|
197
|
+
// factory — never recomputed at read time. revalidate_after is explicit null
|
|
198
|
+
// when no source profile is available (no decay basis = no honest hint).
|
|
199
|
+
const evaluatedAt = computeEvaluatedAt(options.now);
|
|
200
|
+
const revalidateAfter = computeRevalidateAfter(evaluatedAt, sourceProfile);
|
|
120
201
|
const reasons = unique([
|
|
121
202
|
evaluation.explanation,
|
|
122
203
|
...evaluation.signal.reasons,
|
|
@@ -136,38 +217,38 @@ export function interpretEvaluation(evaluation, options = {}) {
|
|
|
136
217
|
reasons.push(`intent profile ${intentProfile} selected`);
|
|
137
218
|
}
|
|
138
219
|
if (isFailed) {
|
|
139
|
-
return decisionResult("exclude", reasons, warnings);
|
|
220
|
+
return decisionResult("exclude", reasons, warnings, evaluation, sourceProfileId, intentProfile, evaluatedAt, revalidateAfter);
|
|
140
221
|
}
|
|
141
222
|
if (sourceProfileId
|
|
142
223
|
&& STRICT_REFRESH_PROFILES.has(sourceProfileId)
|
|
143
224
|
&& (freshnessScore === null || freshnessScore < 50)) {
|
|
144
|
-
return decisionResult("needs_refresh", reasons, warnings);
|
|
225
|
+
return decisionResult("needs_refresh", reasons, warnings, evaluation, sourceProfileId, intentProfile, evaluatedAt, revalidateAfter);
|
|
145
226
|
}
|
|
146
227
|
if (evaluation.signal.date_confidence === "unknown") {
|
|
147
228
|
if (sourceProfileId === "academic_research" && finalScore >= 0.75) {
|
|
148
|
-
return decisionResult(isCitationIntent(intentProfile) ? "cite_as_supporting" : "use_as_background", reasons, warnings);
|
|
229
|
+
return decisionResult(isCitationIntent(intentProfile) ? "cite_as_supporting" : "use_as_background", reasons, warnings, evaluation, sourceProfileId, intentProfile, evaluatedAt, revalidateAfter);
|
|
149
230
|
}
|
|
150
|
-
return decisionResult("needs_verification", reasons, warnings);
|
|
231
|
+
return decisionResult("needs_verification", reasons, warnings, evaluation, sourceProfileId, intentProfile, evaluatedAt, revalidateAfter);
|
|
151
232
|
}
|
|
152
233
|
if (finalScore >= 0.85
|
|
153
234
|
&& freshnessScore !== null
|
|
154
235
|
&& freshnessScore >= 70
|
|
155
236
|
&& confidence === "high") {
|
|
156
237
|
if (sourceProfile?.authority_hint === "high" && isCitationIntent(intentProfile)) {
|
|
157
|
-
return decisionResult("cite_as_primary", reasons, warnings);
|
|
238
|
+
return decisionResult("cite_as_primary", reasons, warnings, evaluation, sourceProfileId, intentProfile, evaluatedAt, revalidateAfter);
|
|
158
239
|
}
|
|
159
|
-
return decisionResult("use_first", reasons, warnings);
|
|
240
|
+
return decisionResult("use_first", reasons, warnings, evaluation, sourceProfileId, intentProfile, evaluatedAt, revalidateAfter);
|
|
160
241
|
}
|
|
161
242
|
if (finalScore >= 0.55 && freshnessScore !== null && freshnessScore < 50) {
|
|
162
|
-
return decisionResult(isCitationIntent(intentProfile) ? "cite_as_supporting" : "use_as_background", reasons, warnings);
|
|
243
|
+
return decisionResult(isCitationIntent(intentProfile) ? "cite_as_supporting" : "use_as_background", reasons, warnings, evaluation, sourceProfileId, intentProfile, evaluatedAt, revalidateAfter);
|
|
163
244
|
}
|
|
164
245
|
if (finalScore < 0.35) {
|
|
165
|
-
return decisionResult(confidence === "low" ? "exclude" : "watch_only", reasons, warnings);
|
|
246
|
+
return decisionResult(confidence === "low" ? "exclude" : "watch_only", reasons, warnings, evaluation, sourceProfileId, intentProfile, evaluatedAt, revalidateAfter);
|
|
166
247
|
}
|
|
167
248
|
if (finalScore >= 0.55) {
|
|
168
|
-
return decisionResult("use_as_background", reasons, warnings);
|
|
249
|
+
return decisionResult("use_as_background", reasons, warnings, evaluation, sourceProfileId, intentProfile, evaluatedAt, revalidateAfter);
|
|
169
250
|
}
|
|
170
|
-
return decisionResult("watch_only", reasons, warnings);
|
|
251
|
+
return decisionResult("watch_only", reasons, warnings, evaluation, sourceProfileId, intentProfile, evaluatedAt, revalidateAfter);
|
|
171
252
|
}
|
|
172
253
|
export function interpretEvaluations(evaluations, options = {}) {
|
|
173
254
|
return evaluations.map((evaluation) => interpretEvaluation(evaluation, options));
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// edge.ts — Edge-safe Core math boundary (Pass 20-A).
|
|
2
|
+
//
|
|
3
|
+
// Re-exports ONLY Core modules whose entire transitive import graph is free
|
|
4
|
+
// of node:crypto, so the Worker — or any runtime without nodejs_compat — can
|
|
5
|
+
// pull the math without dragging in Node-only APIs.
|
|
6
|
+
//
|
|
7
|
+
// Guard: tests/coreEdgeBoundary.test.ts walks this barrel's transitive value-
|
|
8
|
+
// import graph and fails if anything reachable from here imports node:crypto.
|
|
9
|
+
// Adding a new export here MUST keep that test green.
|
|
10
|
+
//
|
|
11
|
+
// Cleared-but-deliberately-deferred (verified crypto-free in Phase 0, kept
|
|
12
|
+
// out for now because no consumer needs them at the edge yet):
|
|
13
|
+
// guards.ts, rank.ts, utility.ts, signal.ts
|
|
14
|
+
// Widening the boundary before there is a real consumer just creates more
|
|
15
|
+
// contract to maintain. Add here only when a Worker or external consumer
|
|
16
|
+
// actually needs them at the edge — and re-run the guard test at that moment.
|
|
17
|
+
export { LAMBDA, calculateFreshnessScore, scoreLabel } from "./decay.js";
|
|
18
|
+
export { BUILT_IN_SOURCE_PROFILES, getSourceProfile, listSourceProfiles, } from "./sourceProfiles.js";
|
package/dist/core/envelope.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { calculateFreshnessScore, isMeaningfullyFutureDate, scoreLabel } from "./decay.js";
|
|
1
|
+
import { calculateFreshnessScore, computeRevalidateAfter, isMeaningfullyFutureDate, scoreLabel, stalenessVerdict } from "./decay.js";
|
|
2
2
|
import { looksLikeFailedAdapterContent } from "./guards.js";
|
|
3
3
|
export const MAX_ENVELOPE_CONTENT_LENGTH = 20000;
|
|
4
4
|
function clampEnvelopeMaxLength(maxLength) {
|
|
@@ -15,6 +15,8 @@ export function stampFreshness(result, options, adapter) {
|
|
|
15
15
|
const futureDated = !failedContent && isMeaningfullyFutureDate(content_date, retrieved_at);
|
|
16
16
|
const freshness_confidence = failedContent || futureDated ? "low" : result.freshness_confidence;
|
|
17
17
|
const freshness_score = calculateFreshnessScore(content_date, retrieved_at, adapter);
|
|
18
|
+
const staleness = stalenessVerdict(freshness_score);
|
|
19
|
+
const revalidate_after = computeRevalidateAfter(content_date, retrieved_at, adapter);
|
|
18
20
|
return {
|
|
19
21
|
content: result.raw.slice(0, clampEnvelopeMaxLength(options.maxLength)),
|
|
20
22
|
source_url: options.url,
|
|
@@ -23,6 +25,8 @@ export function stampFreshness(result, options, adapter) {
|
|
|
23
25
|
freshness_confidence,
|
|
24
26
|
freshness_score,
|
|
25
27
|
adapter,
|
|
28
|
+
staleness,
|
|
29
|
+
revalidate_after,
|
|
26
30
|
};
|
|
27
31
|
}
|
|
28
32
|
export function toStructuredJSON(ctx) {
|
|
@@ -34,6 +38,8 @@ export function toStructuredJSON(ctx) {
|
|
|
34
38
|
freshness_confidence: ctx.freshness_confidence,
|
|
35
39
|
freshness_score: ctx.freshness_score,
|
|
36
40
|
adapter: ctx.adapter,
|
|
41
|
+
staleness: ctx.staleness,
|
|
42
|
+
revalidate_after: ctx.revalidate_after,
|
|
37
43
|
},
|
|
38
44
|
content: ctx.content,
|
|
39
45
|
};
|
|
@@ -47,6 +53,9 @@ export function formatForLLM(ctx, options = {}) {
|
|
|
47
53
|
const scoreLine = ctx.freshness_score !== null
|
|
48
54
|
? `Score: ${ctx.freshness_score}/100 (${scoreLabel(ctx.freshness_score)})`
|
|
49
55
|
: `Score: unknown`;
|
|
56
|
+
const stalenessLine = ctx.revalidate_after !== null
|
|
57
|
+
? `Staleness: ${ctx.staleness} (revalidate by ${ctx.revalidate_after})`
|
|
58
|
+
: `Staleness: ${ctx.staleness}`;
|
|
50
59
|
const textEnvelope = [
|
|
51
60
|
`[FRESHCONTEXT]`,
|
|
52
61
|
`Source: ${ctx.source_url}`,
|
|
@@ -54,6 +63,7 @@ export function formatForLLM(ctx, options = {}) {
|
|
|
54
63
|
`Retrieved: ${ctx.retrieved_at}`,
|
|
55
64
|
`Confidence: ${ctx.freshness_confidence}`,
|
|
56
65
|
`${scoreLine}`,
|
|
66
|
+
`${stalenessLine}`,
|
|
57
67
|
`---`,
|
|
58
68
|
ctx.content,
|
|
59
69
|
`[/FRESHCONTEXT]`,
|
package/dist/core/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export { LAMBDA, calculateFreshnessScore, scoreLabel } from "./decay.js";
|
|
1
|
+
export { LAMBDA, calculateFreshnessScore, scoreLabel, stalenessVerdict, computeRevalidateAfter } from "./decay.js";
|
|
2
|
+
export type { StalenessVerdict } from "./decay.js";
|
|
2
3
|
export { looksLikeFailedAdapterContent } from "./guards.js";
|
|
3
4
|
export { stampFreshness, toStructuredJSON, formatForLLM } from "./envelope.js";
|
|
4
5
|
export { explainSignal } from "./explain.js";
|
|
@@ -6,9 +7,9 @@ export { rankSignals, rankSignal, clampScore } from "./rank.js";
|
|
|
6
7
|
export { calculateContextUtility } from "./utility.js";
|
|
7
8
|
export { SIGNAL_CONTRACT_VERSION, normalizeSignal } from "./signal.js";
|
|
8
9
|
export { evaluateSignal, evaluateSignals } from "./pipeline.js";
|
|
9
|
-
export { interpretEvaluation, interpretEvaluations } from "./decision.js";
|
|
10
|
+
export { interpretEvaluation, interpretEvaluations, computeVerdictId } from "./decision.js";
|
|
10
11
|
export { toReadableContextResult } from "./readable.js";
|
|
11
12
|
export { prepareProvenanceReadiness } from "./provenanceReadiness.js";
|
|
12
13
|
export { BUILT_IN_SOURCE_PROFILES, getSourceProfile, listSourceProfiles } from "./sourceProfiles.js";
|
|
13
|
-
export { canonicalizeHaPriContent, sha256Hex, calculateHaPriV2, verifyHaPriV2, } from "./provenance.js";
|
|
14
|
-
export type { FreshContext, ExtractOptions, AdapterResult, EnvelopeFormatOptions, SignalConfidence, SignalDateConfidence, SignalContractVersion, SourceAuthorityHint, SourceDatePolicy, SourceFailurePolicy, SourceProfile, SourceProfileId, SourceSurface, ContextDecision, IntentProfileId, ContextDecisionOptions, ContextDecisionResult, HumanReadableHandoffResult, HumanReadableContextResult, SignalNormalizeOptions, FreshContextSignalInput, FreshContextSignal, FreshSignal, RankedSignal, RankOptions, ContextUtilityStatus, ContextUtilityInput, ContextUtilityResult, HaPriV2Input, HaPriV2Material, HaPriV2Result, HaPriVerificationStatus, HaPriV2VerificationResult, ProvenanceReadinessState, ProvenanceSourceIdentityCompleteness, ProvenanceTimingCompleteness, ProvenanceReadinessInput, ProvenanceReadinessOptions, ProvenanceSourceIdentityResult, ProvenanceReadinessResult, CoreSignalProvenanceOptions, CoreSignalEnvelopeResult, CoreSignalEvaluationOptions, CoreSignalEvaluationResult, } from "./types.js";
|
|
14
|
+
export { canonicalizeHaPriContent, sha256Hex, calculateHaPriV2, buildHaPriPayload, buildHaPriPayloadV3, verifyHaPriV2, } from "./provenance.js";
|
|
15
|
+
export type { FreshContext, ExtractOptions, AdapterResult, EnvelopeFormatOptions, SignalConfidence, SignalDateConfidence, SignalContractVersion, SourceAuthorityHint, SourceDatePolicy, SourceFailurePolicy, SourceProfile, SourceProfileId, SourceSurface, ContextDecision, IntentProfileId, ContextDecisionOptions, ContextDecisionResult, HumanReadableHandoffResult, HumanReadableContextResult, SignalNormalizeOptions, FreshContextSignalInput, FreshContextSignal, FreshSignal, RankedSignal, RankOptions, ContextUtilityStatus, ContextUtilityInput, ContextUtilityResult, HaPriV2Input, HaPriV3Input, HaPriV2Material, HaPriV2Result, HaPriVerificationStatus, HaPriV2VerificationResult, ProvenanceReadinessState, ProvenanceSourceIdentityCompleteness, ProvenanceTimingCompleteness, ProvenanceReadinessInput, ProvenanceReadinessOptions, ProvenanceSourceIdentityResult, ProvenanceReadinessResult, CoreSignalProvenanceOptions, CoreSignalEnvelopeResult, CoreSignalEvaluationOptions, CoreSignalEvaluationResult, } from "./types.js";
|
package/dist/core/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { LAMBDA, calculateFreshnessScore, scoreLabel } from "./decay.js";
|
|
1
|
+
export { LAMBDA, calculateFreshnessScore, scoreLabel, stalenessVerdict, computeRevalidateAfter } from "./decay.js";
|
|
2
2
|
export { looksLikeFailedAdapterContent } from "./guards.js";
|
|
3
3
|
export { stampFreshness, toStructuredJSON, formatForLLM } from "./envelope.js";
|
|
4
4
|
export { explainSignal } from "./explain.js";
|
|
@@ -6,8 +6,8 @@ export { rankSignals, rankSignal, clampScore } from "./rank.js";
|
|
|
6
6
|
export { calculateContextUtility } from "./utility.js";
|
|
7
7
|
export { SIGNAL_CONTRACT_VERSION, normalizeSignal } from "./signal.js";
|
|
8
8
|
export { evaluateSignal, evaluateSignals } from "./pipeline.js";
|
|
9
|
-
export { interpretEvaluation, interpretEvaluations } from "./decision.js";
|
|
9
|
+
export { interpretEvaluation, interpretEvaluations, computeVerdictId } from "./decision.js";
|
|
10
10
|
export { toReadableContextResult } from "./readable.js";
|
|
11
11
|
export { prepareProvenanceReadiness } from "./provenanceReadiness.js";
|
|
12
12
|
export { BUILT_IN_SOURCE_PROFILES, getSourceProfile, listSourceProfiles } from "./sourceProfiles.js";
|
|
13
|
-
export { canonicalizeHaPriContent, sha256Hex, calculateHaPriV2, verifyHaPriV2, } from "./provenance.js";
|
|
13
|
+
export { canonicalizeHaPriContent, sha256Hex, calculateHaPriV2, buildHaPriPayload, buildHaPriPayloadV3, verifyHaPriV2, } from "./provenance.js";
|
package/dist/core/pipeline.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LAMBDA, calculateFreshnessScore } from "./decay.js";
|
|
1
|
+
import { LAMBDA, calculateFreshnessScore, computeRevalidateAfter, stalenessVerdict } from "./decay.js";
|
|
2
2
|
import { formatForLLM, toStructuredJSON } from "./envelope.js";
|
|
3
3
|
import { calculateHaPriV2 } from "./provenance.js";
|
|
4
4
|
import { prepareProvenanceReadiness } from "./provenanceReadiness.js";
|
|
@@ -22,6 +22,13 @@ function envelopeConfidence(signal) {
|
|
|
22
22
|
function createEnvelope(signal, freshnessScore, options) {
|
|
23
23
|
if (!options.includeEnvelope || signal.content === undefined)
|
|
24
24
|
return undefined;
|
|
25
|
+
// Mirrors the same failed/unknown override applied to freshnessScore at the
|
|
26
|
+
// call site, so staleness === "unknown" iff revalidate_after === null holds
|
|
27
|
+
// here too (computeRevalidateAfter's own date guard alone wouldn't catch a
|
|
28
|
+
// failed-status signal that still carries a parseable published_at).
|
|
29
|
+
const revalidate_after = signal.status === "failed" || signal.date_confidence === "unknown"
|
|
30
|
+
? null
|
|
31
|
+
: computeRevalidateAfter(signal.published_at, signal.retrieved_at, signal.source_type);
|
|
25
32
|
const ctx = {
|
|
26
33
|
content: signal.content.slice(0, options.envelopeMaxLength ?? 8000),
|
|
27
34
|
source_url: signal.source,
|
|
@@ -30,6 +37,8 @@ function createEnvelope(signal, freshnessScore, options) {
|
|
|
30
37
|
freshness_confidence: envelopeConfidence(signal),
|
|
31
38
|
freshness_score: freshnessScore,
|
|
32
39
|
adapter: signal.source_type,
|
|
40
|
+
staleness: stalenessVerdict(freshnessScore),
|
|
41
|
+
revalidate_after,
|
|
33
42
|
};
|
|
34
43
|
return {
|
|
35
44
|
context: ctx,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import type { HaPriV2Input, HaPriV2Result, HaPriV2VerificationResult } from "./types.js";
|
|
1
|
+
import type { HaPriV2Input, HaPriV3Input, HaPriV2Result, HaPriV2VerificationResult } from "./types.js";
|
|
2
2
|
export declare function canonicalizeHaPriContent(input: string): string;
|
|
3
3
|
export declare function sha256Hex(input: string): string;
|
|
4
4
|
export declare function calculateHaPriV2(input: HaPriV2Input): HaPriV2Result;
|
|
5
|
+
export declare function buildHaPriPayload(input: HaPriV2Input): string;
|
|
6
|
+
export declare function buildHaPriPayloadV3(input: HaPriV3Input): string;
|
|
5
7
|
export declare function verifyHaPriV2(input: HaPriV2Input, actualSig: string | null | undefined): HaPriV2VerificationResult;
|