@evomap/evolver-adapter-public 2.0.0-beta.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/dist/antiAbuseTelemetry.d.ts +36 -0
- package/dist/antiAbuseTelemetry.js +267 -0
- package/dist/atp.d.ts +54 -0
- package/dist/atp.js +140 -0
- package/dist/auth/credentialStore.d.ts +8 -0
- package/dist/auth/credentialStore.js +23 -0
- package/dist/auth/keypair.d.ts +42 -0
- package/dist/auth/keypair.js +80 -0
- package/dist/auth/legacyShim.d.ts +43 -0
- package/dist/auth/legacyShim.js +82 -0
- package/dist/auth/machineId.d.ts +20 -0
- package/dist/auth/machineId.js +38 -0
- package/dist/auth/oauthDeviceToken.d.ts +62 -0
- package/dist/auth/oauthDeviceToken.js +83 -0
- package/dist/auth/oauthHttpTransport.d.ts +33 -0
- package/dist/auth/oauthHttpTransport.js +93 -0
- package/dist/connect.d.ts +40 -0
- package/dist/connect.js +38 -0
- package/dist/hubCapability.d.ts +169 -0
- package/dist/hubCapability.js +899 -0
- package/dist/hubFetch.d.ts +116 -0
- package/dist/hubFetch.js +469 -0
- package/dist/hubReuse.d.ts +112 -0
- package/dist/hubReuse.js +292 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +16 -0
- package/dist/offlinePermit.d.ts +74 -0
- package/dist/offlinePermit.js +309 -0
- package/dist/pricing/modelPrices.d.ts +16 -0
- package/dist/pricing/modelPrices.js +44 -0
- package/dist/wireMap.d.ts +28 -0
- package/dist/wireMap.js +99 -0
- package/package.json +29 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { hub } from '@evomap/evolver-core';
|
|
2
|
+
import type { algo } from '@evomap/evolver-core';
|
|
3
|
+
type HubMetadata = hub.HubMetadata;
|
|
4
|
+
type ReuseDecision = hub.ReuseDecision;
|
|
5
|
+
type GeneCandidateInput = algo.GeneCandidateInput;
|
|
6
|
+
export declare const SEARCH_CACHE_TTL_MS: number;
|
|
7
|
+
export declare const SEARCH_CACHE_MAX = 200;
|
|
8
|
+
export declare const PAYLOAD_CACHE_MAX = 100;
|
|
9
|
+
/** Default reuse mode (ported from v1): 'reference' injects the asset as a strong hint; 'direct' applies it. */
|
|
10
|
+
export type ReuseMode = 'direct' | 'reference';
|
|
11
|
+
export declare const DEFAULT_REUSE_MODE: ReuseMode;
|
|
12
|
+
/** Reads EVOLVER_MIN_REUSE_SCORE here (env is an ADAPTER concern — core never reads it). */
|
|
13
|
+
export declare function getMinReuseScore(env?: NodeJS.ProcessEnv): number;
|
|
14
|
+
/** Reads EVOLVER_REUSE_MODE here (adapter concern). */
|
|
15
|
+
export declare function getReuseMode(env?: NodeJS.ProcessEnv): ReuseMode;
|
|
16
|
+
/** Stable signal fingerprint (ported from v1 _cacheKey: sort + join). */
|
|
17
|
+
export declare function signalFingerprint(signals: readonly string[]): string;
|
|
18
|
+
/**
|
|
19
|
+
* The two-layer reuse cache. Bounded + TTL'd, per-process. A search-cache hit means phase 1 makes ZERO hub
|
|
20
|
+
* calls; a payload-cache hit means phase 3 makes ZERO hub calls. The clock is injected for deterministic tests.
|
|
21
|
+
*/
|
|
22
|
+
export declare class ReuseCache {
|
|
23
|
+
private readonly now;
|
|
24
|
+
private readonly searchTtlMs;
|
|
25
|
+
private readonly searchMax;
|
|
26
|
+
private readonly payloadMax;
|
|
27
|
+
private readonly search;
|
|
28
|
+
private readonly payload;
|
|
29
|
+
constructor(now?: () => number, searchTtlMs?: number, searchMax?: number, payloadMax?: number);
|
|
30
|
+
getSearch(key: string): HubMetadata[] | null;
|
|
31
|
+
setSearch(key: string, value: HubMetadata[]): void;
|
|
32
|
+
getPayload(assetId: string): hub.AssetRecord | null;
|
|
33
|
+
setPayload(assetId: string, payload: hub.AssetRecord): void;
|
|
34
|
+
clear(): void;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Map a hub search row (AssetRecord with arbitrary quality fields) → the core's price-free HubMetadata.
|
|
38
|
+
* Accepts both camelCase and the hub's snake_case (gdi_score / success_rate / reuse_count / ...). Drops any
|
|
39
|
+
* price/credit field by simply not reading it — the core HubMetadata has no slot for cost.
|
|
40
|
+
*/
|
|
41
|
+
export declare function toHubMetadata(rec: hub.AssetRecord): HubMetadata;
|
|
42
|
+
/** Map a fetched hub asset (full payload) → a selection candidate so it competes in candidateAssembly. */
|
|
43
|
+
export declare function toGeneCandidate(rec: hub.AssetRecord): GeneCandidateInput;
|
|
44
|
+
/** A receipt the adapter SURFACES but never interprets (mirrors core's PublishReceipt.economic handling). */
|
|
45
|
+
export interface ReuseReceipt {
|
|
46
|
+
/** The hub's credit_cost block, passed through verbatim for observability/UI. Never gated on. */
|
|
47
|
+
creditCost?: unknown;
|
|
48
|
+
fromCache: boolean;
|
|
49
|
+
}
|
|
50
|
+
export interface ReuseBeforeSolveOptions {
|
|
51
|
+
/** Reuse threshold; default from EVOLVER_MIN_REUSE_SCORE (read in the adapter, never in core). */
|
|
52
|
+
threshold?: number;
|
|
53
|
+
/** Injected clock for recency scoring (passed to core scoreSearchResults). */
|
|
54
|
+
now?: number;
|
|
55
|
+
/** Cap on how many candidates the free search pulls. */
|
|
56
|
+
searchLimit?: number;
|
|
57
|
+
/** Reuse mode label carried into the result (direct/reference). */
|
|
58
|
+
mode?: ReuseMode;
|
|
59
|
+
/** Observability sink (asset-call log). Receives structured records; never throws. */
|
|
60
|
+
log?: {
|
|
61
|
+
append(entry: Record<string, unknown>): void;
|
|
62
|
+
};
|
|
63
|
+
runId?: string | null;
|
|
64
|
+
/**
|
|
65
|
+
* Value-ledger emission seam (#112). Fired exactly once on a reuse HIT (action === 'fetch') with the audit
|
|
66
|
+
* anchors the ledger needs to derive a source=reuse entry: the reused assetId, the cycleId it feeds, the
|
|
67
|
+
* signal fingerprint, and the tokens the fetch actually consumed (≈0 — payload/cache pull, no fresh solve).
|
|
68
|
+
* The composition layer wires this to a `value.reuse_hit` root_event. Optional + must never throw (reuse is
|
|
69
|
+
* an optimization, and observability emission can never be allowed to break it).
|
|
70
|
+
*/
|
|
71
|
+
onReuseHit?: (hit: ReuseHitInfo) => void;
|
|
72
|
+
/** The cycle this resolution feeds — carried into onReuseHit so the event refs the SAME cycleId as the cycle. */
|
|
73
|
+
cycleId?: string;
|
|
74
|
+
}
|
|
75
|
+
/** What onReuseHit reports on a reuse hit — the value-ledger audit anchors (#112). */
|
|
76
|
+
export interface ReuseHitInfo {
|
|
77
|
+
assetId: string;
|
|
78
|
+
cycleId: string;
|
|
79
|
+
signalFingerprint: string;
|
|
80
|
+
/** Tokens the fetch actually consumed (cache/payload pull — typically 0). */
|
|
81
|
+
fetchTokens: number;
|
|
82
|
+
}
|
|
83
|
+
export interface ReuseBeforeSolveResult {
|
|
84
|
+
/** 'fetch' = a winner was fetched and is ready to compete; 'solve-fresh' = nothing worth reusing. */
|
|
85
|
+
action: ReuseDecision['action'];
|
|
86
|
+
/** The fetched winner as a selection candidate (only when action === 'fetch'). */
|
|
87
|
+
candidate?: GeneCandidateInput;
|
|
88
|
+
/** The raw fetched payload (only when action === 'fetch'), for ingest/reference injection. */
|
|
89
|
+
asset?: hub.AssetRecord;
|
|
90
|
+
/** The reuse mode applied. */
|
|
91
|
+
mode: ReuseMode;
|
|
92
|
+
/** Decision score of the winner (rounded), for logging. */
|
|
93
|
+
score?: number;
|
|
94
|
+
/** Economic receipt read-through (read-only; the adapter never interprets credits). */
|
|
95
|
+
receipt?: ReuseReceipt;
|
|
96
|
+
/** True when the whole flow made ZERO hub calls (both layers hit). */
|
|
97
|
+
zeroHubCalls: boolean;
|
|
98
|
+
/** Why we didn't reuse, when action === 'solve-fresh' (no_signals / no_results / below_threshold). */
|
|
99
|
+
reason?: string;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The reuse-before-solve flow. Returns the single winner (already fetched) as a selection candidate, or a
|
|
103
|
+
* solve-fresh verdict. Never throws on a hub error — reuse is an optimization, not a hard dependency:
|
|
104
|
+
* a failed search/fetch degrades to solve-fresh.
|
|
105
|
+
*
|
|
106
|
+
* @param cap the HubCapability (free search + paid fetch).
|
|
107
|
+
* @param cache the two-layer cache (caller owns it across cycles so it actually warms).
|
|
108
|
+
* @param signals the local problem signals.
|
|
109
|
+
*/
|
|
110
|
+
export declare function reuseBeforeSolve(cap: hub.HubCapability, cache: ReuseCache, signals: readonly string[], opts?: ReuseBeforeSolveOptions): Promise<ReuseBeforeSolveResult>;
|
|
111
|
+
export declare function assetMatchesId(asset: hub.AssetRecord | null | undefined, assetId: string): asset is hub.AssetRecord;
|
|
112
|
+
export {};
|
package/dist/hubReuse.js
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
// Reuse-before-solve ORCHESTRATION + BILLING + CACHE (#110) — the adapter half of the cost lever.
|
|
2
|
+
// Ported from v1 hubSearch.js (the two-phase flow + the two-layer cache). The PURE decision lives in core
|
|
3
|
+
// (evolver-core/src/hub/reuseDecision.ts) — this file owns everything core must NOT know about: the paid
|
|
4
|
+
// fetch, the economic receipt read-through, and the cache that turns repeat lookups into ZERO hub calls.
|
|
5
|
+
//
|
|
6
|
+
// Two-phase flow (the #69 lesson: search != fetch — search is free metadata, fetch is the paid content pull):
|
|
7
|
+
// Phase 1 (free): cap.search(query) → candidate metadata only
|
|
8
|
+
// Phase 2 (decide): core scoreSearchResults + decideReuse → AT MOST ONE winner
|
|
9
|
+
// Phase 3 (paid): cap.fetch(winner) → full payload for the single winner (and only then)
|
|
10
|
+
//
|
|
11
|
+
// Two-layer cache (ported from v1):
|
|
12
|
+
// - search cache: signal-fingerprint → phase-1 metadata (short TTL). Repeat signal set → ZERO hub calls.
|
|
13
|
+
// - payload cache: assetId → phase-3 payload (content-addressed, long/permanent, bounded LRU). A cached
|
|
14
|
+
// payload → ZERO fetch. Both clocks are injected so TTL/eviction is deterministic and testable.
|
|
15
|
+
import { hub } from '@evomap/evolver-core';
|
|
16
|
+
const { scoreSearchResults, decideReuse, DEFAULT_MIN_REUSE_SCORE, } = hub;
|
|
17
|
+
const GENE_WIRE_KEYS = new Set([
|
|
18
|
+
'type',
|
|
19
|
+
'schema_version',
|
|
20
|
+
'id',
|
|
21
|
+
'category',
|
|
22
|
+
'signals_match',
|
|
23
|
+
'preconditions',
|
|
24
|
+
'strategy',
|
|
25
|
+
'constraints',
|
|
26
|
+
'validation',
|
|
27
|
+
'summary',
|
|
28
|
+
'epigenetic_marks',
|
|
29
|
+
'learning_history',
|
|
30
|
+
'anti_patterns',
|
|
31
|
+
'routing_hint',
|
|
32
|
+
'tool_policy',
|
|
33
|
+
'asset_id',
|
|
34
|
+
]);
|
|
35
|
+
// ── Cache config (ported from v1 hubSearch.js) ───────────────────────────────
|
|
36
|
+
export const SEARCH_CACHE_TTL_MS = 5 * 60 * 1000; // metadata is hot but staleable — short TTL
|
|
37
|
+
export const SEARCH_CACHE_MAX = 200;
|
|
38
|
+
export const PAYLOAD_CACHE_MAX = 100;
|
|
39
|
+
export const DEFAULT_REUSE_MODE = 'reference';
|
|
40
|
+
/** Reads EVOLVER_MIN_REUSE_SCORE here (env is an ADAPTER concern — core never reads it). */
|
|
41
|
+
export function getMinReuseScore(env = process.env) {
|
|
42
|
+
const n = Number(env['EVOLVER_MIN_REUSE_SCORE']);
|
|
43
|
+
return Number.isFinite(n) && n > 0 ? n : DEFAULT_MIN_REUSE_SCORE;
|
|
44
|
+
}
|
|
45
|
+
/** Reads EVOLVER_REUSE_MODE here (adapter concern). */
|
|
46
|
+
export function getReuseMode(env = process.env) {
|
|
47
|
+
return String(env['EVOLVER_REUSE_MODE'] ?? DEFAULT_REUSE_MODE).toLowerCase() === 'direct' ? 'direct' : 'reference';
|
|
48
|
+
}
|
|
49
|
+
/** Stable signal fingerprint (ported from v1 _cacheKey: sort + join). */
|
|
50
|
+
export function signalFingerprint(signals) {
|
|
51
|
+
return [...signals].map((s) => String(s).trim()).filter(Boolean).sort().join('|');
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The two-layer reuse cache. Bounded + TTL'd, per-process. A search-cache hit means phase 1 makes ZERO hub
|
|
55
|
+
* calls; a payload-cache hit means phase 3 makes ZERO hub calls. The clock is injected for deterministic tests.
|
|
56
|
+
*/
|
|
57
|
+
export class ReuseCache {
|
|
58
|
+
now;
|
|
59
|
+
searchTtlMs;
|
|
60
|
+
searchMax;
|
|
61
|
+
payloadMax;
|
|
62
|
+
search = new Map();
|
|
63
|
+
payload = new Map();
|
|
64
|
+
constructor(now = () => Date.now(), searchTtlMs = SEARCH_CACHE_TTL_MS, searchMax = SEARCH_CACHE_MAX, payloadMax = PAYLOAD_CACHE_MAX) {
|
|
65
|
+
this.now = now;
|
|
66
|
+
this.searchTtlMs = searchTtlMs;
|
|
67
|
+
this.searchMax = searchMax;
|
|
68
|
+
this.payloadMax = payloadMax;
|
|
69
|
+
}
|
|
70
|
+
getSearch(key) {
|
|
71
|
+
const e = this.search.get(key);
|
|
72
|
+
if (!e)
|
|
73
|
+
return null;
|
|
74
|
+
if (this.now() - e.ts > this.searchTtlMs) {
|
|
75
|
+
this.search.delete(key);
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
return e.value;
|
|
79
|
+
}
|
|
80
|
+
setSearch(key, value) {
|
|
81
|
+
if (this.search.size >= this.searchMax) {
|
|
82
|
+
const oldest = this.search.keys().next().value;
|
|
83
|
+
if (oldest !== undefined)
|
|
84
|
+
this.search.delete(oldest);
|
|
85
|
+
}
|
|
86
|
+
this.search.set(key, { ts: this.now(), value });
|
|
87
|
+
}
|
|
88
|
+
getPayload(assetId) {
|
|
89
|
+
const asset = this.payload.get(assetId) ?? null;
|
|
90
|
+
if (!asset)
|
|
91
|
+
return null;
|
|
92
|
+
if (assetMatchesId(asset, assetId))
|
|
93
|
+
return asset;
|
|
94
|
+
this.payload.delete(assetId);
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
setPayload(assetId, payload) {
|
|
98
|
+
if (!assetMatchesId(payload, assetId))
|
|
99
|
+
return;
|
|
100
|
+
if (this.payload.size >= this.payloadMax) {
|
|
101
|
+
const oldest = this.payload.keys().next().value;
|
|
102
|
+
if (oldest !== undefined)
|
|
103
|
+
this.payload.delete(oldest);
|
|
104
|
+
}
|
|
105
|
+
this.payload.set(assetId, payload);
|
|
106
|
+
}
|
|
107
|
+
clear() { this.search.clear(); this.payload.clear(); }
|
|
108
|
+
}
|
|
109
|
+
// ── metadata mapping (hub wire → core HubMetadata, NO price) ──────────────────
|
|
110
|
+
function num(v) {
|
|
111
|
+
const n = Number(v);
|
|
112
|
+
return Number.isFinite(n) ? n : undefined;
|
|
113
|
+
}
|
|
114
|
+
function strArr(v) {
|
|
115
|
+
return Array.isArray(v) ? v.filter((x) => typeof x === 'string') : undefined;
|
|
116
|
+
}
|
|
117
|
+
function ts(v) {
|
|
118
|
+
if (typeof v === 'number' && Number.isFinite(v))
|
|
119
|
+
return v;
|
|
120
|
+
if (typeof v === 'string') {
|
|
121
|
+
const t = Date.parse(v);
|
|
122
|
+
return Number.isFinite(t) ? t : undefined;
|
|
123
|
+
}
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
function stripHubPayloadMetadata(rec) {
|
|
127
|
+
const out = {};
|
|
128
|
+
for (const [key, value] of Object.entries(rec)) {
|
|
129
|
+
if (GENE_WIRE_KEYS.has(key))
|
|
130
|
+
out[key] = value;
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Map a hub search row (AssetRecord with arbitrary quality fields) → the core's price-free HubMetadata.
|
|
136
|
+
* Accepts both camelCase and the hub's snake_case (gdi_score / success_rate / reuse_count / ...). Drops any
|
|
137
|
+
* price/credit field by simply not reading it — the core HubMetadata has no slot for cost.
|
|
138
|
+
*/
|
|
139
|
+
export function toHubMetadata(rec) {
|
|
140
|
+
const r = rec;
|
|
141
|
+
const assetId = String(r['asset_id'] ?? r['assetId'] ?? '');
|
|
142
|
+
const updatedAt = ts(r['updated_at'] ?? r['updatedAt'] ?? r['created_at'] ?? r['createdAt']);
|
|
143
|
+
return {
|
|
144
|
+
assetId,
|
|
145
|
+
...(strArr(r['signals_match'] ?? r['signalsMatch']) ? { signalsMatch: strArr(r['signals_match'] ?? r['signalsMatch']) } : {}),
|
|
146
|
+
...(typeof r['category'] === 'string' ? { category: r['category'] } : {}),
|
|
147
|
+
...(typeof r['summary'] === 'string' ? { summary: r['summary'] } : {}),
|
|
148
|
+
...(typeof r['status'] === 'string' ? { status: r['status'] } : {}),
|
|
149
|
+
...(num(r['confidence']) !== undefined ? { confidence: num(r['confidence']) } : {}),
|
|
150
|
+
...(num(r['success_streak'] ?? r['successStreak']) !== undefined ? { successStreak: num(r['success_streak'] ?? r['successStreak']) } : {}),
|
|
151
|
+
...(num(r['reputation_score'] ?? r['reputationScore']) !== undefined ? { reputationScore: num(r['reputation_score'] ?? r['reputationScore']) } : {}),
|
|
152
|
+
...(num(r['gdi_score'] ?? r['gdiScore']) !== undefined ? { gdiScore: num(r['gdi_score'] ?? r['gdiScore']) } : {}),
|
|
153
|
+
...(num(r['success_rate'] ?? r['successRate']) !== undefined ? { successRate: num(r['success_rate'] ?? r['successRate']) } : {}),
|
|
154
|
+
...(num(r['reuse_count'] ?? r['reuseCount']) !== undefined ? { reuseCount: num(r['reuse_count'] ?? r['reuseCount']) } : {}),
|
|
155
|
+
...(num(r['similarity'] ?? r['semanticSimilarity']) !== undefined ? { semanticSimilarity: num(r['similarity'] ?? r['semanticSimilarity']) } : {}),
|
|
156
|
+
...(updatedAt !== undefined ? { updatedAt } : {}),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/** Map a fetched hub asset (full payload) → a selection candidate so it competes in candidateAssembly. */
|
|
160
|
+
export function toGeneCandidate(rec) {
|
|
161
|
+
const r = rec;
|
|
162
|
+
const assetId = String(r['asset_id'] ?? r['assetId'] ?? '');
|
|
163
|
+
const geneId = typeof r['id'] === 'string' ? r['id'] : assetId;
|
|
164
|
+
const signalsMatch = strArr(r['signals_match'] ?? r['signalsMatch']) ?? [];
|
|
165
|
+
const reuseCount = num(r['reuse_count'] ?? r['reuseCount']) ?? 0;
|
|
166
|
+
return {
|
|
167
|
+
geneId,
|
|
168
|
+
assetId,
|
|
169
|
+
signalsMatch,
|
|
170
|
+
// A freshly-fetched hub gene has no LOCAL learning history yet; selection scores it on signal-match +
|
|
171
|
+
// reuse popularity. The local learning view stays empty (it earns history once it's actually applied).
|
|
172
|
+
view: { geneId, total: 0, success: 0, failed: 0, successRate: 0, avgScore: 0, recentCapsuleIds: [] },
|
|
173
|
+
reuseCount,
|
|
174
|
+
...(typeof r['category'] === 'string' ? { category: r['category'] } : {}),
|
|
175
|
+
...(typeof r['summary'] === 'string' ? { summary: r['summary'] } : {}),
|
|
176
|
+
hubAsset: stripHubPayloadMetadata(rec),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* The reuse-before-solve flow. Returns the single winner (already fetched) as a selection candidate, or a
|
|
181
|
+
* solve-fresh verdict. Never throws on a hub error — reuse is an optimization, not a hard dependency:
|
|
182
|
+
* a failed search/fetch degrades to solve-fresh.
|
|
183
|
+
*
|
|
184
|
+
* @param cap the HubCapability (free search + paid fetch).
|
|
185
|
+
* @param cache the two-layer cache (caller owns it across cycles so it actually warms).
|
|
186
|
+
* @param signals the local problem signals.
|
|
187
|
+
*/
|
|
188
|
+
export async function reuseBeforeSolve(cap, cache, signals, opts = {}) {
|
|
189
|
+
const mode = opts.mode ?? getReuseMode();
|
|
190
|
+
const threshold = opts.threshold ?? getMinReuseScore();
|
|
191
|
+
const runId = opts.runId ?? null;
|
|
192
|
+
const log = opts.log;
|
|
193
|
+
const signalList = signals.map((s) => String(s).trim()).filter(Boolean);
|
|
194
|
+
if (signalList.length === 0) {
|
|
195
|
+
return { action: 'solve-fresh', mode, zeroHubCalls: true, reason: 'no_signals' };
|
|
196
|
+
}
|
|
197
|
+
// ── Phase 1: free search (signal fingerprint cache → ZERO hub calls on hit) ──
|
|
198
|
+
const key = signalFingerprint(signalList);
|
|
199
|
+
let metadata = cache.getSearch(key);
|
|
200
|
+
const searchCached = metadata !== null;
|
|
201
|
+
if (metadata === null) {
|
|
202
|
+
let rows = [];
|
|
203
|
+
try {
|
|
204
|
+
rows = await cap.search({ signalsAny: signalList, ...(opts.searchLimit ? { limit: opts.searchLimit } : {}) });
|
|
205
|
+
}
|
|
206
|
+
catch (e) {
|
|
207
|
+
log?.append({ run_id: runId, action: 'hub_search_miss', signals: signalList, reason: 'search_error', error: errMsg(e) });
|
|
208
|
+
return { action: 'solve-fresh', mode, zeroHubCalls: false, reason: 'search_error' };
|
|
209
|
+
}
|
|
210
|
+
metadata = rows.map(toHubMetadata).filter((m) => m.assetId.length > 0);
|
|
211
|
+
cache.setSearch(key, metadata);
|
|
212
|
+
}
|
|
213
|
+
if (metadata.length === 0) {
|
|
214
|
+
log?.append({ run_id: runId, action: 'hub_search_miss', signals: signalList, reason: 'no_results', via: searchCached ? 'search_cached' : 'search' });
|
|
215
|
+
return { action: 'solve-fresh', mode, zeroHubCalls: searchCached, reason: 'no_results' };
|
|
216
|
+
}
|
|
217
|
+
// ── Phase 2: PURE decision (core — no price) ──
|
|
218
|
+
const ranked = scoreSearchResults(signalList, metadata, opts.now !== undefined ? { now: opts.now } : {});
|
|
219
|
+
const decision = decideReuse(ranked, { threshold });
|
|
220
|
+
if (decision.action === 'solve-fresh' || !decision.candidate) {
|
|
221
|
+
log?.append({ run_id: runId, action: 'hub_search_miss', signals: signalList, reason: 'below_threshold', candidates: metadata.length, threshold });
|
|
222
|
+
return { action: 'solve-fresh', mode, zeroHubCalls: searchCached, reason: 'below_threshold' };
|
|
223
|
+
}
|
|
224
|
+
// ── Phase 3: paid fetch for the ONE winner (payload cache → ZERO hub calls on hit) ──
|
|
225
|
+
const winner = decision.candidate;
|
|
226
|
+
const winnerId = winner.assetId;
|
|
227
|
+
let asset = cache.getPayload(winnerId);
|
|
228
|
+
let payloadCached = asset !== null;
|
|
229
|
+
let creditCost;
|
|
230
|
+
if (asset === null) {
|
|
231
|
+
try {
|
|
232
|
+
if (isAssetByIdFetcher(cap)) {
|
|
233
|
+
asset = await cap.fetchAssetById(winnerId);
|
|
234
|
+
creditCost = asset?.['credit_cost'];
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
const results = await cap.fetch({ signalsAny: signalList, limit: metadata.length });
|
|
238
|
+
// The paid fetch returns full payloads; select the winner by id (content-addressed match).
|
|
239
|
+
asset = results.find((a) => String(a['asset_id'] ?? '') === winnerId)
|
|
240
|
+
?? null;
|
|
241
|
+
// Economic receipt read-through (read-only): surface credit_cost if the hub attached one, never gate.
|
|
242
|
+
const carrier = results.credit_cost
|
|
243
|
+
?? asset?.['credit_cost'];
|
|
244
|
+
creditCost = carrier;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
catch (e) {
|
|
248
|
+
log?.append({ run_id: runId, action: 'hub_search_miss', signals: signalList, reason: 'fetch_error', error: errMsg(e) });
|
|
249
|
+
return { action: 'solve-fresh', mode, zeroHubCalls: false, reason: 'fetch_error' };
|
|
250
|
+
}
|
|
251
|
+
if (assetMatchesId(asset, winnerId))
|
|
252
|
+
cache.setPayload(winnerId, asset);
|
|
253
|
+
else
|
|
254
|
+
asset = null;
|
|
255
|
+
payloadCached = false;
|
|
256
|
+
}
|
|
257
|
+
if (!asset) {
|
|
258
|
+
return { action: 'solve-fresh', mode, zeroHubCalls: searchCached, reason: 'fetch_empty' };
|
|
259
|
+
}
|
|
260
|
+
const zeroHubCalls = searchCached && payloadCached;
|
|
261
|
+
log?.append({
|
|
262
|
+
run_id: runId, action: 'hub_search_hit', asset_id: winnerId, score: winner.score, mode,
|
|
263
|
+
signals: signalList, via: zeroHubCalls ? 'cache' : (searchCached ? 'search_cached' : 'search_then_fetch'),
|
|
264
|
+
});
|
|
265
|
+
// Value-ledger emission (#112): a reuse HIT lands a replayable record so the ledger can derive a
|
|
266
|
+
// source=reuse entry anchored on the real assetId + cycleId. fetchTokens is the actual fetch cost — a
|
|
267
|
+
// payload/cache pull rather than a fresh LLM solve, so ≈0 here. Never lets an emission error break reuse.
|
|
268
|
+
if (opts.onReuseHit) {
|
|
269
|
+
try {
|
|
270
|
+
opts.onReuseHit({ assetId: winnerId, cycleId: opts.cycleId ?? '', signalFingerprint: key, fetchTokens: 0 });
|
|
271
|
+
}
|
|
272
|
+
catch { /* emission must never break the reuse path */ }
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
action: 'fetch',
|
|
276
|
+
candidate: toGeneCandidate(asset),
|
|
277
|
+
asset,
|
|
278
|
+
mode,
|
|
279
|
+
score: winner.score,
|
|
280
|
+
receipt: { fromCache: payloadCached, ...(creditCost !== undefined ? { creditCost } : {}) },
|
|
281
|
+
zeroHubCalls,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
function errMsg(e) {
|
|
285
|
+
return e instanceof Error ? e.message : String(e);
|
|
286
|
+
}
|
|
287
|
+
function isAssetByIdFetcher(value) {
|
|
288
|
+
return typeof value.fetchAssetById === 'function';
|
|
289
|
+
}
|
|
290
|
+
export function assetMatchesId(asset, assetId) {
|
|
291
|
+
return Boolean(asset && asset.asset_id === assetId);
|
|
292
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const PACKAGE = "@evomap/evolver-adapter-public";
|
|
2
|
+
export * from './auth/machineId.js';
|
|
3
|
+
export * from './auth/credentialStore.js';
|
|
4
|
+
export * from './auth/keypair.js';
|
|
5
|
+
export * from './auth/legacyShim.js';
|
|
6
|
+
export * from './auth/oauthDeviceToken.js';
|
|
7
|
+
export * from './auth/oauthHttpTransport.js';
|
|
8
|
+
export * from './hubFetch.js';
|
|
9
|
+
export * from './wireMap.js';
|
|
10
|
+
export * from './hubCapability.js';
|
|
11
|
+
export * from './antiAbuseTelemetry.js';
|
|
12
|
+
export * from './offlinePermit.js';
|
|
13
|
+
export * from './hubReuse.js';
|
|
14
|
+
export * from './atp.js';
|
|
15
|
+
export * from './pricing/modelPrices.js';
|
|
16
|
+
export * from './connect.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const PACKAGE = '@evomap/evolver-adapter-public';
|
|
2
|
+
export * from './auth/machineId.js';
|
|
3
|
+
export * from './auth/credentialStore.js';
|
|
4
|
+
export * from './auth/keypair.js';
|
|
5
|
+
export * from './auth/legacyShim.js';
|
|
6
|
+
export * from './auth/oauthDeviceToken.js';
|
|
7
|
+
export * from './auth/oauthHttpTransport.js';
|
|
8
|
+
export * from './hubFetch.js';
|
|
9
|
+
export * from './wireMap.js';
|
|
10
|
+
export * from './hubCapability.js';
|
|
11
|
+
export * from './antiAbuseTelemetry.js';
|
|
12
|
+
export * from './offlinePermit.js';
|
|
13
|
+
export * from './hubReuse.js';
|
|
14
|
+
export * from './atp.js';
|
|
15
|
+
export * from './pricing/modelPrices.js';
|
|
16
|
+
export * from './connect.js';
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { type algo, type hub } from '@evomap/evolver-core';
|
|
2
|
+
import { type FetchLike } from './hubFetch.js';
|
|
3
|
+
export declare const DEFAULT_MAX_OFFLINE_SOLIDIFIES = 10;
|
|
4
|
+
export declare const DEFAULT_MAX_OFFLINE_DURATION_MS: number;
|
|
5
|
+
export declare const DEFAULT_MAX_CLOCK_DRIFT_MS: number;
|
|
6
|
+
export interface OfflinePermitToken {
|
|
7
|
+
usedCount?: number;
|
|
8
|
+
maxOfflineSolidifies?: number;
|
|
9
|
+
expiresAt?: number;
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
}
|
|
12
|
+
export type OfflinePermitFailure = 'no_offline_token' | 'clock_drift_detected' | 'offline_token_expired' | 'offline_duration_exceeded' | 'offline_quota_exhausted' | 'offline_permit_busy' | 'offline_lock_failed';
|
|
13
|
+
export type OfflinePermitResult = {
|
|
14
|
+
ok: true;
|
|
15
|
+
offline: true;
|
|
16
|
+
remaining: number;
|
|
17
|
+
} | {
|
|
18
|
+
ok: false;
|
|
19
|
+
offline: true;
|
|
20
|
+
error: OfflinePermitFailure;
|
|
21
|
+
detail?: string;
|
|
22
|
+
};
|
|
23
|
+
export interface OfflinePermitStoreOptions {
|
|
24
|
+
dir: string;
|
|
25
|
+
nodeSecret: string | (() => string | null | undefined) | null | undefined;
|
|
26
|
+
now?: () => number;
|
|
27
|
+
maxOfflineSolidifies?: number;
|
|
28
|
+
maxOfflineDurationMs?: number;
|
|
29
|
+
maxClockDriftMs?: number;
|
|
30
|
+
lock?: OfflinePermitLockOptions;
|
|
31
|
+
}
|
|
32
|
+
export interface OfflinePermitLockOptions {
|
|
33
|
+
maxTries?: number;
|
|
34
|
+
waitMs?: number;
|
|
35
|
+
}
|
|
36
|
+
export interface SolidifyPermitCheckOptions {
|
|
37
|
+
hubUrl: string;
|
|
38
|
+
auth: hub.AuthProvider;
|
|
39
|
+
senderId: () => string | undefined;
|
|
40
|
+
dir: string;
|
|
41
|
+
nodeSecret?: string | (() => string | null | undefined) | null | undefined;
|
|
42
|
+
fetchFn?: FetchLike;
|
|
43
|
+
now?: () => number;
|
|
44
|
+
store?: OfflinePermitStore;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* HMAC-backed local offline permit counter.
|
|
48
|
+
*
|
|
49
|
+
* This ports v1 PR #157's concurrency fix into v2: the full
|
|
50
|
+
* load -> cap-check -> increment -> write pipeline is serialized with the
|
|
51
|
+
* shared PID-liveness file lock, so daemon and CLI processes cannot both
|
|
52
|
+
* consume the same local offline quota slot.
|
|
53
|
+
*/
|
|
54
|
+
export declare class OfflinePermitStore {
|
|
55
|
+
private readonly opts;
|
|
56
|
+
private readonly now;
|
|
57
|
+
private readonly maxOfflineSolidifies;
|
|
58
|
+
private readonly maxOfflineDurationMs;
|
|
59
|
+
private readonly maxClockDriftMs;
|
|
60
|
+
constructor(opts: OfflinePermitStoreOptions);
|
|
61
|
+
offlineTokenPath(): string;
|
|
62
|
+
lastVerifyPath(): string;
|
|
63
|
+
lockPath(): string;
|
|
64
|
+
cacheOfflineToken(token: OfflinePermitToken): boolean;
|
|
65
|
+
loadOfflineToken(): OfflinePermitToken | null;
|
|
66
|
+
recordLastOnlineVerify(ts?: number): boolean;
|
|
67
|
+
getLastOnlineVerifyTs(): number;
|
|
68
|
+
consumeOfflinePermit(): OfflinePermitResult;
|
|
69
|
+
private consumeLocked;
|
|
70
|
+
private nodeSecret;
|
|
71
|
+
private errorDetail;
|
|
72
|
+
}
|
|
73
|
+
export declare function createSolidifyPermitCheck(opts: SolidifyPermitCheckOptions): algo.SolidifyPermitGate;
|
|
74
|
+
export declare function hmacSha256(key: string, data: string): string;
|