@evomap/evolver-adapter-public 2.0.0-beta.2 → 2.0.0-beta.22
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.js +2 -1
- package/dist/auth/credentialStore.d.ts +90 -3
- package/dist/auth/credentialStore.js +1096 -10
- package/dist/auth/legacyShim.d.ts +2 -2
- package/dist/auth/legacyShim.js +2 -2
- package/dist/auth/oauthDeviceToken.d.ts +9 -6
- package/dist/auth/oauthDeviceToken.js +71 -18
- package/dist/auth/oauthHttpTransport.d.ts +4 -0
- package/dist/auth/oauthHttpTransport.js +66 -15
- package/dist/auth/windowsPowerShell.d.ts +3 -0
- package/dist/auth/windowsPowerShell.js +91 -0
- package/dist/hubCapability.d.ts +35 -9
- package/dist/hubCapability.js +593 -54
- package/dist/hubFetch.d.ts +47 -14
- package/dist/hubFetch.js +374 -79
- package/dist/hubReuse.d.ts +41 -0
- package/dist/hubReuse.js +328 -33
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/learningPacketFeedback.d.ts +68 -0
- package/dist/learningPacketFeedback.js +104 -0
- package/dist/learningPacketSink.d.ts +48 -0
- package/dist/learningPacketSink.js +194 -0
- package/dist/wireMap.d.ts +3 -1
- package/dist/wireMap.js +29 -3
- package/package.json +9 -2
package/dist/hubReuse.d.ts
CHANGED
|
@@ -5,6 +5,9 @@ type GeneCandidateInput = algo.GeneCandidateInput;
|
|
|
5
5
|
export declare const SEARCH_CACHE_TTL_MS: number;
|
|
6
6
|
export declare const SEARCH_CACHE_MAX = 200;
|
|
7
7
|
export declare const PAYLOAD_CACHE_MAX = 100;
|
|
8
|
+
export declare const SEMANTIC_SEARCH_LIMIT = 10;
|
|
9
|
+
export declare const SEMANTIC_QUERY_MAX_TERMS = 12;
|
|
10
|
+
export declare const SEMANTIC_QUERY_MAX_CHARS = 512;
|
|
8
11
|
/** Default reuse mode (ported from v1): 'reference' injects the asset as a strong hint; 'direct' applies it. */
|
|
9
12
|
export type ReuseMode = 'direct' | 'reference';
|
|
10
13
|
export declare const DEFAULT_REUSE_MODE: ReuseMode;
|
|
@@ -12,8 +15,23 @@ export declare const DEFAULT_REUSE_MODE: ReuseMode;
|
|
|
12
15
|
export declare function getMinReuseScore(env?: NodeJS.ProcessEnv): number;
|
|
13
16
|
/** Reads EVOLVER_REUSE_MODE here (adapter concern). */
|
|
14
17
|
export declare function getReuseMode(env?: NodeJS.ProcessEnv): ReuseMode;
|
|
18
|
+
/** V1-compatible kill-switch. Semantic recall is on unless explicitly disabled. */
|
|
19
|
+
export declare function isSemanticSearchEnabled(env?: NodeJS.ProcessEnv): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Derive a bounded public semantic query from structured signal tags. Error signatures, paths, prose, and other
|
|
22
|
+
* unstructured values are excluded so the vector-search leg cannot become a side channel for local diagnostics.
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildSemanticQuery(signals: readonly string[]): string;
|
|
15
25
|
/** Stable signal fingerprint (ported from v1 _cacheKey: sort + join). */
|
|
16
26
|
export declare function signalFingerprint(signals: readonly string[]): string;
|
|
27
|
+
export declare const TASK_DOMAIN_SIGNAL_PREFIX: "task_domain:";
|
|
28
|
+
/**
|
|
29
|
+
* Resolve the hub-side domain fence from this turn's signals. Exactly one domain is used and only
|
|
30
|
+
* when the turn is unambiguous: with two or more distinct task_domain:* signals the turn spans
|
|
31
|
+
* domains, and scoping recall to either one would hide the other's assets — so we return null and
|
|
32
|
+
* fall back to unscoped recall (today's behaviour).
|
|
33
|
+
*/
|
|
34
|
+
export declare function hubDomainFromSignals(signals: readonly string[]): string | null;
|
|
17
35
|
/**
|
|
18
36
|
* The two-layer reuse cache. Bounded + TTL'd, per-process. A search-cache hit means phase 1 makes ZERO hub
|
|
19
37
|
* calls; a payload-cache hit means phase 3 makes ZERO hub calls. The clock is injected for deterministic tests.
|
|
@@ -32,6 +50,7 @@ export declare class ReuseCache {
|
|
|
32
50
|
setPayload(assetId: string, payload: hub.AssetRecord): void;
|
|
33
51
|
clear(): void;
|
|
34
52
|
}
|
|
53
|
+
export declare function stripHubDeliveryMetadataForIntegrity(rec: hub.AssetRecord): hub.AssetRecord;
|
|
35
54
|
/**
|
|
36
55
|
* Map a hub search row (AssetRecord with arbitrary quality fields) → the core's price-free HubMetadata.
|
|
37
56
|
* Accepts both camelCase and the hub's snake_case (gdi_score / success_rate / reuse_count / ...). Drops any
|
|
@@ -55,6 +74,8 @@ export interface ReuseBeforeSolveOptions {
|
|
|
55
74
|
searchLimit?: number;
|
|
56
75
|
/** Reuse mode label carried into the result (direct/reference). */
|
|
57
76
|
mode?: ReuseMode;
|
|
77
|
+
/** Environment snapshot for adapter-owned reuse settings and the semantic-search kill-switch. */
|
|
78
|
+
env?: NodeJS.ProcessEnv;
|
|
58
79
|
/** Observability sink (asset-call log). Receives structured records; never throws. */
|
|
59
80
|
log?: {
|
|
60
81
|
append(entry: Record<string, unknown>): void;
|
|
@@ -97,6 +118,26 @@ export interface ReuseBeforeSolveResult {
|
|
|
97
118
|
/** Why we didn't reuse, when action === 'solve-fresh' (no_signals / no_results / below_threshold). */
|
|
98
119
|
reason?: string;
|
|
99
120
|
}
|
|
121
|
+
export interface HubMetadataSearchOptions {
|
|
122
|
+
/** Environment snapshot for the semantic-search kill-switch. */
|
|
123
|
+
env?: NodeJS.ProcessEnv;
|
|
124
|
+
/** Cap on the signal-search leg. The semantic leg keeps its own bounded limit. */
|
|
125
|
+
searchLimit?: number;
|
|
126
|
+
}
|
|
127
|
+
export interface HubMetadataSearchResult {
|
|
128
|
+
signals: string[];
|
|
129
|
+
fingerprint: string;
|
|
130
|
+
metadata: hub.HubMetadata[];
|
|
131
|
+
searchCached: boolean;
|
|
132
|
+
/** False when either free search leg failed. Incomplete results must never prove a miss. */
|
|
133
|
+
complete: boolean;
|
|
134
|
+
error?: unknown;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Run the complete free-search phase shared by reuse and economic miss probes. This function never performs the
|
|
138
|
+
* paid fetch. Only complete dual-leg results enter the cache, so a partial outage cannot become a verified miss.
|
|
139
|
+
*/
|
|
140
|
+
export declare function searchHubMetadata(cap: hub.HubCapability, cache: ReuseCache, signals: readonly string[], opts?: HubMetadataSearchOptions): Promise<HubMetadataSearchResult>;
|
|
100
141
|
/**
|
|
101
142
|
* The reuse-before-solve flow. Returns the single winner (already fetched) as a selection candidate, or a
|
|
102
143
|
* solve-fresh verdict. Never throws on a hub error — reuse is an optimization, not a hard dependency:
|
package/dist/hubReuse.js
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
// - search cache: signal-fingerprint → phase-1 metadata (short TTL). Repeat signal set → ZERO hub calls.
|
|
13
13
|
// - payload cache: assetId → phase-3 payload (content-addressed, long/permanent, bounded LRU). A cached
|
|
14
14
|
// payload → ZERO fetch. Both clocks are injected so TTL/eviction is deterministic and testable.
|
|
15
|
-
import {
|
|
15
|
+
import { createHash } from 'node:crypto';
|
|
16
|
+
import { hub, algo, signals as signalNs, wire } from '@evomap/evolver-core';
|
|
16
17
|
const { scoreSearchResults, decideReuse, DEFAULT_MIN_REUSE_SCORE, } = hub;
|
|
17
18
|
const GENE_WIRE_KEYS = new Set([
|
|
18
19
|
'type',
|
|
@@ -31,12 +32,68 @@ const GENE_WIRE_KEYS = new Set([
|
|
|
31
32
|
'routing_hint',
|
|
32
33
|
'tool_policy',
|
|
33
34
|
'generation_meta',
|
|
35
|
+
// K_auto projection-key coordinates + runtime authorship (v2-delta; ride-along until gep-sdk 1.13).
|
|
36
|
+
'model_name',
|
|
37
|
+
'claims',
|
|
38
|
+
'scope',
|
|
39
|
+
'runtime_profile',
|
|
40
|
+
'verifier_profile',
|
|
34
41
|
'asset_id',
|
|
35
42
|
]);
|
|
43
|
+
const HUB_DELIVERY_METADATA_KEYS = new Set([
|
|
44
|
+
'status',
|
|
45
|
+
'trust_state',
|
|
46
|
+
'success_streak',
|
|
47
|
+
'reputation_score',
|
|
48
|
+
'gdi_score',
|
|
49
|
+
'gdi_score_mean',
|
|
50
|
+
'success_rate',
|
|
51
|
+
'reuse_count',
|
|
52
|
+
'ranking_score',
|
|
53
|
+
'credit_cost',
|
|
54
|
+
'source_node_id',
|
|
55
|
+
'fetched_at',
|
|
56
|
+
'receipt',
|
|
57
|
+
'hub_receipt',
|
|
58
|
+
'already_purchased',
|
|
59
|
+
'_semantic_similarity',
|
|
60
|
+
'semantic_similarity',
|
|
61
|
+
'similarity',
|
|
62
|
+
'semanticSimilarity',
|
|
63
|
+
'_search_score',
|
|
64
|
+
'search_score',
|
|
65
|
+
'_match_score',
|
|
66
|
+
'match_score',
|
|
67
|
+
'_retrieval_rank',
|
|
68
|
+
'retrieval_rank',
|
|
69
|
+
'payload_backfill_reason',
|
|
70
|
+
'original_asset_id',
|
|
71
|
+
'asset_type',
|
|
72
|
+
'local_id',
|
|
73
|
+
'source',
|
|
74
|
+
'bundle_id',
|
|
75
|
+
'callable',
|
|
76
|
+
'payload_ready',
|
|
77
|
+
'bundle_capsule',
|
|
78
|
+
'bundle_events',
|
|
79
|
+
]);
|
|
36
80
|
// ── Cache config (ported from v1 hubSearch.js) ───────────────────────────────
|
|
37
81
|
export const SEARCH_CACHE_TTL_MS = 5 * 60 * 1000; // metadata is hot but staleable — short TTL
|
|
38
82
|
export const SEARCH_CACHE_MAX = 200;
|
|
39
83
|
export const PAYLOAD_CACHE_MAX = 100;
|
|
84
|
+
export const SEMANTIC_SEARCH_LIMIT = 10;
|
|
85
|
+
export const SEMANTIC_QUERY_MAX_TERMS = 12;
|
|
86
|
+
export const SEMANTIC_QUERY_MAX_CHARS = 512;
|
|
87
|
+
// Namespace filtering removes obviously private signal classes. The term allowlist below is still mandatory:
|
|
88
|
+
// even a public namespace can contain an arbitrary user-controlled value that must not enter a logged GET URL.
|
|
89
|
+
const PUBLIC_SEMANTIC_NAMESPACES = new Set(['area', 'cap', 'capability_gap', 'risk']);
|
|
90
|
+
const PUBLIC_SEMANTIC_TERMS = new Set([
|
|
91
|
+
'401', '403', '404', '409', '429', '500', '502', '503', '504',
|
|
92
|
+
'auth', 'cache', 'capability_gap', 'code_review', 'concurrency', 'database', 'debugging', 'go',
|
|
93
|
+
'javascript', 'latency', 'memory', 'network', 'performance', 'python',
|
|
94
|
+
'rate_limit', 'reliability', 'retry', 'rust', 'security', 'testing', 'timeout',
|
|
95
|
+
'typescript',
|
|
96
|
+
]);
|
|
40
97
|
export const DEFAULT_REUSE_MODE = 'reference';
|
|
41
98
|
/** Reads EVOLVER_MIN_REUSE_SCORE here (env is an ADAPTER concern — core never reads it). */
|
|
42
99
|
export function getMinReuseScore(env = process.env) {
|
|
@@ -47,10 +104,74 @@ export function getMinReuseScore(env = process.env) {
|
|
|
47
104
|
export function getReuseMode(env = process.env) {
|
|
48
105
|
return String(env['EVOLVER_REUSE_MODE'] ?? DEFAULT_REUSE_MODE).toLowerCase() === 'direct' ? 'direct' : 'reference';
|
|
49
106
|
}
|
|
107
|
+
/** V1-compatible kill-switch. Semantic recall is on unless explicitly disabled. */
|
|
108
|
+
export function isSemanticSearchEnabled(env = process.env) {
|
|
109
|
+
const value = String(env['HUBSEARCH_SEMANTIC'] ?? '').trim().toLowerCase();
|
|
110
|
+
return value !== '0' && value !== 'false';
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Derive a bounded public semantic query from structured signal tags. Error signatures, paths, prose, and other
|
|
114
|
+
* unstructured values are excluded so the vector-search leg cannot become a side channel for local diagnostics.
|
|
115
|
+
*/
|
|
116
|
+
export function buildSemanticQuery(signals) {
|
|
117
|
+
const terms = [];
|
|
118
|
+
const seen = new Set();
|
|
119
|
+
for (const raw of signals) {
|
|
120
|
+
const signal = String(raw).trim();
|
|
121
|
+
const lower = signal.toLowerCase();
|
|
122
|
+
if (!signal || lower.startsWith('errsig:') || lower.startsWith('errsig_norm:') || lower.startsWith('recurring_errsig'))
|
|
123
|
+
continue;
|
|
124
|
+
const colon = signal.indexOf(':');
|
|
125
|
+
if (colon > 0 && !PUBLIC_SEMANTIC_NAMESPACES.has(lower.slice(0, colon)))
|
|
126
|
+
continue;
|
|
127
|
+
const candidate = (colon > 0 && colon < 30 ? signal.slice(colon + 1) : signal).trim().toLowerCase();
|
|
128
|
+
if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(candidate)
|
|
129
|
+
// Signals are user-controlled. Only a fixed public taxonomy may enter the GET query because URLs are
|
|
130
|
+
// commonly retained by Hub and reverse-proxy access logs. Unknown terms remain in the structured POST leg.
|
|
131
|
+
|| !PUBLIC_SEMANTIC_TERMS.has(candidate)
|
|
132
|
+
|| seen.has(candidate))
|
|
133
|
+
continue;
|
|
134
|
+
const nextLength = terms.length === 0 ? candidate.length : terms.join(' ').length + 1 + candidate.length;
|
|
135
|
+
if (nextLength > SEMANTIC_QUERY_MAX_CHARS)
|
|
136
|
+
break;
|
|
137
|
+
seen.add(candidate);
|
|
138
|
+
terms.push(candidate);
|
|
139
|
+
if (terms.length >= SEMANTIC_QUERY_MAX_TERMS)
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
return terms.join(' ');
|
|
143
|
+
}
|
|
50
144
|
/** Stable signal fingerprint (ported from v1 _cacheKey: sort + join). */
|
|
51
145
|
export function signalFingerprint(signals) {
|
|
52
146
|
return [...signals].map((s) => String(s).trim()).filter(Boolean).sort().join('|');
|
|
53
147
|
}
|
|
148
|
+
export const TASK_DOMAIN_SIGNAL_PREFIX = signalNs.TASK_DOMAIN_SIGNAL_PREFIX;
|
|
149
|
+
/**
|
|
150
|
+
* evolver domain slug → hub domain taxonomy (evomap-hub domainDetectionService VALID_DOMAINS).
|
|
151
|
+
* Only mapped slugs may ride the wire: the hub validates against its own taxonomy and silently
|
|
152
|
+
* ignores unknown values (fail-open), so an unmapped slug would just waste the fence. Slugs the
|
|
153
|
+
* hub has no counterpart for (pdf/mail/calendar) intentionally map to nothing.
|
|
154
|
+
*/
|
|
155
|
+
const HUB_DOMAIN_BY_SLUG = {
|
|
156
|
+
coding: 'software_engineering',
|
|
157
|
+
sql: 'software_engineering',
|
|
158
|
+
pptx: 'content_creation',
|
|
159
|
+
docx: 'content_creation',
|
|
160
|
+
xlsx: 'data_analysis',
|
|
161
|
+
marketing: 'marketing',
|
|
162
|
+
};
|
|
163
|
+
/**
|
|
164
|
+
* Resolve the hub-side domain fence from this turn's signals. Exactly one domain is used and only
|
|
165
|
+
* when the turn is unambiguous: with two or more distinct task_domain:* signals the turn spans
|
|
166
|
+
* domains, and scoping recall to either one would hide the other's assets — so we return null and
|
|
167
|
+
* fall back to unscoped recall (today's behaviour).
|
|
168
|
+
*/
|
|
169
|
+
export function hubDomainFromSignals(signals) {
|
|
170
|
+
const resolution = signalNs.resolveTaskDomainSignals(signals);
|
|
171
|
+
return resolution.status === 'resolved'
|
|
172
|
+
? HUB_DOMAIN_BY_SLUG[resolution.slug] ?? null
|
|
173
|
+
: null;
|
|
174
|
+
}
|
|
54
175
|
/**
|
|
55
176
|
* The two-layer reuse cache. Bounded + TTL'd, per-process. A search-cache hit means phase 1 makes ZERO hub
|
|
56
177
|
* calls; a payload-cache hit means phase 3 makes ZERO hub calls. The clock is injected for deterministic tests.
|
|
@@ -76,10 +197,13 @@ export class ReuseCache {
|
|
|
76
197
|
this.search.delete(key);
|
|
77
198
|
return null;
|
|
78
199
|
}
|
|
200
|
+
this.search.delete(key);
|
|
201
|
+
this.search.set(key, e);
|
|
79
202
|
return e.value;
|
|
80
203
|
}
|
|
81
204
|
setSearch(key, value) {
|
|
82
|
-
|
|
205
|
+
const exists = this.search.delete(key);
|
|
206
|
+
if (!exists && this.search.size >= this.searchMax) {
|
|
83
207
|
const oldest = this.search.keys().next().value;
|
|
84
208
|
if (oldest !== undefined)
|
|
85
209
|
this.search.delete(oldest);
|
|
@@ -90,15 +214,19 @@ export class ReuseCache {
|
|
|
90
214
|
const asset = this.payload.get(assetId) ?? null;
|
|
91
215
|
if (!asset)
|
|
92
216
|
return null;
|
|
93
|
-
if (assetMatchesId(asset, assetId))
|
|
217
|
+
if (assetMatchesId(asset, assetId)) {
|
|
218
|
+
this.payload.delete(assetId);
|
|
219
|
+
this.payload.set(assetId, asset);
|
|
94
220
|
return asset;
|
|
221
|
+
}
|
|
95
222
|
this.payload.delete(assetId);
|
|
96
223
|
return null;
|
|
97
224
|
}
|
|
98
225
|
setPayload(assetId, payload) {
|
|
99
226
|
if (!assetMatchesId(payload, assetId))
|
|
100
227
|
return;
|
|
101
|
-
|
|
228
|
+
const exists = this.payload.delete(assetId);
|
|
229
|
+
if (!exists && this.payload.size >= this.payloadMax) {
|
|
102
230
|
const oldest = this.payload.keys().next().value;
|
|
103
231
|
if (oldest !== undefined)
|
|
104
232
|
this.payload.delete(oldest);
|
|
@@ -132,6 +260,20 @@ function stripHubPayloadMetadata(rec) {
|
|
|
132
260
|
}
|
|
133
261
|
return out;
|
|
134
262
|
}
|
|
263
|
+
// Shared content projection for by-id verification and sync quarantine classification.
|
|
264
|
+
export function stripHubDeliveryMetadataForIntegrity(rec) {
|
|
265
|
+
const out = { ...rec };
|
|
266
|
+
for (const key of HUB_DELIVERY_METADATA_KEYS) {
|
|
267
|
+
// Hub ranking streak is metadata for Genes, while Capsule.success_streak is canonical content.
|
|
268
|
+
if (key === 'success_streak' && out['type'] === 'Capsule')
|
|
269
|
+
continue;
|
|
270
|
+
delete out[key];
|
|
271
|
+
}
|
|
272
|
+
// Hub ranking confidence is metadata for Genes, while Capsule.confidence is canonical content.
|
|
273
|
+
if (out['type'] === 'Gene')
|
|
274
|
+
delete out['confidence'];
|
|
275
|
+
return out;
|
|
276
|
+
}
|
|
135
277
|
/**
|
|
136
278
|
* Map a hub search row (AssetRecord with arbitrary quality fields) → the core's price-free HubMetadata.
|
|
137
279
|
* Accepts both camelCase and the hub's snake_case (gdi_score / success_rate / reuse_count / ...). Drops any
|
|
@@ -153,7 +295,9 @@ export function toHubMetadata(rec) {
|
|
|
153
295
|
...(num(r['gdi_score'] ?? r['gdiScore']) !== undefined ? { gdiScore: num(r['gdi_score'] ?? r['gdiScore']) } : {}),
|
|
154
296
|
...(num(r['success_rate'] ?? r['successRate']) !== undefined ? { successRate: num(r['success_rate'] ?? r['successRate']) } : {}),
|
|
155
297
|
...(num(r['reuse_count'] ?? r['reuseCount']) !== undefined ? { reuseCount: num(r['reuse_count'] ?? r['reuseCount']) } : {}),
|
|
156
|
-
...(num(r['similarity'] ?? r['
|
|
298
|
+
...(num(r['similarity'] ?? r['semantic_similarity'] ?? r['_semantic_similarity'] ?? r['semanticSimilarity']) !== undefined
|
|
299
|
+
? { semanticSimilarity: num(r['similarity'] ?? r['semantic_similarity'] ?? r['_semantic_similarity'] ?? r['semanticSimilarity']) }
|
|
300
|
+
: {}),
|
|
157
301
|
...(updatedAt !== undefined ? { updatedAt } : {}),
|
|
158
302
|
};
|
|
159
303
|
}
|
|
@@ -165,6 +309,10 @@ export function toGeneCandidate(rec) {
|
|
|
165
309
|
const signalsMatch = strArr(r['signals_match'] ?? r['signalsMatch']) ?? [];
|
|
166
310
|
const reuseCount = num(r['reuse_count'] ?? r['reuseCount']) ?? 0;
|
|
167
311
|
const generationSource = algo.geneGenerationSource(r, geneId);
|
|
312
|
+
// Project onto the wire allowlist first, then re-run strict K_auto on the same bytes selection
|
|
313
|
+
// will see. Soft-preference must not stamp membership from hub delivery metadata that strip drops.
|
|
314
|
+
const hubAsset = stripHubPayloadMetadata(rec);
|
|
315
|
+
const kautoMember = algo.decideKauto(hubAsset).inKauto;
|
|
168
316
|
return {
|
|
169
317
|
geneId,
|
|
170
318
|
assetId,
|
|
@@ -176,7 +324,70 @@ export function toGeneCandidate(rec) {
|
|
|
176
324
|
...(typeof r['category'] === 'string' ? { category: r['category'] } : {}),
|
|
177
325
|
...(typeof r['summary'] === 'string' ? { summary: r['summary'] } : {}),
|
|
178
326
|
...(generationSource ? { generationSource } : {}),
|
|
179
|
-
|
|
327
|
+
...(kautoMember ? { kautoMember: true } : {}),
|
|
328
|
+
hubAsset,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Run the complete free-search phase shared by reuse and economic miss probes. This function never performs the
|
|
333
|
+
* paid fetch. Only complete dual-leg results enter the cache, so a partial outage cannot become a verified miss.
|
|
334
|
+
*/
|
|
335
|
+
export async function searchHubMetadata(cap, cache, signals, opts = {}) {
|
|
336
|
+
const signalList = signals.map((signal) => String(signal).trim()).filter(Boolean);
|
|
337
|
+
const fingerprint = signalFingerprint(signalList);
|
|
338
|
+
if (signalList.length === 0) {
|
|
339
|
+
return { signals: signalList, fingerprint, metadata: [], searchCached: false, complete: true };
|
|
340
|
+
}
|
|
341
|
+
const env = opts.env ?? process.env;
|
|
342
|
+
const semanticQuery = isSemanticSearchEnabled(env) ? buildSemanticQuery(signalList) : '';
|
|
343
|
+
const semanticActive = semanticQuery.length >= 3;
|
|
344
|
+
const semanticQueryDigest = semanticActive
|
|
345
|
+
? createHash('sha256').update(semanticQuery).digest('hex')
|
|
346
|
+
: undefined;
|
|
347
|
+
// Domain fence: derived from the turn's own task_domain:* signals (never from prose), mapped to
|
|
348
|
+
// the hub taxonomy. Scopes the structured signal leg only — the semantic leg already carries its
|
|
349
|
+
// own allowlisted free-text and stays domain-agnostic as the discovery fallback.
|
|
350
|
+
const hubDomain = hubDomainFromSignals(signals);
|
|
351
|
+
const signalSearchLimit = opts.searchLimit ? opts.searchLimit : undefined;
|
|
352
|
+
const limitKey = signalSearchLimit === undefined ? 'all' : String(signalSearchLimit);
|
|
353
|
+
const domainKey = hubDomain === null ? '' : `:domain:${hubDomain}`;
|
|
354
|
+
const key = semanticQueryDigest
|
|
355
|
+
? `semantic:${fingerprint}:${semanticQueryDigest}:limit:${limitKey}${domainKey}`
|
|
356
|
+
: `signals:${fingerprint}:limit:${limitKey}${domainKey}`;
|
|
357
|
+
const cached = cache.getSearch(key);
|
|
358
|
+
if (cached !== null) {
|
|
359
|
+
return { signals: signalList, fingerprint, metadata: cached, searchCached: true, complete: true };
|
|
360
|
+
}
|
|
361
|
+
// Enter a promise boundary before invoking an injected provider: interface implementations can still throw
|
|
362
|
+
// synchronously even though their declared return type is Promise, and reuse must remain best-effort.
|
|
363
|
+
const signalSearch = Promise.resolve().then(() => cap.search({
|
|
364
|
+
signalsAny: signalList,
|
|
365
|
+
...(hubDomain !== null ? { domain: hubDomain } : {}),
|
|
366
|
+
...(signalSearchLimit ? { limit: signalSearchLimit } : {}),
|
|
367
|
+
}));
|
|
368
|
+
const semanticSearch = semanticActive
|
|
369
|
+
? Promise.resolve().then(() => cap.search({ text: semanticQuery, kind: 'Gene', limit: SEMANTIC_SEARCH_LIMIT }))
|
|
370
|
+
: Promise.resolve([]);
|
|
371
|
+
const [signalResult, semanticResult] = await Promise.allSettled([signalSearch, semanticSearch]);
|
|
372
|
+
const signalRows = signalResult.status === 'fulfilled' ? signalResult.value : [];
|
|
373
|
+
const semanticRows = semanticResult.status === 'fulfilled' ? semanticResult.value : [];
|
|
374
|
+
const failedSearch = signalResult.status === 'rejected'
|
|
375
|
+
? signalResult
|
|
376
|
+
: semanticResult.status === 'rejected'
|
|
377
|
+
? semanticResult
|
|
378
|
+
: undefined;
|
|
379
|
+
const metadata = mergeSearchRows(signalRows, semanticRows)
|
|
380
|
+
.map(toHubMetadata)
|
|
381
|
+
.filter((candidate) => candidate.assetId.length > 0);
|
|
382
|
+
if (!failedSearch)
|
|
383
|
+
cache.setSearch(key, metadata);
|
|
384
|
+
return {
|
|
385
|
+
signals: signalList,
|
|
386
|
+
fingerprint,
|
|
387
|
+
metadata,
|
|
388
|
+
searchCached: false,
|
|
389
|
+
complete: failedSearch === undefined,
|
|
390
|
+
...(failedSearch ? { error: failedSearch.reason } : {}),
|
|
180
391
|
};
|
|
181
392
|
}
|
|
182
393
|
/**
|
|
@@ -189,40 +400,58 @@ export function toGeneCandidate(rec) {
|
|
|
189
400
|
* @param signals the local problem signals.
|
|
190
401
|
*/
|
|
191
402
|
export async function reuseBeforeSolve(cap, cache, signals, opts = {}) {
|
|
192
|
-
const
|
|
193
|
-
const
|
|
403
|
+
const env = opts.env ?? process.env;
|
|
404
|
+
const mode = opts.mode ?? getReuseMode(env);
|
|
405
|
+
const threshold = opts.threshold ?? getMinReuseScore(env);
|
|
194
406
|
const runId = opts.runId ?? null;
|
|
195
407
|
const log = opts.log;
|
|
196
|
-
const
|
|
408
|
+
const searchResult = await searchHubMetadata(cap, cache, signals, {
|
|
409
|
+
env,
|
|
410
|
+
...(opts.searchLimit ? { searchLimit: opts.searchLimit } : {}),
|
|
411
|
+
});
|
|
412
|
+
const { signals: signalList, fingerprint, metadata, searchCached, complete: searchComplete, error: searchFailure, } = searchResult;
|
|
197
413
|
if (signalList.length === 0) {
|
|
198
414
|
return { action: 'solve-fresh', mode, zeroHubCalls: true, reason: 'no_signals' };
|
|
199
415
|
}
|
|
200
416
|
// ── Phase 1: free search (signal fingerprint cache → ZERO hub calls on hit) ──
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
return { action: 'solve-fresh', mode, zeroHubCalls: false, reason: 'search_error' };
|
|
212
|
-
}
|
|
213
|
-
metadata = rows.map(toHubMetadata).filter((m) => m.assetId.length > 0);
|
|
214
|
-
cache.setSearch(key, metadata);
|
|
417
|
+
const searchIncomplete = !searchComplete;
|
|
418
|
+
if (searchIncomplete && metadata.length === 0) {
|
|
419
|
+
log?.append({
|
|
420
|
+
run_id: runId,
|
|
421
|
+
action: 'hub_search_miss',
|
|
422
|
+
signals: signalList,
|
|
423
|
+
reason: 'search_error',
|
|
424
|
+
error: errMsg(searchFailure),
|
|
425
|
+
});
|
|
426
|
+
return { action: 'solve-fresh', mode, zeroHubCalls: false, reason: 'search_error' };
|
|
215
427
|
}
|
|
216
428
|
if (metadata.length === 0) {
|
|
217
|
-
|
|
218
|
-
|
|
429
|
+
const reason = searchIncomplete ? 'search_error' : 'no_results';
|
|
430
|
+
log?.append({
|
|
431
|
+
run_id: runId,
|
|
432
|
+
action: 'hub_search_miss',
|
|
433
|
+
signals: signalList,
|
|
434
|
+
reason,
|
|
435
|
+
via: searchCached ? 'search_cached' : 'search',
|
|
436
|
+
...(searchIncomplete ? { error: errMsg(searchFailure) } : {}),
|
|
437
|
+
});
|
|
438
|
+
return { action: 'solve-fresh', mode, zeroHubCalls: searchCached, reason };
|
|
219
439
|
}
|
|
220
440
|
// ── Phase 2: PURE decision (core — no price) ──
|
|
221
441
|
const ranked = scoreSearchResults(signalList, metadata, opts.now !== undefined ? { now: opts.now } : {});
|
|
222
442
|
const decision = decideReuse(ranked, { threshold });
|
|
223
443
|
if (decision.action === 'solve-fresh' || !decision.candidate) {
|
|
224
|
-
|
|
225
|
-
|
|
444
|
+
const reason = searchIncomplete ? 'search_error' : 'below_threshold';
|
|
445
|
+
log?.append({
|
|
446
|
+
run_id: runId,
|
|
447
|
+
action: 'hub_search_miss',
|
|
448
|
+
signals: signalList,
|
|
449
|
+
reason,
|
|
450
|
+
candidates: metadata.length,
|
|
451
|
+
threshold,
|
|
452
|
+
...(searchIncomplete ? { error: errMsg(searchFailure) } : {}),
|
|
453
|
+
});
|
|
454
|
+
return { action: 'solve-fresh', mode, zeroHubCalls: searchCached, reason };
|
|
226
455
|
}
|
|
227
456
|
// ── Phase 3: paid fetch for the ONE winner (payload cache → ZERO hub calls on hit) ──
|
|
228
457
|
const winner = decision.candidate;
|
|
@@ -233,14 +462,14 @@ export async function reuseBeforeSolve(cap, cache, signals, opts = {}) {
|
|
|
233
462
|
if (asset === null) {
|
|
234
463
|
try {
|
|
235
464
|
if (isAssetByIdFetcher(cap)) {
|
|
236
|
-
asset = await cap.fetchAssetById(winnerId);
|
|
465
|
+
asset = normalizeMatchedAssetId(await cap.fetchAssetById(winnerId), winnerId);
|
|
237
466
|
creditCost = asset?.['credit_cost'];
|
|
238
467
|
}
|
|
239
468
|
else {
|
|
240
469
|
const results = await cap.fetch({ signalsAny: signalList, limit: metadata.length });
|
|
241
470
|
// The paid fetch returns full payloads; select the winner by id (content-addressed match).
|
|
242
|
-
|
|
243
|
-
|
|
471
|
+
const fetched = results.find((candidate) => assetMatchesId(candidate, winnerId));
|
|
472
|
+
asset = normalizeMatchedAssetId(fetched, winnerId);
|
|
244
473
|
// Economic receipt read-through (read-only): surface credit_cost if the hub attached one, never gate.
|
|
245
474
|
const carrier = results.credit_cost
|
|
246
475
|
?? asset?.['credit_cost'];
|
|
@@ -258,7 +487,7 @@ export async function reuseBeforeSolve(cap, cache, signals, opts = {}) {
|
|
|
258
487
|
payloadCached = false;
|
|
259
488
|
}
|
|
260
489
|
if (!asset) {
|
|
261
|
-
return { action: 'solve-fresh', mode, zeroHubCalls:
|
|
490
|
+
return { action: 'solve-fresh', mode, zeroHubCalls: false, reason: 'fetch_empty' };
|
|
262
491
|
}
|
|
263
492
|
const zeroHubCalls = searchCached && payloadCached;
|
|
264
493
|
log?.append({
|
|
@@ -270,7 +499,7 @@ export async function reuseBeforeSolve(cap, cache, signals, opts = {}) {
|
|
|
270
499
|
// payload/cache pull rather than a fresh LLM solve, so ≈0 here. Never lets an emission error break reuse.
|
|
271
500
|
if (opts.onReuseHit) {
|
|
272
501
|
try {
|
|
273
|
-
opts.onReuseHit({ assetId: winnerId, cycleId: opts.cycleId ?? '', signalFingerprint:
|
|
502
|
+
opts.onReuseHit({ assetId: winnerId, cycleId: opts.cycleId ?? '', signalFingerprint: fingerprint, fetchTokens: 0 });
|
|
274
503
|
}
|
|
275
504
|
catch { /* emission must never break the reuse path */ }
|
|
276
505
|
}
|
|
@@ -284,12 +513,78 @@ export async function reuseBeforeSolve(cap, cache, signals, opts = {}) {
|
|
|
284
513
|
zeroHubCalls,
|
|
285
514
|
};
|
|
286
515
|
}
|
|
516
|
+
function mergeSearchRows(signalRows, semanticRows) {
|
|
517
|
+
const merged = [];
|
|
518
|
+
const indexById = new Map();
|
|
519
|
+
for (const row of signalRows) {
|
|
520
|
+
const record = row;
|
|
521
|
+
const assetId = String(record['asset_id'] ?? record['assetId'] ?? '');
|
|
522
|
+
if (!assetId || indexById.has(assetId))
|
|
523
|
+
continue;
|
|
524
|
+
indexById.set(assetId, merged.length);
|
|
525
|
+
merged.push(row);
|
|
526
|
+
}
|
|
527
|
+
for (const row of semanticRows) {
|
|
528
|
+
const record = row;
|
|
529
|
+
const assetId = String(record['asset_id'] ?? record['assetId'] ?? '');
|
|
530
|
+
if (!assetId)
|
|
531
|
+
continue;
|
|
532
|
+
const existingIndex = indexById.get(assetId);
|
|
533
|
+
if (existingIndex === undefined) {
|
|
534
|
+
indexById.set(assetId, merged.length);
|
|
535
|
+
merged.push(row);
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
const similarity = num(record['similarity'] ?? record['semantic_similarity'] ?? record['_semantic_similarity'] ?? record['semanticSimilarity']);
|
|
539
|
+
if (similarity !== undefined) {
|
|
540
|
+
merged[existingIndex] = { ...merged[existingIndex], similarity };
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return merged;
|
|
544
|
+
}
|
|
287
545
|
function errMsg(e) {
|
|
288
546
|
return e instanceof Error ? e.message : String(e);
|
|
289
547
|
}
|
|
290
548
|
function isAssetByIdFetcher(value) {
|
|
291
549
|
return typeof value.fetchAssetById === 'function';
|
|
292
550
|
}
|
|
551
|
+
function normalizeMatchedAssetId(asset, assetId) {
|
|
552
|
+
if (!assetMatchesId(asset, assetId))
|
|
553
|
+
return null;
|
|
554
|
+
const record = asset;
|
|
555
|
+
if (typeof record['asset_id'] === 'string')
|
|
556
|
+
return asset;
|
|
557
|
+
const normalized = { ...record, asset_id: assetId };
|
|
558
|
+
if (record['assetId'] === assetId)
|
|
559
|
+
delete normalized['assetId'];
|
|
560
|
+
if (record['id'] === assetId)
|
|
561
|
+
delete normalized['id'];
|
|
562
|
+
return normalized;
|
|
563
|
+
}
|
|
293
564
|
export function assetMatchesId(asset, assetId) {
|
|
294
|
-
|
|
565
|
+
if (!asset)
|
|
566
|
+
return false;
|
|
567
|
+
const record = asset;
|
|
568
|
+
const canonicalAssetId = typeof record['asset_id'] === 'string' ? record['asset_id'] : undefined;
|
|
569
|
+
if (canonicalAssetId !== undefined && canonicalAssetId !== assetId)
|
|
570
|
+
return false;
|
|
571
|
+
const hasCamelAlias = typeof record['assetId'] === 'string';
|
|
572
|
+
if (canonicalAssetId === undefined && hasCamelAlias && record['assetId'] !== assetId)
|
|
573
|
+
return false;
|
|
574
|
+
if (canonicalAssetId === undefined && !hasCamelAlias && record['id'] !== assetId)
|
|
575
|
+
return false;
|
|
576
|
+
const content = { ...record };
|
|
577
|
+
if (record['assetId'] === assetId)
|
|
578
|
+
delete content['assetId'];
|
|
579
|
+
if (record['id'] === assetId)
|
|
580
|
+
delete content['id'];
|
|
581
|
+
try {
|
|
582
|
+
if (assetId.startsWith('sha256:') && !/^sha256:[0-9a-f]{64}$/.test(assetId))
|
|
583
|
+
return false;
|
|
584
|
+
const contentMatches = wire.computeAssetId(stripHubDeliveryMetadataForIntegrity(content)) === assetId;
|
|
585
|
+
return assetId.startsWith('sha256:') ? contentMatches : canonicalAssetId !== undefined || contentMatches;
|
|
586
|
+
}
|
|
587
|
+
catch {
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
295
590
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export * from './antiAbuseTelemetry.js';
|
|
|
14
14
|
export * from './offlinePermit.js';
|
|
15
15
|
export * from './hubReuse.js';
|
|
16
16
|
export * from './hubUrl.js';
|
|
17
|
+
export * from './learningPacketSink.js';
|
|
18
|
+
export * from './learningPacketFeedback.js';
|
|
17
19
|
export * from './atp.js';
|
|
18
20
|
export * from './pricing/modelPrices.js';
|
|
19
21
|
export * from './connect.js';
|
package/dist/index.js
CHANGED
|
@@ -14,6 +14,8 @@ export * from './antiAbuseTelemetry.js';
|
|
|
14
14
|
export * from './offlinePermit.js';
|
|
15
15
|
export * from './hubReuse.js';
|
|
16
16
|
export * from './hubUrl.js';
|
|
17
|
+
export * from './learningPacketSink.js';
|
|
18
|
+
export * from './learningPacketFeedback.js';
|
|
17
19
|
export * from './atp.js';
|
|
18
20
|
export * from './pricing/modelPrices.js';
|
|
19
21
|
export * from './connect.js';
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { hub } from '@evomap/evolver-core';
|
|
2
|
+
import { type FetchLike } from './hubFetch.js';
|
|
3
|
+
/** Hub appendLearningFeedbackSchema closed enums (evomap-hub src/schemas/learningOps.js). */
|
|
4
|
+
export declare const LEARNING_FEEDBACK_TYPES: readonly ["outcome", "rating", "correction", "governance", "note"];
|
|
5
|
+
export type LearningFeedbackType = (typeof LEARNING_FEEDBACK_TYPES)[number];
|
|
6
|
+
export declare const LEARNING_FEEDBACK_DECISIONS: readonly ["accepted", "rejected", "needs_redaction", "not_training_eligible", "training_candidate", "note"];
|
|
7
|
+
export type LearningFeedbackDecision = (typeof LEARNING_FEEDBACK_DECISIONS)[number];
|
|
8
|
+
export interface LearningPacketFeedbackInput {
|
|
9
|
+
/** Default hub-side: 'outcome'. */
|
|
10
|
+
feedbackType?: LearningFeedbackType;
|
|
11
|
+
decision: LearningFeedbackDecision;
|
|
12
|
+
/** 0..1 (hub-validated). */
|
|
13
|
+
rating?: number;
|
|
14
|
+
scores?: Record<string, unknown>;
|
|
15
|
+
rationale?: string;
|
|
16
|
+
/** Hub VERIFIERS enum member (e.g. 'automated_test', 'human'). */
|
|
17
|
+
verifier?: string;
|
|
18
|
+
/** Hub FAILURE_CATEGORIES enum member. */
|
|
19
|
+
failureCategory?: string;
|
|
20
|
+
/** Anchor the feedback to one trace event instead of the whole packet. */
|
|
21
|
+
traceEventId?: string;
|
|
22
|
+
actorNodeId?: string;
|
|
23
|
+
}
|
|
24
|
+
export type LearningFeedbackResult = {
|
|
25
|
+
ok: true;
|
|
26
|
+
feedbackId?: string;
|
|
27
|
+
} | {
|
|
28
|
+
ok: false;
|
|
29
|
+
reason: string;
|
|
30
|
+
};
|
|
31
|
+
/** Server-managed governance/eligibility state read back from GET /api/learning-packets/:id. */
|
|
32
|
+
export interface LearningPacketStatus {
|
|
33
|
+
id: string;
|
|
34
|
+
status?: string;
|
|
35
|
+
outcomeStatus?: string | null;
|
|
36
|
+
verifier?: string | null;
|
|
37
|
+
/** LearningOpsTrainingEligibility mirror: pending/eligible/ineligible/revoked/expired. */
|
|
38
|
+
trainingEligibilityStatus?: string | null;
|
|
39
|
+
/** pending/approved/blocked/purge_requested. */
|
|
40
|
+
governanceStatus?: string | null;
|
|
41
|
+
trainingEligible?: boolean;
|
|
42
|
+
consentStatus?: string | null;
|
|
43
|
+
redactionStatus?: string | null;
|
|
44
|
+
retentionPolicy?: string | null;
|
|
45
|
+
}
|
|
46
|
+
export type LearningPacketReadResult = {
|
|
47
|
+
ok: true;
|
|
48
|
+
packet: LearningPacketStatus;
|
|
49
|
+
} | {
|
|
50
|
+
ok: false;
|
|
51
|
+
reason: string;
|
|
52
|
+
};
|
|
53
|
+
export interface HubLearningPacketFeedbackClientOptions {
|
|
54
|
+
baseUrl: string;
|
|
55
|
+
auth: hub.AuthProvider;
|
|
56
|
+
fetchFn: FetchLike;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Feedback append + packet governance read-back against the hub Learning Ops API. Best-effort by the
|
|
60
|
+
* same contract as HubLearningPacketSink: this is observability/ops tooling, so every failure —
|
|
61
|
+
* network, auth, 4xx/5xx, unparseable body — returns { ok:false, reason } and never throws.
|
|
62
|
+
*/
|
|
63
|
+
export declare class HubLearningPacketFeedbackClient {
|
|
64
|
+
private readonly opts;
|
|
65
|
+
constructor(opts: HubLearningPacketFeedbackClientOptions);
|
|
66
|
+
submitFeedback(packetId: string, feedback: LearningPacketFeedbackInput): Promise<LearningFeedbackResult>;
|
|
67
|
+
getPacket(packetId: string): Promise<LearningPacketReadResult>;
|
|
68
|
+
}
|