freshcontext-mcp 0.3.19 → 0.3.21
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 +317 -0
- package/METHODOLOGY.md +381 -0
- package/README.md +55 -5
- package/SECURITY.md +9 -7
- package/dist/adapters/arxiv.d.ts +15 -0
- package/dist/adapters/arxiv.js +3 -2
- package/dist/adapters/changelog.d.ts +2 -0
- package/dist/adapters/changelog.js +4 -2
- package/dist/adapters/finance.d.ts +2 -0
- package/dist/adapters/finance.js +1 -1
- package/dist/adapters/gdelt.d.ts +2 -0
- package/dist/adapters/gdelt.js +1 -1
- package/dist/adapters/gebiz.d.ts +2 -0
- package/dist/adapters/gebiz.js +1 -1
- package/dist/adapters/github.d.ts +2 -0
- package/dist/adapters/govcontracts.d.ts +2 -0
- package/dist/adapters/hackernews.d.ts +2 -0
- package/dist/adapters/jobs.d.ts +2 -0
- package/dist/adapters/jobs.js +6 -6
- package/dist/adapters/packageTrends.d.ts +2 -0
- package/dist/adapters/productHunt.d.ts +2 -0
- package/dist/adapters/reddit.d.ts +8 -0
- package/dist/adapters/reddit.js +12 -5
- package/dist/adapters/registry.d.ts +19 -0
- package/dist/adapters/repoSearch.d.ts +2 -0
- package/dist/adapters/repoSearch.js +1 -1
- package/dist/adapters/scholar.d.ts +2 -0
- package/dist/adapters/secFilings.d.ts +2 -0
- package/dist/adapters/secFilings.js +1 -1
- package/dist/adapters/yc.d.ts +2 -0
- package/dist/core/decay.d.ts +5 -0
- package/dist/core/decision.d.ts +3 -0
- package/dist/core/decision.js +1 -3
- package/dist/core/envelope.d.ts +5 -0
- package/dist/core/envelope.js +9 -1
- package/dist/core/explain.d.ts +12 -0
- package/dist/core/guards.d.ts +1 -0
- package/dist/core/index.d.ts +14 -0
- package/dist/core/index.js +2 -0
- package/dist/core/pipeline.d.ts +3 -0
- package/dist/core/pipeline.js +8 -0
- package/dist/core/provenance.d.ts +5 -0
- package/dist/core/provenanceReadiness.d.ts +2 -0
- package/dist/core/provenanceReadiness.js +220 -0
- package/dist/core/rank.d.ts +4 -0
- package/dist/core/readable.d.ts +2 -0
- package/dist/core/readable.js +75 -0
- package/dist/core/signal.d.ts +3 -0
- package/dist/core/sourceProfiles.d.ts +4 -0
- package/dist/core/types.d.ts +239 -0
- package/dist/core/utility.d.ts +2 -0
- package/dist/rest/handler.d.ts +1 -0
- package/dist/security.d.ts +15 -0
- package/dist/security.js +3 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.js +2 -2
- package/dist/tools/evaluateContext.d.ts +21 -0
- package/dist/tools/evaluateContext.js +22 -1
- package/dist/tools/freshnessStamp.d.ts +1 -0
- package/dist/types.d.ts +1 -0
- package/docs/API_DESIGN.md +28 -1
- package/docs/CLIENT_SETUP.md +166 -0
- package/docs/CODEX_MCP_USAGE.md +4 -4
- package/docs/CORE_API.md +69 -5
- package/docs/CORE_MCP_BOUNDARY.md +13 -4
- package/docs/FUTURE_LANES.md +26 -3
- package/docs/HA_PRI_V2_DESIGN.md +7 -1
- package/docs/HA_PRI_V2_PRODUCTION_ENFORCEMENT_PLAN.md +414 -0
- package/docs/HUMAN_READABLE_OUTPUT_CONTRACT.md +293 -0
- package/docs/RELEASE_INTEGRITY.md +1 -1
- package/docs/RELEASE_NOTES.md +33 -5
- package/docs/SIGNAL_CONTRACT.md +200 -2
- package/package-script-guard.mjs +59 -3
- package/package.json +33 -13
- package/server.json +2 -2
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.3.
|
|
134
|
+
headers: { "User-Agent": "freshcontext-mcp/0.3.21", "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.3.
|
|
164
|
+
headers: { "User-Agent": "freshcontext-mcp/0.3.21", "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.3.
|
|
202
|
+
headers: { "User-Agent": "freshcontext-mcp/0.3.21", "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.3.
|
|
236
|
+
headers: { "User-Agent": "freshcontext-mcp/0.3.21", "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.3.
|
|
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.3.21" } });
|
|
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.3.
|
|
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.3.21" } });
|
|
277
277
|
if (!commentsRes.ok)
|
|
278
278
|
throw new Error(`HN comments ${commentsRes.status}`);
|
|
279
279
|
const commentsData = await commentsRes.json();
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { AdapterResult, ExtractOptions } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Reddit adapter — public JSON API, no auth required.
|
|
4
|
+
* Accepts subreddit URLs or search queries.
|
|
5
|
+
* e.g. https://www.reddit.com/r/MachineLearning/.json
|
|
6
|
+
* https://www.reddit.com/search.json?q=mcp+server&sort=hot
|
|
7
|
+
*/
|
|
8
|
+
export declare function redditAdapter(options: ExtractOptions): Promise<AdapterResult>;
|
package/dist/adapters/reddit.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sanitizeQuery, validateUrl } from "../security.js";
|
|
1
2
|
/**
|
|
2
3
|
* Reddit adapter — public JSON API, no auth required.
|
|
3
4
|
* Accepts subreddit URLs or search queries.
|
|
@@ -6,10 +7,15 @@
|
|
|
6
7
|
*/
|
|
7
8
|
export async function redditAdapter(options) {
|
|
8
9
|
let apiUrl = options.url;
|
|
9
|
-
// If they pass a plain subreddit
|
|
10
|
+
// If they pass a plain subreddit or search query, build a Reddit JSON URL.
|
|
10
11
|
if (!apiUrl.startsWith("http")) {
|
|
11
|
-
const clean = apiUrl.replace(/^r\//, "");
|
|
12
|
-
|
|
12
|
+
const clean = sanitizeQuery(apiUrl, 120).replace(/^r\//, "").replace(/^\/+|\/+$/g, "");
|
|
13
|
+
if (/^[A-Za-z0-9_]{2,21}$/.test(clean)) {
|
|
14
|
+
apiUrl = `https://www.reddit.com/r/${clean}/.json?limit=25&sort=hot`;
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
apiUrl = `https://www.reddit.com/search.json?q=${encodeURIComponent(clean)}&sort=new&limit=25`;
|
|
18
|
+
}
|
|
13
19
|
}
|
|
14
20
|
// Ensure we hit the JSON endpoint
|
|
15
21
|
if (!apiUrl.includes(".json")) {
|
|
@@ -19,9 +25,10 @@ export async function redditAdapter(options) {
|
|
|
19
25
|
if (!apiUrl.includes("limit=")) {
|
|
20
26
|
apiUrl += (apiUrl.includes("?") ? "&" : "?") + "limit=25";
|
|
21
27
|
}
|
|
22
|
-
const
|
|
28
|
+
const safeUrl = validateUrl(apiUrl, "reddit");
|
|
29
|
+
const res = await fetch(safeUrl, {
|
|
23
30
|
headers: {
|
|
24
|
-
"User-Agent": "freshcontext-mcp/0.
|
|
31
|
+
"User-Agent": "freshcontext-mcp/0.3.21 (https://github.com/PrinceGabriel-lgtm/freshcontext-mcp)",
|
|
25
32
|
"Accept": "application/json",
|
|
26
33
|
},
|
|
27
34
|
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { SourceProfileId } from "../core/index.js";
|
|
2
|
+
export type AdapterRisk = "low" | "medium" | "high";
|
|
3
|
+
export type AdapterOutputMode = "single" | "batch" | "composite";
|
|
4
|
+
export type AdapterRuntimeKind = "api" | "browser" | "composite" | "mixed" | "local";
|
|
5
|
+
export interface FreshContextAdapterDescriptor {
|
|
6
|
+
adapter_id: string;
|
|
7
|
+
tool_name: string;
|
|
8
|
+
source_profile: SourceProfileId;
|
|
9
|
+
secondary_source_profiles?: SourceProfileId[];
|
|
10
|
+
output_mode: AdapterOutputMode;
|
|
11
|
+
runtime_kind: AdapterRuntimeKind;
|
|
12
|
+
risk: AdapterRisk;
|
|
13
|
+
notes?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare const BUILT_IN_ADAPTER_REGISTRY: readonly FreshContextAdapterDescriptor[];
|
|
16
|
+
export declare function listAdapterDescriptors(): FreshContextAdapterDescriptor[];
|
|
17
|
+
export declare function getAdapterDescriptor(adapterIdOrToolName: string): FreshContextAdapterDescriptor | undefined;
|
|
18
|
+
export declare function listAdaptersBySourceProfile(profileId: SourceProfileId): FreshContextAdapterDescriptor[];
|
|
19
|
+
export declare function listAdaptersByRisk(risk: AdapterRisk): FreshContextAdapterDescriptor[];
|
|
@@ -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.3.
|
|
25
|
+
"User-Agent": "freshcontext-mcp/0.3.21 (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.3.
|
|
15
|
+
"User-Agent": "freshcontext-mcp/0.3.21 (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,5 @@
|
|
|
1
|
+
export declare const LAMBDA: Record<string, number>;
|
|
2
|
+
export declare const FUTURE_CLOCK_SKEW_TOLERANCE_MS: number;
|
|
3
|
+
export declare function isMeaningfullyFutureDate(content_date: string | null, retrieved_at: string): boolean;
|
|
4
|
+
export declare function calculateFreshnessScore(content_date: string | null, retrieved_at: string, adapter: string): number | null;
|
|
5
|
+
export declare function scoreLabel(score: number | null): string;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ContextDecisionOptions, ContextDecisionResult, CoreSignalEvaluationResult } from "./types.js";
|
|
2
|
+
export declare function interpretEvaluation(evaluation: CoreSignalEvaluationResult, options?: ContextDecisionOptions): ContextDecisionResult;
|
|
3
|
+
export declare function interpretEvaluations(evaluations: CoreSignalEvaluationResult[], options?: ContextDecisionOptions): ContextDecisionResult[];
|
package/dist/core/decision.js
CHANGED
|
@@ -125,7 +125,6 @@ export function interpretEvaluation(evaluation, options = {}) {
|
|
|
125
125
|
]);
|
|
126
126
|
const warnings = [...nonAdviceWarnings(intentProfile)];
|
|
127
127
|
const finalScore = evaluation.ranked.final_score;
|
|
128
|
-
const utilityScore = evaluation.utility.score;
|
|
129
128
|
const freshnessScore = evaluation.freshness_score;
|
|
130
129
|
const confidence = evaluation.ranked.confidence;
|
|
131
130
|
const isFailed = evaluation.signal.status === "failed"
|
|
@@ -153,7 +152,6 @@ export function interpretEvaluation(evaluation, options = {}) {
|
|
|
153
152
|
if (finalScore >= 0.85
|
|
154
153
|
&& freshnessScore !== null
|
|
155
154
|
&& freshnessScore >= 70
|
|
156
|
-
&& utilityScore >= 60
|
|
157
155
|
&& confidence === "high") {
|
|
158
156
|
if (sourceProfile?.authority_hint === "high" && isCitationIntent(intentProfile)) {
|
|
159
157
|
return decisionResult("cite_as_primary", reasons, warnings);
|
|
@@ -163,7 +161,7 @@ export function interpretEvaluation(evaluation, options = {}) {
|
|
|
163
161
|
if (finalScore >= 0.55 && freshnessScore !== null && freshnessScore < 50) {
|
|
164
162
|
return decisionResult(isCitationIntent(intentProfile) ? "cite_as_supporting" : "use_as_background", reasons, warnings);
|
|
165
163
|
}
|
|
166
|
-
if (finalScore < 0.35
|
|
164
|
+
if (finalScore < 0.35) {
|
|
167
165
|
return decisionResult(confidence === "low" ? "exclude" : "watch_only", reasons, warnings);
|
|
168
166
|
}
|
|
169
167
|
if (finalScore >= 0.55) {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { FreshContext, AdapterResult, ExtractOptions, EnvelopeFormatOptions } from "./types.js";
|
|
2
|
+
export declare const MAX_ENVELOPE_CONTENT_LENGTH = 20000;
|
|
3
|
+
export declare function stampFreshness(result: AdapterResult, options: ExtractOptions, adapter: string): FreshContext;
|
|
4
|
+
export declare function toStructuredJSON(ctx: FreshContext): object;
|
|
5
|
+
export declare function formatForLLM(ctx: FreshContext, options?: EnvelopeFormatOptions): string;
|
package/dist/core/envelope.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { calculateFreshnessScore, isMeaningfullyFutureDate, scoreLabel } from "./decay.js";
|
|
2
2
|
import { looksLikeFailedAdapterContent } from "./guards.js";
|
|
3
|
+
export const MAX_ENVELOPE_CONTENT_LENGTH = 20000;
|
|
4
|
+
function clampEnvelopeMaxLength(maxLength) {
|
|
5
|
+
if (maxLength === 0)
|
|
6
|
+
return 0;
|
|
7
|
+
if (maxLength === undefined || !Number.isFinite(maxLength))
|
|
8
|
+
return 8000;
|
|
9
|
+
return Math.min(MAX_ENVELOPE_CONTENT_LENGTH, Math.max(1, Math.floor(maxLength)));
|
|
10
|
+
}
|
|
3
11
|
export function stampFreshness(result, options, adapter) {
|
|
4
12
|
const retrieved_at = new Date().toISOString();
|
|
5
13
|
const failedContent = looksLikeFailedAdapterContent(result.raw);
|
|
@@ -8,7 +16,7 @@ export function stampFreshness(result, options, adapter) {
|
|
|
8
16
|
const freshness_confidence = failedContent || futureDated ? "low" : result.freshness_confidence;
|
|
9
17
|
const freshness_score = calculateFreshnessScore(content_date, retrieved_at, adapter);
|
|
10
18
|
return {
|
|
11
|
-
content: result.raw.slice(0, options.maxLength
|
|
19
|
+
content: result.raw.slice(0, clampEnvelopeMaxLength(options.maxLength)),
|
|
12
20
|
source_url: options.url,
|
|
13
21
|
content_date,
|
|
14
22
|
retrieved_at,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SignalConfidence } from "./types.js";
|
|
2
|
+
interface ExplainSignalInput {
|
|
3
|
+
source?: string;
|
|
4
|
+
source_type?: string;
|
|
5
|
+
semantic_score: number;
|
|
6
|
+
freshness_score: number | null;
|
|
7
|
+
final_score: number;
|
|
8
|
+
confidence: SignalConfidence;
|
|
9
|
+
published_at?: string | null;
|
|
10
|
+
}
|
|
11
|
+
export declare function explainSignal(input: ExplainSignalInput): string;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function looksLikeFailedAdapterContent(raw: string): boolean;
|
|
@@ -0,0 +1,14 @@
|
|
|
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 { toReadableContextResult } from "./readable.js";
|
|
11
|
+
export { prepareProvenanceReadiness } from "./provenanceReadiness.js";
|
|
12
|
+
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";
|
package/dist/core/index.js
CHANGED
|
@@ -7,5 +7,7 @@ export { calculateContextUtility } from "./utility.js";
|
|
|
7
7
|
export { SIGNAL_CONTRACT_VERSION, normalizeSignal } from "./signal.js";
|
|
8
8
|
export { evaluateSignal, evaluateSignals } from "./pipeline.js";
|
|
9
9
|
export { interpretEvaluation, interpretEvaluations } from "./decision.js";
|
|
10
|
+
export { toReadableContextResult } from "./readable.js";
|
|
11
|
+
export { prepareProvenanceReadiness } from "./provenanceReadiness.js";
|
|
10
12
|
export { BUILT_IN_SOURCE_PROFILES, getSourceProfile, listSourceProfiles } from "./sourceProfiles.js";
|
|
11
13
|
export { canonicalizeHaPriContent, sha256Hex, calculateHaPriV2, verifyHaPriV2, } from "./provenance.js";
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { CoreSignalEvaluationOptions, CoreSignalEvaluationResult, FreshContextSignalInput } from "./types.js";
|
|
2
|
+
export declare function evaluateSignal(input: FreshContextSignalInput, options?: CoreSignalEvaluationOptions): CoreSignalEvaluationResult;
|
|
3
|
+
export declare function evaluateSignals(inputs: FreshContextSignalInput[], options?: CoreSignalEvaluationOptions): CoreSignalEvaluationResult[];
|
package/dist/core/pipeline.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { LAMBDA, calculateFreshnessScore } from "./decay.js";
|
|
2
2
|
import { formatForLLM, toStructuredJSON } from "./envelope.js";
|
|
3
3
|
import { calculateHaPriV2 } from "./provenance.js";
|
|
4
|
+
import { prepareProvenanceReadiness } from "./provenanceReadiness.js";
|
|
4
5
|
import { rankSignal } from "./rank.js";
|
|
5
6
|
import { normalizeSignal } from "./signal.js";
|
|
6
7
|
import { calculateContextUtility } from "./utility.js";
|
|
@@ -79,6 +80,12 @@ export function evaluateSignal(input, options = {}) {
|
|
|
79
80
|
const reasons = [...signal.reasons, ...utility.reasons];
|
|
80
81
|
const envelope = createEnvelope(signal, freshness_score, options);
|
|
81
82
|
const provenance = createProvenance(signal, options, reasons);
|
|
83
|
+
const provenance_readiness = prepareProvenanceReadiness(signal, {
|
|
84
|
+
now: options.now,
|
|
85
|
+
resultId: options.provenance?.resultId ?? signal.id,
|
|
86
|
+
semanticFingerprint: options.provenance?.semanticFingerprint ?? null,
|
|
87
|
+
engineVersion: options.provenance?.engineVersion,
|
|
88
|
+
});
|
|
82
89
|
return {
|
|
83
90
|
signal,
|
|
84
91
|
freshness_score,
|
|
@@ -87,6 +94,7 @@ export function evaluateSignal(input, options = {}) {
|
|
|
87
94
|
explanation: ranked.reason,
|
|
88
95
|
envelope,
|
|
89
96
|
provenance,
|
|
97
|
+
provenance_readiness,
|
|
90
98
|
reasons,
|
|
91
99
|
};
|
|
92
100
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { HaPriV2Input, HaPriV2Result, HaPriV2VerificationResult } from "./types.js";
|
|
2
|
+
export declare function canonicalizeHaPriContent(input: string): string;
|
|
3
|
+
export declare function sha256Hex(input: string): string;
|
|
4
|
+
export declare function calculateHaPriV2(input: HaPriV2Input): HaPriV2Result;
|
|
5
|
+
export declare function verifyHaPriV2(input: HaPriV2Input, actualSig: string | null | undefined): HaPriV2VerificationResult;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { calculateHaPriV2, canonicalizeHaPriContent, sha256Hex } from "./provenance.js";
|
|
2
|
+
import { SIGNAL_CONTRACT_VERSION, normalizeSignal } from "./signal.js";
|
|
3
|
+
const UNUSABLE_SOURCE_VALUES = new Set([
|
|
4
|
+
"unknown",
|
|
5
|
+
"n/a",
|
|
6
|
+
"na",
|
|
7
|
+
"none",
|
|
8
|
+
"null",
|
|
9
|
+
"undefined",
|
|
10
|
+
"not provided",
|
|
11
|
+
"tbd",
|
|
12
|
+
]);
|
|
13
|
+
const WEAK_SOURCE_TYPES = new Set(["", "default", "unknown", "custom"]);
|
|
14
|
+
const DERIVED_SOURCE_TYPES = new Set([
|
|
15
|
+
"copy",
|
|
16
|
+
"copied",
|
|
17
|
+
"derived",
|
|
18
|
+
"excerpt",
|
|
19
|
+
"local",
|
|
20
|
+
"local_custom",
|
|
21
|
+
"local_file",
|
|
22
|
+
"local_handoff",
|
|
23
|
+
"secondary",
|
|
24
|
+
"summary",
|
|
25
|
+
]);
|
|
26
|
+
function cleanString(value) {
|
|
27
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
28
|
+
}
|
|
29
|
+
function unique(values) {
|
|
30
|
+
return [...new Set(values.filter(Boolean))];
|
|
31
|
+
}
|
|
32
|
+
function isNormalizedSignal(input) {
|
|
33
|
+
return input.contract_version === SIGNAL_CONTRACT_VERSION;
|
|
34
|
+
}
|
|
35
|
+
function coerceInput(input) {
|
|
36
|
+
const value = input;
|
|
37
|
+
return {
|
|
38
|
+
...value,
|
|
39
|
+
source: cleanString(value.source) ?? "",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function metadataString(signal, keys) {
|
|
43
|
+
for (const key of keys) {
|
|
44
|
+
const value = signal.metadata[key];
|
|
45
|
+
if (typeof value === "string" && value.trim().length > 0)
|
|
46
|
+
return value.trim();
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
function metadataBoolean(signal, keys) {
|
|
51
|
+
return keys.some((key) => signal.metadata[key] === true);
|
|
52
|
+
}
|
|
53
|
+
function hasStringField(input, field) {
|
|
54
|
+
return cleanString(input[field]) !== null;
|
|
55
|
+
}
|
|
56
|
+
function sourceIdentityCompleteness(source) {
|
|
57
|
+
if (!source)
|
|
58
|
+
return "missing";
|
|
59
|
+
const lower = source.toLowerCase();
|
|
60
|
+
if (UNUSABLE_SOURCE_VALUES.has(lower))
|
|
61
|
+
return "unusable";
|
|
62
|
+
if (source.length < 4)
|
|
63
|
+
return "weak";
|
|
64
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(source))
|
|
65
|
+
return "complete";
|
|
66
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(source))
|
|
67
|
+
return "complete";
|
|
68
|
+
if (/^[a-z]:\\/i.test(source) || source.startsWith("/") || source.startsWith("\\\\"))
|
|
69
|
+
return "complete";
|
|
70
|
+
if (source.includes("/") || source.includes("\\") || source.includes("."))
|
|
71
|
+
return "complete";
|
|
72
|
+
return source.length >= 12 ? "complete" : "weak";
|
|
73
|
+
}
|
|
74
|
+
function isDerivedContext(signal) {
|
|
75
|
+
const sourceType = signal.source_type.toLowerCase();
|
|
76
|
+
if (DERIVED_SOURCE_TYPES.has(sourceType))
|
|
77
|
+
return true;
|
|
78
|
+
if (metadataBoolean(signal, ["is_derived", "derived", "copied", "is_secondary"]))
|
|
79
|
+
return true;
|
|
80
|
+
return metadataString(signal, [
|
|
81
|
+
"derived_from",
|
|
82
|
+
"copied_from",
|
|
83
|
+
"original_source",
|
|
84
|
+
"origin_source",
|
|
85
|
+
"source_chain",
|
|
86
|
+
]) !== null;
|
|
87
|
+
}
|
|
88
|
+
function timingCompleteness(signal, retrievedAtWasProvided) {
|
|
89
|
+
if (signal.status === "failed")
|
|
90
|
+
return "unknown";
|
|
91
|
+
if (!signal.published_at && !signal.retrieved_at)
|
|
92
|
+
return "missing";
|
|
93
|
+
if (!signal.published_at || signal.date_confidence === "unknown" || retrievedAtWasProvided === false) {
|
|
94
|
+
return "partial";
|
|
95
|
+
}
|
|
96
|
+
return "complete";
|
|
97
|
+
}
|
|
98
|
+
function readinessState(input) {
|
|
99
|
+
if (input.signal.status === "failed" || input.sourceCompleteness === "unusable")
|
|
100
|
+
return "unknown";
|
|
101
|
+
if (input.derived)
|
|
102
|
+
return "derived";
|
|
103
|
+
if (input.sourceCompleteness === "missing" || !input.hasCanonicalHash)
|
|
104
|
+
return "incomplete";
|
|
105
|
+
if (input.sourceCompleteness === "complete"
|
|
106
|
+
&& input.timingCompleteness === "complete"
|
|
107
|
+
&& !input.sourceTypeWeak) {
|
|
108
|
+
return "complete";
|
|
109
|
+
}
|
|
110
|
+
return "partial";
|
|
111
|
+
}
|
|
112
|
+
function canonicalContentHash(signal) {
|
|
113
|
+
return signal.content === undefined ? null : sha256Hex(canonicalizeHaPriContent(signal.content));
|
|
114
|
+
}
|
|
115
|
+
function prepareHaPriV2(input) {
|
|
116
|
+
if (input.signal.status === "failed") {
|
|
117
|
+
input.reasons.push("Ha-Pri v2 identity material was not prepared for failed context");
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
if (!input.signal.content) {
|
|
121
|
+
input.reasons.push("Ha-Pri v2 identity material needs content");
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
if (!input.resultId) {
|
|
125
|
+
input.reasons.push("Ha-Pri v2 identity material needs a result id or stable source identity");
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
if (!input.engineVersion) {
|
|
129
|
+
input.reasons.push("Ha-Pri v2 identity material needs an explicit engine version");
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
input.reasons.push("Ha-Pri v2 identity material prepared from caller-provided context");
|
|
133
|
+
return calculateHaPriV2({
|
|
134
|
+
resultId: input.resultId,
|
|
135
|
+
rawContent: input.signal.content,
|
|
136
|
+
semanticFingerprint: input.semanticFingerprint,
|
|
137
|
+
adapter: input.signal.source_type,
|
|
138
|
+
publishedAt: input.signal.published_at,
|
|
139
|
+
retrievedAt: input.signal.retrieved_at,
|
|
140
|
+
engineVersion: input.engineVersion,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
export function prepareProvenanceReadiness(input, options = {}) {
|
|
144
|
+
const normalizedInput = isNormalizedSignal(input);
|
|
145
|
+
const signal = normalizedInput ? input : normalizeSignal(coerceInput(input), options);
|
|
146
|
+
const warnings = [];
|
|
147
|
+
const reasons = [...signal.reasons];
|
|
148
|
+
const source = cleanString(signal.source);
|
|
149
|
+
const sourceCompleteness = sourceIdentityCompleteness(source);
|
|
150
|
+
const sourceType = cleanString(signal.source_type);
|
|
151
|
+
const sourceTypeWeak = !sourceType || WEAK_SOURCE_TYPES.has(sourceType.toLowerCase());
|
|
152
|
+
const retrievedAtWasProvided = normalizedInput ? null : hasStringField(input, "retrieved_at");
|
|
153
|
+
const timeCompleteness = timingCompleteness(signal, retrievedAtWasProvided);
|
|
154
|
+
const canonicalHash = canonicalContentHash(signal);
|
|
155
|
+
const semanticFingerprint = cleanString(options.semanticFingerprint)
|
|
156
|
+
?? metadataString(signal, ["semantic_fingerprint", "semanticFingerprint"]);
|
|
157
|
+
const semanticFingerprintHash = semanticFingerprint ? sha256Hex(semanticFingerprint) : null;
|
|
158
|
+
const resultId = cleanString(options.resultId)
|
|
159
|
+
?? cleanString(signal.id)
|
|
160
|
+
?? metadataString(signal, ["result_id", "resultId", "source_id", "sourceId"])
|
|
161
|
+
?? (sourceCompleteness === "complete" ? source : null);
|
|
162
|
+
const engineVersion = cleanString(options.engineVersion);
|
|
163
|
+
const derived = isDerivedContext(signal);
|
|
164
|
+
if (sourceCompleteness === "missing")
|
|
165
|
+
warnings.push("source identity is missing");
|
|
166
|
+
if (sourceCompleteness === "weak")
|
|
167
|
+
warnings.push("source identity is weak");
|
|
168
|
+
if (sourceCompleteness === "unusable")
|
|
169
|
+
warnings.push("source identity is unusable");
|
|
170
|
+
if (sourceTypeWeak)
|
|
171
|
+
warnings.push("source_type is missing or generic");
|
|
172
|
+
if (!signal.published_at)
|
|
173
|
+
warnings.push("published_at is missing or unusable");
|
|
174
|
+
if (retrievedAtWasProvided === false)
|
|
175
|
+
warnings.push("retrieved_at was missing; normalization time was used");
|
|
176
|
+
if (signal.date_confidence === "unknown")
|
|
177
|
+
warnings.push("timing confidence is unknown");
|
|
178
|
+
if (!canonicalHash)
|
|
179
|
+
warnings.push("content is missing; canonical content hash is unavailable");
|
|
180
|
+
if (!semanticFingerprintHash)
|
|
181
|
+
reasons.push("semantic fingerprint was not provided");
|
|
182
|
+
if (derived)
|
|
183
|
+
warnings.push("context appears copied, local, secondary, or derived; preserve the upstream source chain");
|
|
184
|
+
if (signal.status === "failed")
|
|
185
|
+
warnings.push("failed context has unknown provenance readiness");
|
|
186
|
+
const haPriV2 = prepareHaPriV2({
|
|
187
|
+
signal,
|
|
188
|
+
resultId,
|
|
189
|
+
semanticFingerprint,
|
|
190
|
+
engineVersion,
|
|
191
|
+
reasons,
|
|
192
|
+
});
|
|
193
|
+
const state = readinessState({
|
|
194
|
+
signal,
|
|
195
|
+
sourceCompleteness,
|
|
196
|
+
timingCompleteness: timeCompleteness,
|
|
197
|
+
sourceTypeWeak,
|
|
198
|
+
hasCanonicalHash: canonicalHash !== null,
|
|
199
|
+
derived,
|
|
200
|
+
});
|
|
201
|
+
return {
|
|
202
|
+
state,
|
|
203
|
+
source_identity: {
|
|
204
|
+
source,
|
|
205
|
+
source_type: sourceType,
|
|
206
|
+
result_id: resultId,
|
|
207
|
+
completeness: sourceCompleteness,
|
|
208
|
+
},
|
|
209
|
+
source_type: sourceType,
|
|
210
|
+
published_at: signal.published_at,
|
|
211
|
+
retrieved_at: signal.retrieved_at,
|
|
212
|
+
timing_confidence: signal.date_confidence,
|
|
213
|
+
timing_completeness: timeCompleteness,
|
|
214
|
+
canonical_content_sha256: canonicalHash,
|
|
215
|
+
semantic_fingerprint_sha256: semanticFingerprintHash,
|
|
216
|
+
ha_pri_v2: haPriV2,
|
|
217
|
+
warnings: unique(warnings),
|
|
218
|
+
reasons: unique(reasons),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { FreshSignal, RankOptions, RankedSignal } from "./types.js";
|
|
2
|
+
export declare function clampScore(value: number): number;
|
|
3
|
+
export declare function rankSignal(signal: FreshSignal, options?: RankOptions): RankedSignal;
|
|
4
|
+
export declare function rankSignals(signals: FreshSignal[], options?: RankOptions): RankedSignal[];
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const MAX_READABLE_REASONS = 5;
|
|
2
|
+
const HANDOFF_SAFE_REASON = "Decision and complete provenance support agent handoff.";
|
|
3
|
+
const HANDOFF_UNSAFE_DECISION_REASON = "Decision does not support agent handoff.";
|
|
4
|
+
const HANDOFF_UNSAFE_PROVENANCE_REASON = "Provenance is not complete enough for agent handoff.";
|
|
5
|
+
const HANDOFF_SAFE_DECISIONS = new Set([
|
|
6
|
+
"use_first",
|
|
7
|
+
"cite_as_primary",
|
|
8
|
+
"cite_as_supporting",
|
|
9
|
+
"use_as_background",
|
|
10
|
+
]);
|
|
11
|
+
const READER_LABELS = {
|
|
12
|
+
use_first: "Use first",
|
|
13
|
+
cite_as_primary: "Primary source",
|
|
14
|
+
cite_as_supporting: "Supporting source",
|
|
15
|
+
use_as_background: "Background only",
|
|
16
|
+
needs_verification: "Needs verification",
|
|
17
|
+
needs_refresh: "Needs refresh",
|
|
18
|
+
watch_only: "Watch only",
|
|
19
|
+
exclude: "Excluded",
|
|
20
|
+
};
|
|
21
|
+
const SUMMARIES = {
|
|
22
|
+
use_first: "This source is strong enough to use early in the context bundle.",
|
|
23
|
+
cite_as_primary: "This source is strong enough to use as main evidence.",
|
|
24
|
+
cite_as_supporting: "This source is useful as supporting evidence.",
|
|
25
|
+
use_as_background: "This source is useful as background, but should not carry the main claim.",
|
|
26
|
+
needs_verification: "This source may be relevant, but timing, origin, or confidence needs checking.",
|
|
27
|
+
needs_refresh: "This source may be outdated for the current task and should be refreshed.",
|
|
28
|
+
watch_only: "This source may be related, but is too weak to rely on directly.",
|
|
29
|
+
exclude: "This source should not be included as trusted context.",
|
|
30
|
+
};
|
|
31
|
+
const ACTIONS = {
|
|
32
|
+
use_first: "Use this near the front of the selected context while preserving provenance.",
|
|
33
|
+
cite_as_primary: "Use this as main evidence while preserving citation and provenance.",
|
|
34
|
+
cite_as_supporting: "Use this to support or qualify the answer, not as the only source.",
|
|
35
|
+
use_as_background: "Keep this as background context only.",
|
|
36
|
+
needs_verification: "Do not use this as primary evidence until it is checked.",
|
|
37
|
+
needs_refresh: "Look for a newer source before using this as current evidence.",
|
|
38
|
+
watch_only: "Do not rely on this directly; keep it for monitoring or review.",
|
|
39
|
+
exclude: "Keep this out of the final model context unless a human reviews it.",
|
|
40
|
+
};
|
|
41
|
+
function unique(values) {
|
|
42
|
+
return [...new Set(values.filter(Boolean))];
|
|
43
|
+
}
|
|
44
|
+
function handoffFor(evaluation, decision) {
|
|
45
|
+
if (!HANDOFF_SAFE_DECISIONS.has(decision.decision)) {
|
|
46
|
+
return {
|
|
47
|
+
safe_for_agent_handoff: false,
|
|
48
|
+
reason: HANDOFF_UNSAFE_DECISION_REASON,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (evaluation.provenance_readiness.state !== "complete") {
|
|
52
|
+
return {
|
|
53
|
+
safe_for_agent_handoff: false,
|
|
54
|
+
reason: HANDOFF_UNSAFE_PROVENANCE_REASON,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
safe_for_agent_handoff: true,
|
|
59
|
+
reason: HANDOFF_SAFE_REASON,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export function toReadableContextResult(evaluation, decision) {
|
|
63
|
+
const why = unique([
|
|
64
|
+
...decision.reasons,
|
|
65
|
+
evaluation.explanation,
|
|
66
|
+
]).slice(0, MAX_READABLE_REASONS);
|
|
67
|
+
return {
|
|
68
|
+
label: READER_LABELS[decision.decision],
|
|
69
|
+
summary: SUMMARIES[decision.decision],
|
|
70
|
+
why,
|
|
71
|
+
action: ACTIONS[decision.decision],
|
|
72
|
+
warnings: [...decision.warnings],
|
|
73
|
+
handoff: handoffFor(evaluation, decision),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { FreshContextSignal, FreshContextSignalInput, SignalNormalizeOptions } from "./types.js";
|
|
2
|
+
export declare const SIGNAL_CONTRACT_VERSION: "freshcontext.signal.v1";
|
|
3
|
+
export declare function normalizeSignal(input: FreshContextSignalInput, options?: SignalNormalizeOptions): FreshContextSignal;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { SourceProfile, SourceProfileId } from "./types.js";
|
|
2
|
+
export declare const BUILT_IN_SOURCE_PROFILES: Readonly<Record<SourceProfileId, SourceProfile>>;
|
|
3
|
+
export declare function listSourceProfiles(): SourceProfile[];
|
|
4
|
+
export declare function getSourceProfile(profileId: string): SourceProfile | undefined;
|