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.
Files changed (75) hide show
  1. package/FRESHCONTEXT_SPEC.md +317 -0
  2. package/METHODOLOGY.md +381 -0
  3. package/README.md +55 -5
  4. package/SECURITY.md +9 -7
  5. package/dist/adapters/arxiv.d.ts +15 -0
  6. package/dist/adapters/arxiv.js +3 -2
  7. package/dist/adapters/changelog.d.ts +2 -0
  8. package/dist/adapters/changelog.js +4 -2
  9. package/dist/adapters/finance.d.ts +2 -0
  10. package/dist/adapters/finance.js +1 -1
  11. package/dist/adapters/gdelt.d.ts +2 -0
  12. package/dist/adapters/gdelt.js +1 -1
  13. package/dist/adapters/gebiz.d.ts +2 -0
  14. package/dist/adapters/gebiz.js +1 -1
  15. package/dist/adapters/github.d.ts +2 -0
  16. package/dist/adapters/govcontracts.d.ts +2 -0
  17. package/dist/adapters/hackernews.d.ts +2 -0
  18. package/dist/adapters/jobs.d.ts +2 -0
  19. package/dist/adapters/jobs.js +6 -6
  20. package/dist/adapters/packageTrends.d.ts +2 -0
  21. package/dist/adapters/productHunt.d.ts +2 -0
  22. package/dist/adapters/reddit.d.ts +8 -0
  23. package/dist/adapters/reddit.js +12 -5
  24. package/dist/adapters/registry.d.ts +19 -0
  25. package/dist/adapters/repoSearch.d.ts +2 -0
  26. package/dist/adapters/repoSearch.js +1 -1
  27. package/dist/adapters/scholar.d.ts +2 -0
  28. package/dist/adapters/secFilings.d.ts +2 -0
  29. package/dist/adapters/secFilings.js +1 -1
  30. package/dist/adapters/yc.d.ts +2 -0
  31. package/dist/core/decay.d.ts +5 -0
  32. package/dist/core/decision.d.ts +3 -0
  33. package/dist/core/decision.js +1 -3
  34. package/dist/core/envelope.d.ts +5 -0
  35. package/dist/core/envelope.js +9 -1
  36. package/dist/core/explain.d.ts +12 -0
  37. package/dist/core/guards.d.ts +1 -0
  38. package/dist/core/index.d.ts +14 -0
  39. package/dist/core/index.js +2 -0
  40. package/dist/core/pipeline.d.ts +3 -0
  41. package/dist/core/pipeline.js +8 -0
  42. package/dist/core/provenance.d.ts +5 -0
  43. package/dist/core/provenanceReadiness.d.ts +2 -0
  44. package/dist/core/provenanceReadiness.js +220 -0
  45. package/dist/core/rank.d.ts +4 -0
  46. package/dist/core/readable.d.ts +2 -0
  47. package/dist/core/readable.js +75 -0
  48. package/dist/core/signal.d.ts +3 -0
  49. package/dist/core/sourceProfiles.d.ts +4 -0
  50. package/dist/core/types.d.ts +239 -0
  51. package/dist/core/utility.d.ts +2 -0
  52. package/dist/rest/handler.d.ts +1 -0
  53. package/dist/security.d.ts +15 -0
  54. package/dist/security.js +3 -1
  55. package/dist/server.d.ts +2 -0
  56. package/dist/server.js +2 -2
  57. package/dist/tools/evaluateContext.d.ts +21 -0
  58. package/dist/tools/evaluateContext.js +22 -1
  59. package/dist/tools/freshnessStamp.d.ts +1 -0
  60. package/dist/types.d.ts +1 -0
  61. package/docs/API_DESIGN.md +28 -1
  62. package/docs/CLIENT_SETUP.md +166 -0
  63. package/docs/CODEX_MCP_USAGE.md +4 -4
  64. package/docs/CORE_API.md +69 -5
  65. package/docs/CORE_MCP_BOUNDARY.md +13 -4
  66. package/docs/FUTURE_LANES.md +26 -3
  67. package/docs/HA_PRI_V2_DESIGN.md +7 -1
  68. package/docs/HA_PRI_V2_PRODUCTION_ENFORCEMENT_PLAN.md +414 -0
  69. package/docs/HUMAN_READABLE_OUTPUT_CONTRACT.md +293 -0
  70. package/docs/RELEASE_INTEGRITY.md +1 -1
  71. package/docs/RELEASE_NOTES.md +33 -5
  72. package/docs/SIGNAL_CONTRACT.md +200 -2
  73. package/package-script-guard.mjs +59 -3
  74. package/package.json +33 -13
  75. package/server.json +2 -2
@@ -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.0", "Accept": "application/json" },
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.0", "Accept": "application/json" },
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.0", "Accept": "application/json" },
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.0", "Accept": "application/json" },
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.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.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.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.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,2 @@
1
+ import { AdapterResult, ExtractOptions } from "../types.js";
2
+ export declare function packageTrendsAdapter(options: ExtractOptions): Promise<AdapterResult>;
@@ -0,0 +1,2 @@
1
+ import { AdapterResult, ExtractOptions } from "../types.js";
2
+ export declare function productHuntAdapter(options: ExtractOptions): Promise<AdapterResult>;
@@ -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>;
@@ -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 name like "r/MachineLearning", build the URL
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
- apiUrl = `https://www.reddit.com/r/${clean}/.json?limit=25&sort=hot`;
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 res = await fetch(apiUrl, {
28
+ const safeUrl = validateUrl(apiUrl, "reddit");
29
+ const res = await fetch(safeUrl, {
23
30
  headers: {
24
- "User-Agent": "freshcontext-mcp/0.1.5 (https://github.com/PrinceGabriel-lgtm/freshcontext-mcp)",
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[];
@@ -0,0 +1,2 @@
1
+ import { AdapterResult, ExtractOptions } from "../types.js";
2
+ export declare function repoSearchAdapter(options: ExtractOptions): Promise<AdapterResult>;
@@ -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.17 (https://github.com/PrinceGabriel-lgtm/freshcontext-mcp)",
25
+ "User-Agent": "freshcontext-mcp/0.3.21 (https://github.com/PrinceGabriel-lgtm/freshcontext-mcp)",
26
26
  },
27
27
  });
28
28
  if (!res.ok) {
@@ -0,0 +1,2 @@
1
+ import { AdapterResult, ExtractOptions } from "../types.js";
2
+ export declare function scholarAdapter(options: ExtractOptions): Promise<AdapterResult>;
@@ -0,0 +1,2 @@
1
+ import { AdapterResult, ExtractOptions } from "../types.js";
2
+ export declare function secFilingsAdapter(options: ExtractOptions): Promise<AdapterResult>;
@@ -12,7 +12,7 @@
12
12
  */
13
13
  const HEADERS = {
14
14
  "Accept": "application/json",
15
- "User-Agent": "freshcontext-mcp/0.3.17 (https://github.com/PrinceGabriel-lgtm/freshcontext-mcp)",
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,2 @@
1
+ import { AdapterResult, ExtractOptions } from "../types.js";
2
+ export declare function ycAdapter(options: ExtractOptions): Promise<AdapterResult>;
@@ -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[];
@@ -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 && utilityScore < 30) {
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;
@@ -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 ?? 8000),
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";
@@ -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[];
@@ -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,2 @@
1
+ import type { ProvenanceReadinessInput, ProvenanceReadinessOptions, ProvenanceReadinessResult } from "./types.js";
2
+ export declare function prepareProvenanceReadiness(input: ProvenanceReadinessInput, options?: ProvenanceReadinessOptions): ProvenanceReadinessResult;
@@ -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,2 @@
1
+ import type { ContextDecisionResult, CoreSignalEvaluationResult, HumanReadableContextResult } from "./types.js";
2
+ export declare function toReadableContextResult(evaluation: CoreSignalEvaluationResult, decision: ContextDecisionResult): HumanReadableContextResult;
@@ -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;