@asm-agent/retrieval 0.8.2
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/README.md +10 -0
- package/dist/causal-head.d.ts +29 -0
- package/dist/causal-head.d.ts.map +1 -0
- package/dist/causal-head.js +98 -0
- package/dist/causal-head.js.map +1 -0
- package/dist/checkpoint.d.ts +8 -0
- package/dist/checkpoint.d.ts.map +1 -0
- package/dist/checkpoint.js +54 -0
- package/dist/checkpoint.js.map +1 -0
- package/dist/dual-asm.d.ts +27 -0
- package/dist/dual-asm.d.ts.map +1 -0
- package/dist/dual-asm.js +91 -0
- package/dist/dual-asm.js.map +1 -0
- package/dist/evaluation.d.ts +61 -0
- package/dist/evaluation.d.ts.map +1 -0
- package/dist/evaluation.js +119 -0
- package/dist/evaluation.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/math.d.ts +6 -0
- package/dist/math.d.ts.map +1 -0
- package/dist/math.js +32 -0
- package/dist/math.js.map +1 -0
- package/dist/repository.d.ts +45 -0
- package/dist/repository.d.ts.map +1 -0
- package/dist/repository.js +144 -0
- package/dist/repository.js.map +1 -0
- package/dist/runtime.d.ts +25 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +62 -0
- package/dist/runtime.js.map +1 -0
- package/dist/state.d.ts +15 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +40 -0
- package/dist/state.js.map +1 -0
- package/dist/types.d.ts +32 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/fixtures/benchmark.json +22 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# `@asm-agent/retrieval`
|
|
2
|
+
|
|
3
|
+
Causal Head and Dual-ASM retrieval backends run behind the versioned retrieval
|
|
4
|
+
contract. New backends begin in shadow mode: callers receive the frozen
|
|
5
|
+
lexical/vector baseline result while candidate output and latency are observed.
|
|
6
|
+
|
|
7
|
+
Checkpoints are data-only JSON envelopes validated against an inference
|
|
8
|
+
manifest and SHA-256 digest. Executable pickle/torch deserialization is not
|
|
9
|
+
supported. Promotion requires a persisted passing evaluation; rollback is a
|
|
10
|
+
transactional configuration change.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { RankedItem, RetrievalBackend, RetrievalItem, RetrievalQuery } from "./types.js";
|
|
2
|
+
export interface CausalHeadState {
|
|
3
|
+
sequence: number;
|
|
4
|
+
slots: Array<{
|
|
5
|
+
vector: number[];
|
|
6
|
+
strength: number;
|
|
7
|
+
lastSequence: number;
|
|
8
|
+
}>;
|
|
9
|
+
}
|
|
10
|
+
export interface CausalHeadParameters {
|
|
11
|
+
dimensions: number;
|
|
12
|
+
capacity: number;
|
|
13
|
+
queryWeight: number;
|
|
14
|
+
stateWeight: number;
|
|
15
|
+
writeRate: number;
|
|
16
|
+
decay: number;
|
|
17
|
+
}
|
|
18
|
+
export declare class CausalHeadBackend implements RetrievalBackend<CausalHeadState> {
|
|
19
|
+
readonly parameters: CausalHeadParameters;
|
|
20
|
+
readonly name: "causal-head-v1";
|
|
21
|
+
readonly dimensions: number;
|
|
22
|
+
constructor(parameters: CausalHeadParameters);
|
|
23
|
+
emptyState(): CausalHeadState;
|
|
24
|
+
retrieve(query: RetrievalQuery, items: readonly RetrievalItem[], state: CausalHeadState): RankedItem[];
|
|
25
|
+
update(state: CausalHeadState, selected: readonly RetrievalItem[], feedback?: string): CausalHeadState;
|
|
26
|
+
serializeState(state: CausalHeadState): Record<string, unknown>;
|
|
27
|
+
deserializeState(value: unknown): CausalHeadState;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=causal-head.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"causal-head.d.ts","sourceRoot":"","sources":["../src/causal-head.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE9F,MAAM,WAAW,eAAe;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,EAAE,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC3E;AAED,MAAM,WAAW,oBAAoB;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACd;AAED,qBAAa,iBAAkB,YAAW,gBAAgB,CAAC,eAAe,CAAC;IAI9D,QAAQ,CAAC,UAAU,EAAE,oBAAoB;IAHrD,QAAQ,CAAC,IAAI,mBAA6B;IAC1C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAE5B,YAAqB,UAAU,EAAE,oBAAoB,EASpD;IAED,UAAU,IAAI,eAAe,CAE5B;IAED,QAAQ,CAAC,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE,SAAS,aAAa,EAAE,EAAE,KAAK,EAAE,eAAe,GAAG,UAAU,EAAE,CAcrG;IAED,MAAM,CAAC,KAAK,EAAE,eAAe,EAAE,QAAQ,EAAE,SAAS,aAAa,EAAE,EAAE,QAAQ,SAAa,GAAG,eAAe,CA4BzG;IAED,cAAc,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAE9D;IAED,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,CAMhD;CACD","sourcesContent":["import { assertVector, average, blend, clamp01, cosine } from \"./math.js\";\nimport type { RankedItem, RetrievalBackend, RetrievalItem, RetrievalQuery } from \"./types.js\";\n\nexport interface CausalHeadState {\n\tsequence: number;\n\tslots: Array<{ vector: number[]; strength: number; lastSequence: number }>;\n}\n\nexport interface CausalHeadParameters {\n\tdimensions: number;\n\tcapacity: number;\n\tqueryWeight: number;\n\tstateWeight: number;\n\twriteRate: number;\n\tdecay: number;\n}\n\nexport class CausalHeadBackend implements RetrievalBackend<CausalHeadState> {\n\treadonly name = \"causal-head-v1\" as const;\n\treadonly dimensions: number;\n\n\tconstructor(readonly parameters: CausalHeadParameters) {\n\t\tthis.dimensions = parameters.dimensions;\n\t\tif (!Number.isInteger(parameters.capacity) || parameters.capacity < 1)\n\t\t\tthrow new Error(\"Causal Head capacity must be positive\");\n\t\tfor (const [name, value] of Object.entries(parameters)) {\n\t\t\tif (name !== \"dimensions\" && name !== \"capacity\" && (!Number.isFinite(value) || value < 0 || value > 1)) {\n\t\t\t\tthrow new Error(`Invalid Causal Head parameter: ${name}`);\n\t\t\t}\n\t\t}\n\t}\n\n\temptyState(): CausalHeadState {\n\t\treturn { sequence: 0, slots: [] };\n\t}\n\n\tretrieve(query: RetrievalQuery, items: readonly RetrievalItem[], state: CausalHeadState): RankedItem[] {\n\t\tassertVector(query.embedding, this.dimensions, \"query embedding\");\n\t\tconst stateVector = weightedStateVector(state, this.dimensions);\n\t\treturn eligibleItems(query, items, this.dimensions)\n\t\t\t.map((item) => ({\n\t\t\t\tid: item.id,\n\t\t\t\tscore: clamp01(\n\t\t\t\t\tcosine(query.embedding, item.embedding) * this.parameters.queryWeight +\n\t\t\t\t\t\tcosine(stateVector, item.embedding) * this.parameters.stateWeight,\n\t\t\t\t),\n\t\t\t\treasonCodes: [\"query_similarity\", \"causal_state_similarity\"],\n\t\t\t}))\n\t\t\t.sort((left, right) => right.score - left.score || left.id.localeCompare(right.id))\n\t\t\t.slice(0, query.limit);\n\t}\n\n\tupdate(state: CausalHeadState, selected: readonly RetrievalItem[], feedback = \"positive\"): CausalHeadState {\n\t\tconst sequence = state.sequence + 1;\n\t\tconst slots = state.slots.map((slot) => ({\n\t\t\t...slot,\n\t\t\tvector: [...slot.vector],\n\t\t\tstrength: slot.strength * this.parameters.decay,\n\t\t}));\n\t\tif (selected.length > 0) {\n\t\t\tfor (const item of selected) assertVector(item.embedding, this.dimensions, \"selected embedding\");\n\t\t\tconst candidate = average(\n\t\t\t\tselected.map((item) => item.embedding),\n\t\t\t\tthis.dimensions,\n\t\t\t);\n\t\t\tconst nearest = slots\n\t\t\t\t.map((slot, index) => ({ index, score: cosine(slot.vector, candidate) }))\n\t\t\t\t.sort((left, right) => right.score - left.score)[0];\n\t\t\tconst strengthDelta = feedback === \"positive\" ? this.parameters.writeRate : -this.parameters.writeRate;\n\t\t\tif (nearest && nearest.score >= 0.75) {\n\t\t\t\tconst slot = slots[nearest.index]!;\n\t\t\t\tslot.vector = blend(slot.vector, candidate, this.parameters.writeRate);\n\t\t\t\tslot.strength = clamp01(slot.strength + strengthDelta);\n\t\t\t\tslot.lastSequence = sequence;\n\t\t\t} else if (feedback === \"positive\") {\n\t\t\t\tslots.push({ vector: candidate, strength: this.parameters.writeRate, lastSequence: sequence });\n\t\t\t}\n\t\t}\n\t\tslots.sort((left, right) => right.strength - left.strength || right.lastSequence - left.lastSequence);\n\t\treturn { sequence, slots: slots.filter((slot) => slot.strength > 0.001).slice(0, this.parameters.capacity) };\n\t}\n\n\tserializeState(state: CausalHeadState): Record<string, unknown> {\n\t\treturn { sequence: state.sequence, slots: state.slots.map((slot) => ({ ...slot, vector: [...slot.vector] })) };\n\t}\n\n\tdeserializeState(value: unknown): CausalHeadState {\n\t\tif (!isCausalState(value)) throw new Error(\"Invalid Causal Head state snapshot\");\n\t\tfor (const slot of value.slots) assertVector(slot.vector, this.dimensions, \"state slot\");\n\t\tif (value.slots.length > this.parameters.capacity)\n\t\t\tthrow new Error(\"Causal Head state exceeds configured capacity\");\n\t\treturn structuredClone(value);\n\t}\n}\n\nfunction weightedStateVector(state: CausalHeadState, dimensions: number): number[] {\n\tconst total = state.slots.reduce((sum, slot) => sum + slot.strength, 0);\n\tif (total === 0) return Array.from({ length: dimensions }, () => 0);\n\treturn Array.from(\n\t\t{ length: dimensions },\n\t\t(_, index) => state.slots.reduce((sum, slot) => sum + (slot.vector[index] ?? 0) * slot.strength, 0) / total,\n\t);\n}\n\nfunction eligibleItems(query: RetrievalQuery, items: readonly RetrievalItem[], dimensions: number): RetrievalItem[] {\n\treturn items.filter((item) => {\n\t\tassertVector(item.embedding, dimensions, \"item embedding\");\n\t\treturn item.ownerId === query.ownerId && item.projectId === query.projectId && !item.revoked;\n\t});\n}\n\nfunction isCausalState(value: unknown): value is CausalHeadState {\n\tif (!value || typeof value !== \"object\") return false;\n\tconst state = value as Partial<CausalHeadState>;\n\treturn (\n\t\tNumber.isInteger(state.sequence) &&\n\t\t(state.sequence ?? -1) >= 0 &&\n\t\tArray.isArray(state.slots) &&\n\t\tstate.slots.every(\n\t\t\t(slot) =>\n\t\t\t\tArray.isArray(slot.vector) &&\n\t\t\t\ttypeof slot.strength === \"number\" &&\n\t\t\t\tNumber.isFinite(slot.strength) &&\n\t\t\t\tNumber.isInteger(slot.lastSequence),\n\t\t)\n\t);\n}\n"]}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { assertVector, average, blend, clamp01, cosine } from "./math.js";
|
|
2
|
+
export class CausalHeadBackend {
|
|
3
|
+
parameters;
|
|
4
|
+
name = "causal-head-v1";
|
|
5
|
+
dimensions;
|
|
6
|
+
constructor(parameters) {
|
|
7
|
+
this.parameters = parameters;
|
|
8
|
+
this.dimensions = parameters.dimensions;
|
|
9
|
+
if (!Number.isInteger(parameters.capacity) || parameters.capacity < 1)
|
|
10
|
+
throw new Error("Causal Head capacity must be positive");
|
|
11
|
+
for (const [name, value] of Object.entries(parameters)) {
|
|
12
|
+
if (name !== "dimensions" && name !== "capacity" && (!Number.isFinite(value) || value < 0 || value > 1)) {
|
|
13
|
+
throw new Error(`Invalid Causal Head parameter: ${name}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
emptyState() {
|
|
18
|
+
return { sequence: 0, slots: [] };
|
|
19
|
+
}
|
|
20
|
+
retrieve(query, items, state) {
|
|
21
|
+
assertVector(query.embedding, this.dimensions, "query embedding");
|
|
22
|
+
const stateVector = weightedStateVector(state, this.dimensions);
|
|
23
|
+
return eligibleItems(query, items, this.dimensions)
|
|
24
|
+
.map((item) => ({
|
|
25
|
+
id: item.id,
|
|
26
|
+
score: clamp01(cosine(query.embedding, item.embedding) * this.parameters.queryWeight +
|
|
27
|
+
cosine(stateVector, item.embedding) * this.parameters.stateWeight),
|
|
28
|
+
reasonCodes: ["query_similarity", "causal_state_similarity"],
|
|
29
|
+
}))
|
|
30
|
+
.sort((left, right) => right.score - left.score || left.id.localeCompare(right.id))
|
|
31
|
+
.slice(0, query.limit);
|
|
32
|
+
}
|
|
33
|
+
update(state, selected, feedback = "positive") {
|
|
34
|
+
const sequence = state.sequence + 1;
|
|
35
|
+
const slots = state.slots.map((slot) => ({
|
|
36
|
+
...slot,
|
|
37
|
+
vector: [...slot.vector],
|
|
38
|
+
strength: slot.strength * this.parameters.decay,
|
|
39
|
+
}));
|
|
40
|
+
if (selected.length > 0) {
|
|
41
|
+
for (const item of selected)
|
|
42
|
+
assertVector(item.embedding, this.dimensions, "selected embedding");
|
|
43
|
+
const candidate = average(selected.map((item) => item.embedding), this.dimensions);
|
|
44
|
+
const nearest = slots
|
|
45
|
+
.map((slot, index) => ({ index, score: cosine(slot.vector, candidate) }))
|
|
46
|
+
.sort((left, right) => right.score - left.score)[0];
|
|
47
|
+
const strengthDelta = feedback === "positive" ? this.parameters.writeRate : -this.parameters.writeRate;
|
|
48
|
+
if (nearest && nearest.score >= 0.75) {
|
|
49
|
+
const slot = slots[nearest.index];
|
|
50
|
+
slot.vector = blend(slot.vector, candidate, this.parameters.writeRate);
|
|
51
|
+
slot.strength = clamp01(slot.strength + strengthDelta);
|
|
52
|
+
slot.lastSequence = sequence;
|
|
53
|
+
}
|
|
54
|
+
else if (feedback === "positive") {
|
|
55
|
+
slots.push({ vector: candidate, strength: this.parameters.writeRate, lastSequence: sequence });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
slots.sort((left, right) => right.strength - left.strength || right.lastSequence - left.lastSequence);
|
|
59
|
+
return { sequence, slots: slots.filter((slot) => slot.strength > 0.001).slice(0, this.parameters.capacity) };
|
|
60
|
+
}
|
|
61
|
+
serializeState(state) {
|
|
62
|
+
return { sequence: state.sequence, slots: state.slots.map((slot) => ({ ...slot, vector: [...slot.vector] })) };
|
|
63
|
+
}
|
|
64
|
+
deserializeState(value) {
|
|
65
|
+
if (!isCausalState(value))
|
|
66
|
+
throw new Error("Invalid Causal Head state snapshot");
|
|
67
|
+
for (const slot of value.slots)
|
|
68
|
+
assertVector(slot.vector, this.dimensions, "state slot");
|
|
69
|
+
if (value.slots.length > this.parameters.capacity)
|
|
70
|
+
throw new Error("Causal Head state exceeds configured capacity");
|
|
71
|
+
return structuredClone(value);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function weightedStateVector(state, dimensions) {
|
|
75
|
+
const total = state.slots.reduce((sum, slot) => sum + slot.strength, 0);
|
|
76
|
+
if (total === 0)
|
|
77
|
+
return Array.from({ length: dimensions }, () => 0);
|
|
78
|
+
return Array.from({ length: dimensions }, (_, index) => state.slots.reduce((sum, slot) => sum + (slot.vector[index] ?? 0) * slot.strength, 0) / total);
|
|
79
|
+
}
|
|
80
|
+
function eligibleItems(query, items, dimensions) {
|
|
81
|
+
return items.filter((item) => {
|
|
82
|
+
assertVector(item.embedding, dimensions, "item embedding");
|
|
83
|
+
return item.ownerId === query.ownerId && item.projectId === query.projectId && !item.revoked;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
function isCausalState(value) {
|
|
87
|
+
if (!value || typeof value !== "object")
|
|
88
|
+
return false;
|
|
89
|
+
const state = value;
|
|
90
|
+
return (Number.isInteger(state.sequence) &&
|
|
91
|
+
(state.sequence ?? -1) >= 0 &&
|
|
92
|
+
Array.isArray(state.slots) &&
|
|
93
|
+
state.slots.every((slot) => Array.isArray(slot.vector) &&
|
|
94
|
+
typeof slot.strength === "number" &&
|
|
95
|
+
Number.isFinite(slot.strength) &&
|
|
96
|
+
Number.isInteger(slot.lastSequence)));
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=causal-head.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"causal-head.js","sourceRoot":"","sources":["../src/causal-head.ts"],"names":[],"mappings":"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","sourcesContent":["import { assertVector, average, blend, clamp01, cosine } from \"./math.js\";\nimport type { RankedItem, RetrievalBackend, RetrievalItem, RetrievalQuery } from \"./types.js\";\n\nexport interface CausalHeadState {\n\tsequence: number;\n\tslots: Array<{ vector: number[]; strength: number; lastSequence: number }>;\n}\n\nexport interface CausalHeadParameters {\n\tdimensions: number;\n\tcapacity: number;\n\tqueryWeight: number;\n\tstateWeight: number;\n\twriteRate: number;\n\tdecay: number;\n}\n\nexport class CausalHeadBackend implements RetrievalBackend<CausalHeadState> {\n\treadonly name = \"causal-head-v1\" as const;\n\treadonly dimensions: number;\n\n\tconstructor(readonly parameters: CausalHeadParameters) {\n\t\tthis.dimensions = parameters.dimensions;\n\t\tif (!Number.isInteger(parameters.capacity) || parameters.capacity < 1)\n\t\t\tthrow new Error(\"Causal Head capacity must be positive\");\n\t\tfor (const [name, value] of Object.entries(parameters)) {\n\t\t\tif (name !== \"dimensions\" && name !== \"capacity\" && (!Number.isFinite(value) || value < 0 || value > 1)) {\n\t\t\t\tthrow new Error(`Invalid Causal Head parameter: ${name}`);\n\t\t\t}\n\t\t}\n\t}\n\n\temptyState(): CausalHeadState {\n\t\treturn { sequence: 0, slots: [] };\n\t}\n\n\tretrieve(query: RetrievalQuery, items: readonly RetrievalItem[], state: CausalHeadState): RankedItem[] {\n\t\tassertVector(query.embedding, this.dimensions, \"query embedding\");\n\t\tconst stateVector = weightedStateVector(state, this.dimensions);\n\t\treturn eligibleItems(query, items, this.dimensions)\n\t\t\t.map((item) => ({\n\t\t\t\tid: item.id,\n\t\t\t\tscore: clamp01(\n\t\t\t\t\tcosine(query.embedding, item.embedding) * this.parameters.queryWeight +\n\t\t\t\t\t\tcosine(stateVector, item.embedding) * this.parameters.stateWeight,\n\t\t\t\t),\n\t\t\t\treasonCodes: [\"query_similarity\", \"causal_state_similarity\"],\n\t\t\t}))\n\t\t\t.sort((left, right) => right.score - left.score || left.id.localeCompare(right.id))\n\t\t\t.slice(0, query.limit);\n\t}\n\n\tupdate(state: CausalHeadState, selected: readonly RetrievalItem[], feedback = \"positive\"): CausalHeadState {\n\t\tconst sequence = state.sequence + 1;\n\t\tconst slots = state.slots.map((slot) => ({\n\t\t\t...slot,\n\t\t\tvector: [...slot.vector],\n\t\t\tstrength: slot.strength * this.parameters.decay,\n\t\t}));\n\t\tif (selected.length > 0) {\n\t\t\tfor (const item of selected) assertVector(item.embedding, this.dimensions, \"selected embedding\");\n\t\t\tconst candidate = average(\n\t\t\t\tselected.map((item) => item.embedding),\n\t\t\t\tthis.dimensions,\n\t\t\t);\n\t\t\tconst nearest = slots\n\t\t\t\t.map((slot, index) => ({ index, score: cosine(slot.vector, candidate) }))\n\t\t\t\t.sort((left, right) => right.score - left.score)[0];\n\t\t\tconst strengthDelta = feedback === \"positive\" ? this.parameters.writeRate : -this.parameters.writeRate;\n\t\t\tif (nearest && nearest.score >= 0.75) {\n\t\t\t\tconst slot = slots[nearest.index]!;\n\t\t\t\tslot.vector = blend(slot.vector, candidate, this.parameters.writeRate);\n\t\t\t\tslot.strength = clamp01(slot.strength + strengthDelta);\n\t\t\t\tslot.lastSequence = sequence;\n\t\t\t} else if (feedback === \"positive\") {\n\t\t\t\tslots.push({ vector: candidate, strength: this.parameters.writeRate, lastSequence: sequence });\n\t\t\t}\n\t\t}\n\t\tslots.sort((left, right) => right.strength - left.strength || right.lastSequence - left.lastSequence);\n\t\treturn { sequence, slots: slots.filter((slot) => slot.strength > 0.001).slice(0, this.parameters.capacity) };\n\t}\n\n\tserializeState(state: CausalHeadState): Record<string, unknown> {\n\t\treturn { sequence: state.sequence, slots: state.slots.map((slot) => ({ ...slot, vector: [...slot.vector] })) };\n\t}\n\n\tdeserializeState(value: unknown): CausalHeadState {\n\t\tif (!isCausalState(value)) throw new Error(\"Invalid Causal Head state snapshot\");\n\t\tfor (const slot of value.slots) assertVector(slot.vector, this.dimensions, \"state slot\");\n\t\tif (value.slots.length > this.parameters.capacity)\n\t\t\tthrow new Error(\"Causal Head state exceeds configured capacity\");\n\t\treturn structuredClone(value);\n\t}\n}\n\nfunction weightedStateVector(state: CausalHeadState, dimensions: number): number[] {\n\tconst total = state.slots.reduce((sum, slot) => sum + slot.strength, 0);\n\tif (total === 0) return Array.from({ length: dimensions }, () => 0);\n\treturn Array.from(\n\t\t{ length: dimensions },\n\t\t(_, index) => state.slots.reduce((sum, slot) => sum + (slot.vector[index] ?? 0) * slot.strength, 0) / total,\n\t);\n}\n\nfunction eligibleItems(query: RetrievalQuery, items: readonly RetrievalItem[], dimensions: number): RetrievalItem[] {\n\treturn items.filter((item) => {\n\t\tassertVector(item.embedding, dimensions, \"item embedding\");\n\t\treturn item.ownerId === query.ownerId && item.projectId === query.projectId && !item.revoked;\n\t});\n}\n\nfunction isCausalState(value: unknown): value is CausalHeadState {\n\tif (!value || typeof value !== \"object\") return false;\n\tconst state = value as Partial<CausalHeadState>;\n\treturn (\n\t\tNumber.isInteger(state.sequence) &&\n\t\t(state.sequence ?? -1) >= 0 &&\n\t\tArray.isArray(state.slots) &&\n\t\tstate.slots.every(\n\t\t\t(slot) =>\n\t\t\t\tArray.isArray(slot.vector) &&\n\t\t\t\ttypeof slot.strength === \"number\" &&\n\t\t\t\tNumber.isFinite(slot.strength) &&\n\t\t\t\tNumber.isInteger(slot.lastSequence),\n\t\t)\n\t);\n}\n"]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { InferenceManifestContract } from "@asm-agent/contracts";
|
|
2
|
+
import type { RetrievalBackend } from "./types.js";
|
|
3
|
+
export interface LoadedCheckpoint {
|
|
4
|
+
digest: string;
|
|
5
|
+
backend: RetrievalBackend;
|
|
6
|
+
}
|
|
7
|
+
export declare function loadCheckpoint(manifest: InferenceManifestContract, bytes: Uint8Array): LoadedCheckpoint;
|
|
8
|
+
//# sourceMappingURL=checkpoint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checkpoint.d.ts","sourceRoot":"","sources":["../src/checkpoint.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AAItE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AASnD,MAAM,WAAW,gBAAgB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,gBAAgB,CAAC;CAC1B;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,yBAAyB,EAAE,KAAK,EAAE,UAAU,GAAG,gBAAgB,CA2BvG","sourcesContent":["import { createHash } from \"node:crypto\";\nimport type { InferenceManifestContract } from \"@asm-agent/contracts\";\nimport { assertContract } from \"@asm-agent/contracts\";\nimport { CausalHeadBackend, type CausalHeadParameters } from \"./causal-head.js\";\nimport { DualAsmBackend, type DualAsmParameters } from \"./dual-asm.js\";\nimport type { RetrievalBackend } from \"./types.js\";\n\ninterface CheckpointEnvelope {\n\tformatVersion: 1;\n\tbackend: \"causal_head\" | \"dual_asm\";\n\tdimensions: number;\n\tparameters: Record<string, number>;\n}\n\nexport interface LoadedCheckpoint {\n\tdigest: string;\n\tbackend: RetrievalBackend;\n}\n\nexport function loadCheckpoint(manifest: InferenceManifestContract, bytes: Uint8Array): LoadedCheckpoint {\n\tassertContract(\"inferenceManifest\", manifest);\n\tif (manifest.backend !== \"causal_head\" && manifest.backend !== \"dual_asm\") {\n\t\tthrow new Error(`Manifest backend is not learned retrieval: ${manifest.backend}`);\n\t}\n\tif (manifest.contractMajors.retrieval !== 1 || manifest.contractMajors.stateSnapshot !== 1) {\n\t\tthrow new Error(\"Checkpoint manifest requires unsupported contract majors\");\n\t}\n\tconst digest = createHash(\"sha256\").update(bytes).digest(\"hex\");\n\tif (!manifest.artifactDigest || manifest.artifactDigest !== digest) throw new Error(\"Checkpoint SHA-256 mismatch\");\n\tlet value: unknown;\n\ttry {\n\t\tvalue = JSON.parse(new TextDecoder().decode(bytes));\n\t} catch {\n\t\tthrow new Error(\"Checkpoint must be valid JSON; executable serialization is not supported\");\n\t}\n\tif (!isCheckpointEnvelope(value)) throw new Error(\"Invalid checkpoint envelope\");\n\tif (value.backend !== manifest.backend) throw new Error(\"Checkpoint backend does not match manifest\");\n\tconst manifestDimensions = manifest.parameters.dimensions;\n\tif (manifestDimensions !== value.dimensions) throw new Error(\"Checkpoint dimensions do not match manifest\");\n\treturn {\n\t\tdigest,\n\t\tbackend:\n\t\t\tvalue.backend === \"causal_head\"\n\t\t\t\t? new CausalHeadBackend(value.parameters as unknown as CausalHeadParameters)\n\t\t\t\t: new DualAsmBackend(value.parameters as unknown as DualAsmParameters),\n\t};\n}\n\nfunction isCheckpointEnvelope(value: unknown): value is CheckpointEnvelope {\n\tif (!value || typeof value !== \"object\") return false;\n\tconst record = value as Record<string, unknown>;\n\tif (Object.keys(record).some((key) => ![\"formatVersion\", \"backend\", \"dimensions\", \"parameters\"].includes(key)))\n\t\treturn false;\n\tif (\n\t\trecord.formatVersion !== 1 ||\n\t\t(record.backend !== \"causal_head\" && record.backend !== \"dual_asm\") ||\n\t\t!Number.isInteger(record.dimensions) ||\n\t\t(record.dimensions as number) < 1 ||\n\t\t!record.parameters ||\n\t\ttypeof record.parameters !== \"object\" ||\n\t\tArray.isArray(record.parameters)\n\t) {\n\t\treturn false;\n\t}\n\treturn Object.values(record.parameters).every((item) => typeof item === \"number\" && Number.isFinite(item));\n}\n"]}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { assertContract } from "@asm-agent/contracts";
|
|
3
|
+
import { CausalHeadBackend } from "./causal-head.js";
|
|
4
|
+
import { DualAsmBackend } from "./dual-asm.js";
|
|
5
|
+
export function loadCheckpoint(manifest, bytes) {
|
|
6
|
+
assertContract("inferenceManifest", manifest);
|
|
7
|
+
if (manifest.backend !== "causal_head" && manifest.backend !== "dual_asm") {
|
|
8
|
+
throw new Error(`Manifest backend is not learned retrieval: ${manifest.backend}`);
|
|
9
|
+
}
|
|
10
|
+
if (manifest.contractMajors.retrieval !== 1 || manifest.contractMajors.stateSnapshot !== 1) {
|
|
11
|
+
throw new Error("Checkpoint manifest requires unsupported contract majors");
|
|
12
|
+
}
|
|
13
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
14
|
+
if (!manifest.artifactDigest || manifest.artifactDigest !== digest)
|
|
15
|
+
throw new Error("Checkpoint SHA-256 mismatch");
|
|
16
|
+
let value;
|
|
17
|
+
try {
|
|
18
|
+
value = JSON.parse(new TextDecoder().decode(bytes));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
throw new Error("Checkpoint must be valid JSON; executable serialization is not supported");
|
|
22
|
+
}
|
|
23
|
+
if (!isCheckpointEnvelope(value))
|
|
24
|
+
throw new Error("Invalid checkpoint envelope");
|
|
25
|
+
if (value.backend !== manifest.backend)
|
|
26
|
+
throw new Error("Checkpoint backend does not match manifest");
|
|
27
|
+
const manifestDimensions = manifest.parameters.dimensions;
|
|
28
|
+
if (manifestDimensions !== value.dimensions)
|
|
29
|
+
throw new Error("Checkpoint dimensions do not match manifest");
|
|
30
|
+
return {
|
|
31
|
+
digest,
|
|
32
|
+
backend: value.backend === "causal_head"
|
|
33
|
+
? new CausalHeadBackend(value.parameters)
|
|
34
|
+
: new DualAsmBackend(value.parameters),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function isCheckpointEnvelope(value) {
|
|
38
|
+
if (!value || typeof value !== "object")
|
|
39
|
+
return false;
|
|
40
|
+
const record = value;
|
|
41
|
+
if (Object.keys(record).some((key) => !["formatVersion", "backend", "dimensions", "parameters"].includes(key)))
|
|
42
|
+
return false;
|
|
43
|
+
if (record.formatVersion !== 1 ||
|
|
44
|
+
(record.backend !== "causal_head" && record.backend !== "dual_asm") ||
|
|
45
|
+
!Number.isInteger(record.dimensions) ||
|
|
46
|
+
record.dimensions < 1 ||
|
|
47
|
+
!record.parameters ||
|
|
48
|
+
typeof record.parameters !== "object" ||
|
|
49
|
+
Array.isArray(record.parameters)) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
return Object.values(record.parameters).every((item) => typeof item === "number" && Number.isFinite(item));
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=checkpoint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checkpoint.js","sourceRoot":"","sources":["../src/checkpoint.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from \"node:crypto\";\nimport type { InferenceManifestContract } from \"@asm-agent/contracts\";\nimport { assertContract } from \"@asm-agent/contracts\";\nimport { CausalHeadBackend, type CausalHeadParameters } from \"./causal-head.js\";\nimport { DualAsmBackend, type DualAsmParameters } from \"./dual-asm.js\";\nimport type { RetrievalBackend } from \"./types.js\";\n\ninterface CheckpointEnvelope {\n\tformatVersion: 1;\n\tbackend: \"causal_head\" | \"dual_asm\";\n\tdimensions: number;\n\tparameters: Record<string, number>;\n}\n\nexport interface LoadedCheckpoint {\n\tdigest: string;\n\tbackend: RetrievalBackend;\n}\n\nexport function loadCheckpoint(manifest: InferenceManifestContract, bytes: Uint8Array): LoadedCheckpoint {\n\tassertContract(\"inferenceManifest\", manifest);\n\tif (manifest.backend !== \"causal_head\" && manifest.backend !== \"dual_asm\") {\n\t\tthrow new Error(`Manifest backend is not learned retrieval: ${manifest.backend}`);\n\t}\n\tif (manifest.contractMajors.retrieval !== 1 || manifest.contractMajors.stateSnapshot !== 1) {\n\t\tthrow new Error(\"Checkpoint manifest requires unsupported contract majors\");\n\t}\n\tconst digest = createHash(\"sha256\").update(bytes).digest(\"hex\");\n\tif (!manifest.artifactDigest || manifest.artifactDigest !== digest) throw new Error(\"Checkpoint SHA-256 mismatch\");\n\tlet value: unknown;\n\ttry {\n\t\tvalue = JSON.parse(new TextDecoder().decode(bytes));\n\t} catch {\n\t\tthrow new Error(\"Checkpoint must be valid JSON; executable serialization is not supported\");\n\t}\n\tif (!isCheckpointEnvelope(value)) throw new Error(\"Invalid checkpoint envelope\");\n\tif (value.backend !== manifest.backend) throw new Error(\"Checkpoint backend does not match manifest\");\n\tconst manifestDimensions = manifest.parameters.dimensions;\n\tif (manifestDimensions !== value.dimensions) throw new Error(\"Checkpoint dimensions do not match manifest\");\n\treturn {\n\t\tdigest,\n\t\tbackend:\n\t\t\tvalue.backend === \"causal_head\"\n\t\t\t\t? new CausalHeadBackend(value.parameters as unknown as CausalHeadParameters)\n\t\t\t\t: new DualAsmBackend(value.parameters as unknown as DualAsmParameters),\n\t};\n}\n\nfunction isCheckpointEnvelope(value: unknown): value is CheckpointEnvelope {\n\tif (!value || typeof value !== \"object\") return false;\n\tconst record = value as Record<string, unknown>;\n\tif (Object.keys(record).some((key) => ![\"formatVersion\", \"backend\", \"dimensions\", \"parameters\"].includes(key)))\n\t\treturn false;\n\tif (\n\t\trecord.formatVersion !== 1 ||\n\t\t(record.backend !== \"causal_head\" && record.backend !== \"dual_asm\") ||\n\t\t!Number.isInteger(record.dimensions) ||\n\t\t(record.dimensions as number) < 1 ||\n\t\t!record.parameters ||\n\t\ttypeof record.parameters !== \"object\" ||\n\t\tArray.isArray(record.parameters)\n\t) {\n\t\treturn false;\n\t}\n\treturn Object.values(record.parameters).every((item) => typeof item === \"number\" && Number.isFinite(item));\n}\n"]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { RankedItem, RetrievalBackend, RetrievalItem, RetrievalQuery } from "./types.js";
|
|
2
|
+
export interface DualAsmState {
|
|
3
|
+
sequence: number;
|
|
4
|
+
positive: number[];
|
|
5
|
+
negative: number[];
|
|
6
|
+
positiveWrites: number;
|
|
7
|
+
negativeWrites: number;
|
|
8
|
+
}
|
|
9
|
+
export interface DualAsmParameters {
|
|
10
|
+
dimensions: number;
|
|
11
|
+
queryWeight: number;
|
|
12
|
+
positiveWeight: number;
|
|
13
|
+
negativeWeight: number;
|
|
14
|
+
writeRate: number;
|
|
15
|
+
}
|
|
16
|
+
export declare class DualAsmBackend implements RetrievalBackend<DualAsmState> {
|
|
17
|
+
readonly parameters: DualAsmParameters;
|
|
18
|
+
readonly name: "dual-asm-v1";
|
|
19
|
+
readonly dimensions: number;
|
|
20
|
+
constructor(parameters: DualAsmParameters);
|
|
21
|
+
emptyState(): DualAsmState;
|
|
22
|
+
retrieve(query: RetrievalQuery, items: readonly RetrievalItem[], state: DualAsmState): RankedItem[];
|
|
23
|
+
update(state: DualAsmState, selected: readonly RetrievalItem[], feedback?: string): DualAsmState;
|
|
24
|
+
serializeState(state: DualAsmState): Record<string, unknown>;
|
|
25
|
+
deserializeState(value: unknown): DualAsmState;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=dual-asm.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dual-asm.d.ts","sourceRoot":"","sources":["../src/dual-asm.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE9F,MAAM,WAAW,YAAY;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,cAAe,YAAW,gBAAgB,CAAC,YAAY,CAAC;IAIxD,QAAQ,CAAC,UAAU,EAAE,iBAAiB;IAHlD,QAAQ,CAAC,IAAI,gBAA0B;IACvC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAE5B,YAAqB,UAAU,EAAE,iBAAiB,EASjD;IAED,UAAU,IAAI,YAAY,CAQzB;IAED,QAAQ,CAAC,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE,SAAS,aAAa,EAAE,EAAE,KAAK,EAAE,YAAY,GAAG,UAAU,EAAE,CAiBlG;IAED,MAAM,CAAC,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,SAAS,aAAa,EAAE,EAAE,QAAQ,SAAa,GAAG,YAAY,CA4BnG;IAED,cAAc,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAE3D;IAED,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,YAAY,CAK7C;CACD","sourcesContent":["import { assertVector, average, blend, clamp01, cosine } from \"./math.js\";\nimport type { RankedItem, RetrievalBackend, RetrievalItem, RetrievalQuery } from \"./types.js\";\n\nexport interface DualAsmState {\n\tsequence: number;\n\tpositive: number[];\n\tnegative: number[];\n\tpositiveWrites: number;\n\tnegativeWrites: number;\n}\n\nexport interface DualAsmParameters {\n\tdimensions: number;\n\tqueryWeight: number;\n\tpositiveWeight: number;\n\tnegativeWeight: number;\n\twriteRate: number;\n}\n\nexport class DualAsmBackend implements RetrievalBackend<DualAsmState> {\n\treadonly name = \"dual-asm-v1\" as const;\n\treadonly dimensions: number;\n\n\tconstructor(readonly parameters: DualAsmParameters) {\n\t\tthis.dimensions = parameters.dimensions;\n\t\tif (!Number.isInteger(this.dimensions) || this.dimensions < 1)\n\t\t\tthrow new Error(\"Dual-ASM dimensions must be positive\");\n\t\tfor (const [name, value] of Object.entries(parameters)) {\n\t\t\tif (name !== \"dimensions\" && (!Number.isFinite(value) || value < 0 || value > 1)) {\n\t\t\t\tthrow new Error(`Invalid Dual-ASM parameter: ${name}`);\n\t\t\t}\n\t\t}\n\t}\n\n\temptyState(): DualAsmState {\n\t\treturn {\n\t\t\tsequence: 0,\n\t\t\tpositive: Array.from({ length: this.dimensions }, () => 0),\n\t\t\tnegative: Array.from({ length: this.dimensions }, () => 0),\n\t\t\tpositiveWrites: 0,\n\t\t\tnegativeWrites: 0,\n\t\t};\n\t}\n\n\tretrieve(query: RetrievalQuery, items: readonly RetrievalItem[], state: DualAsmState): RankedItem[] {\n\t\tassertVector(query.embedding, this.dimensions, \"query embedding\");\n\t\treturn items\n\t\t\t.filter((item) => {\n\t\t\t\tassertVector(item.embedding, this.dimensions, \"item embedding\");\n\t\t\t\treturn item.ownerId === query.ownerId && item.projectId === query.projectId && !item.revoked;\n\t\t\t})\n\t\t\t.map((item) => {\n\t\t\t\tconst score = clamp01(\n\t\t\t\t\tcosine(query.embedding, item.embedding) * this.parameters.queryWeight +\n\t\t\t\t\t\tcosine(state.positive, item.embedding) * this.parameters.positiveWeight -\n\t\t\t\t\t\tcosine(state.negative, item.embedding) * this.parameters.negativeWeight,\n\t\t\t\t);\n\t\t\t\treturn { id: item.id, score, reasonCodes: [\"query_similarity\", \"positive_state\", \"negative_state\"] };\n\t\t\t})\n\t\t\t.sort((left, right) => right.score - left.score || left.id.localeCompare(right.id))\n\t\t\t.slice(0, query.limit);\n\t}\n\n\tupdate(state: DualAsmState, selected: readonly RetrievalItem[], feedback = \"positive\"): DualAsmState {\n\t\tfor (const item of selected) assertVector(item.embedding, this.dimensions, \"selected embedding\");\n\t\tif (selected.length === 0)\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\tpositive: [...state.positive],\n\t\t\t\tnegative: [...state.negative],\n\t\t\t\tsequence: state.sequence + 1,\n\t\t\t};\n\t\tconst candidate = average(\n\t\t\tselected.map((item) => item.embedding),\n\t\t\tthis.dimensions,\n\t\t);\n\t\treturn feedback === \"positive\"\n\t\t\t? {\n\t\t\t\t\t...state,\n\t\t\t\t\tsequence: state.sequence + 1,\n\t\t\t\t\tpositive: blend(state.positive, candidate, this.parameters.writeRate),\n\t\t\t\t\tnegative: [...state.negative],\n\t\t\t\t\tpositiveWrites: state.positiveWrites + 1,\n\t\t\t\t}\n\t\t\t: {\n\t\t\t\t\t...state,\n\t\t\t\t\tsequence: state.sequence + 1,\n\t\t\t\t\tpositive: [...state.positive],\n\t\t\t\t\tnegative: blend(state.negative, candidate, this.parameters.writeRate),\n\t\t\t\t\tnegativeWrites: state.negativeWrites + 1,\n\t\t\t\t};\n\t}\n\n\tserializeState(state: DualAsmState): Record<string, unknown> {\n\t\treturn { ...state, positive: [...state.positive], negative: [...state.negative] };\n\t}\n\n\tdeserializeState(value: unknown): DualAsmState {\n\t\tif (!isDualState(value)) throw new Error(\"Invalid Dual-ASM state snapshot\");\n\t\tassertVector(value.positive, this.dimensions, \"positive state\");\n\t\tassertVector(value.negative, this.dimensions, \"negative state\");\n\t\treturn structuredClone(value);\n\t}\n}\n\nfunction isDualState(value: unknown): value is DualAsmState {\n\tif (!value || typeof value !== \"object\") return false;\n\tconst state = value as Partial<DualAsmState>;\n\treturn (\n\t\tNumber.isInteger(state.sequence) &&\n\t\t(state.sequence ?? -1) >= 0 &&\n\t\tArray.isArray(state.positive) &&\n\t\tArray.isArray(state.negative) &&\n\t\tNumber.isInteger(state.positiveWrites) &&\n\t\tNumber.isInteger(state.negativeWrites)\n\t);\n}\n"]}
|
package/dist/dual-asm.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { assertVector, average, blend, clamp01, cosine } from "./math.js";
|
|
2
|
+
export class DualAsmBackend {
|
|
3
|
+
parameters;
|
|
4
|
+
name = "dual-asm-v1";
|
|
5
|
+
dimensions;
|
|
6
|
+
constructor(parameters) {
|
|
7
|
+
this.parameters = parameters;
|
|
8
|
+
this.dimensions = parameters.dimensions;
|
|
9
|
+
if (!Number.isInteger(this.dimensions) || this.dimensions < 1)
|
|
10
|
+
throw new Error("Dual-ASM dimensions must be positive");
|
|
11
|
+
for (const [name, value] of Object.entries(parameters)) {
|
|
12
|
+
if (name !== "dimensions" && (!Number.isFinite(value) || value < 0 || value > 1)) {
|
|
13
|
+
throw new Error(`Invalid Dual-ASM parameter: ${name}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
emptyState() {
|
|
18
|
+
return {
|
|
19
|
+
sequence: 0,
|
|
20
|
+
positive: Array.from({ length: this.dimensions }, () => 0),
|
|
21
|
+
negative: Array.from({ length: this.dimensions }, () => 0),
|
|
22
|
+
positiveWrites: 0,
|
|
23
|
+
negativeWrites: 0,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
retrieve(query, items, state) {
|
|
27
|
+
assertVector(query.embedding, this.dimensions, "query embedding");
|
|
28
|
+
return items
|
|
29
|
+
.filter((item) => {
|
|
30
|
+
assertVector(item.embedding, this.dimensions, "item embedding");
|
|
31
|
+
return item.ownerId === query.ownerId && item.projectId === query.projectId && !item.revoked;
|
|
32
|
+
})
|
|
33
|
+
.map((item) => {
|
|
34
|
+
const score = clamp01(cosine(query.embedding, item.embedding) * this.parameters.queryWeight +
|
|
35
|
+
cosine(state.positive, item.embedding) * this.parameters.positiveWeight -
|
|
36
|
+
cosine(state.negative, item.embedding) * this.parameters.negativeWeight);
|
|
37
|
+
return { id: item.id, score, reasonCodes: ["query_similarity", "positive_state", "negative_state"] };
|
|
38
|
+
})
|
|
39
|
+
.sort((left, right) => right.score - left.score || left.id.localeCompare(right.id))
|
|
40
|
+
.slice(0, query.limit);
|
|
41
|
+
}
|
|
42
|
+
update(state, selected, feedback = "positive") {
|
|
43
|
+
for (const item of selected)
|
|
44
|
+
assertVector(item.embedding, this.dimensions, "selected embedding");
|
|
45
|
+
if (selected.length === 0)
|
|
46
|
+
return {
|
|
47
|
+
...state,
|
|
48
|
+
positive: [...state.positive],
|
|
49
|
+
negative: [...state.negative],
|
|
50
|
+
sequence: state.sequence + 1,
|
|
51
|
+
};
|
|
52
|
+
const candidate = average(selected.map((item) => item.embedding), this.dimensions);
|
|
53
|
+
return feedback === "positive"
|
|
54
|
+
? {
|
|
55
|
+
...state,
|
|
56
|
+
sequence: state.sequence + 1,
|
|
57
|
+
positive: blend(state.positive, candidate, this.parameters.writeRate),
|
|
58
|
+
negative: [...state.negative],
|
|
59
|
+
positiveWrites: state.positiveWrites + 1,
|
|
60
|
+
}
|
|
61
|
+
: {
|
|
62
|
+
...state,
|
|
63
|
+
sequence: state.sequence + 1,
|
|
64
|
+
positive: [...state.positive],
|
|
65
|
+
negative: blend(state.negative, candidate, this.parameters.writeRate),
|
|
66
|
+
negativeWrites: state.negativeWrites + 1,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
serializeState(state) {
|
|
70
|
+
return { ...state, positive: [...state.positive], negative: [...state.negative] };
|
|
71
|
+
}
|
|
72
|
+
deserializeState(value) {
|
|
73
|
+
if (!isDualState(value))
|
|
74
|
+
throw new Error("Invalid Dual-ASM state snapshot");
|
|
75
|
+
assertVector(value.positive, this.dimensions, "positive state");
|
|
76
|
+
assertVector(value.negative, this.dimensions, "negative state");
|
|
77
|
+
return structuredClone(value);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function isDualState(value) {
|
|
81
|
+
if (!value || typeof value !== "object")
|
|
82
|
+
return false;
|
|
83
|
+
const state = value;
|
|
84
|
+
return (Number.isInteger(state.sequence) &&
|
|
85
|
+
(state.sequence ?? -1) >= 0 &&
|
|
86
|
+
Array.isArray(state.positive) &&
|
|
87
|
+
Array.isArray(state.negative) &&
|
|
88
|
+
Number.isInteger(state.positiveWrites) &&
|
|
89
|
+
Number.isInteger(state.negativeWrites));
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=dual-asm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dual-asm.js","sourceRoot":"","sources":["../src/dual-asm.ts"],"names":[],"mappings":"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","sourcesContent":["import { assertVector, average, blend, clamp01, cosine } from \"./math.js\";\nimport type { RankedItem, RetrievalBackend, RetrievalItem, RetrievalQuery } from \"./types.js\";\n\nexport interface DualAsmState {\n\tsequence: number;\n\tpositive: number[];\n\tnegative: number[];\n\tpositiveWrites: number;\n\tnegativeWrites: number;\n}\n\nexport interface DualAsmParameters {\n\tdimensions: number;\n\tqueryWeight: number;\n\tpositiveWeight: number;\n\tnegativeWeight: number;\n\twriteRate: number;\n}\n\nexport class DualAsmBackend implements RetrievalBackend<DualAsmState> {\n\treadonly name = \"dual-asm-v1\" as const;\n\treadonly dimensions: number;\n\n\tconstructor(readonly parameters: DualAsmParameters) {\n\t\tthis.dimensions = parameters.dimensions;\n\t\tif (!Number.isInteger(this.dimensions) || this.dimensions < 1)\n\t\t\tthrow new Error(\"Dual-ASM dimensions must be positive\");\n\t\tfor (const [name, value] of Object.entries(parameters)) {\n\t\t\tif (name !== \"dimensions\" && (!Number.isFinite(value) || value < 0 || value > 1)) {\n\t\t\t\tthrow new Error(`Invalid Dual-ASM parameter: ${name}`);\n\t\t\t}\n\t\t}\n\t}\n\n\temptyState(): DualAsmState {\n\t\treturn {\n\t\t\tsequence: 0,\n\t\t\tpositive: Array.from({ length: this.dimensions }, () => 0),\n\t\t\tnegative: Array.from({ length: this.dimensions }, () => 0),\n\t\t\tpositiveWrites: 0,\n\t\t\tnegativeWrites: 0,\n\t\t};\n\t}\n\n\tretrieve(query: RetrievalQuery, items: readonly RetrievalItem[], state: DualAsmState): RankedItem[] {\n\t\tassertVector(query.embedding, this.dimensions, \"query embedding\");\n\t\treturn items\n\t\t\t.filter((item) => {\n\t\t\t\tassertVector(item.embedding, this.dimensions, \"item embedding\");\n\t\t\t\treturn item.ownerId === query.ownerId && item.projectId === query.projectId && !item.revoked;\n\t\t\t})\n\t\t\t.map((item) => {\n\t\t\t\tconst score = clamp01(\n\t\t\t\t\tcosine(query.embedding, item.embedding) * this.parameters.queryWeight +\n\t\t\t\t\t\tcosine(state.positive, item.embedding) * this.parameters.positiveWeight -\n\t\t\t\t\t\tcosine(state.negative, item.embedding) * this.parameters.negativeWeight,\n\t\t\t\t);\n\t\t\t\treturn { id: item.id, score, reasonCodes: [\"query_similarity\", \"positive_state\", \"negative_state\"] };\n\t\t\t})\n\t\t\t.sort((left, right) => right.score - left.score || left.id.localeCompare(right.id))\n\t\t\t.slice(0, query.limit);\n\t}\n\n\tupdate(state: DualAsmState, selected: readonly RetrievalItem[], feedback = \"positive\"): DualAsmState {\n\t\tfor (const item of selected) assertVector(item.embedding, this.dimensions, \"selected embedding\");\n\t\tif (selected.length === 0)\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\tpositive: [...state.positive],\n\t\t\t\tnegative: [...state.negative],\n\t\t\t\tsequence: state.sequence + 1,\n\t\t\t};\n\t\tconst candidate = average(\n\t\t\tselected.map((item) => item.embedding),\n\t\t\tthis.dimensions,\n\t\t);\n\t\treturn feedback === \"positive\"\n\t\t\t? {\n\t\t\t\t\t...state,\n\t\t\t\t\tsequence: state.sequence + 1,\n\t\t\t\t\tpositive: blend(state.positive, candidate, this.parameters.writeRate),\n\t\t\t\t\tnegative: [...state.negative],\n\t\t\t\t\tpositiveWrites: state.positiveWrites + 1,\n\t\t\t\t}\n\t\t\t: {\n\t\t\t\t\t...state,\n\t\t\t\t\tsequence: state.sequence + 1,\n\t\t\t\t\tpositive: [...state.positive],\n\t\t\t\t\tnegative: blend(state.negative, candidate, this.parameters.writeRate),\n\t\t\t\t\tnegativeWrites: state.negativeWrites + 1,\n\t\t\t\t};\n\t}\n\n\tserializeState(state: DualAsmState): Record<string, unknown> {\n\t\treturn { ...state, positive: [...state.positive], negative: [...state.negative] };\n\t}\n\n\tdeserializeState(value: unknown): DualAsmState {\n\t\tif (!isDualState(value)) throw new Error(\"Invalid Dual-ASM state snapshot\");\n\t\tassertVector(value.positive, this.dimensions, \"positive state\");\n\t\tassertVector(value.negative, this.dimensions, \"negative state\");\n\t\treturn structuredClone(value);\n\t}\n}\n\nfunction isDualState(value: unknown): value is DualAsmState {\n\tif (!value || typeof value !== \"object\") return false;\n\tconst state = value as Partial<DualAsmState>;\n\treturn (\n\t\tNumber.isInteger(state.sequence) &&\n\t\t(state.sequence ?? -1) >= 0 &&\n\t\tArray.isArray(state.positive) &&\n\t\tArray.isArray(state.negative) &&\n\t\tNumber.isInteger(state.positiveWrites) &&\n\t\tNumber.isInteger(state.negativeWrites)\n\t);\n}\n"]}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { RankedItem, RetrievalItem, RetrievalQuery } from "./types.js";
|
|
2
|
+
export interface Ranker {
|
|
3
|
+
readonly name: string;
|
|
4
|
+
retrieve(query: RetrievalQuery, items: readonly RetrievalItem[]): RankedItem[];
|
|
5
|
+
}
|
|
6
|
+
export interface BenchmarkCase {
|
|
7
|
+
id: string;
|
|
8
|
+
query: RetrievalQuery;
|
|
9
|
+
items: RetrievalItem[];
|
|
10
|
+
relevantIds: string[];
|
|
11
|
+
}
|
|
12
|
+
export interface BenchmarkMetrics {
|
|
13
|
+
recallAtK: number;
|
|
14
|
+
meanReciprocalRank: number;
|
|
15
|
+
p95LatencyMs: number;
|
|
16
|
+
p99LatencyMs: number;
|
|
17
|
+
capacityRetention: number;
|
|
18
|
+
falseForgettingRate: number;
|
|
19
|
+
revocationLeakage: number;
|
|
20
|
+
projectIsolationLeakage: number;
|
|
21
|
+
}
|
|
22
|
+
export interface PromotionThresholds {
|
|
23
|
+
minimumRecallAtK: number;
|
|
24
|
+
minimumMeanReciprocalRank: number;
|
|
25
|
+
maximumLatencyRatio: number;
|
|
26
|
+
maximumP99LatencyRatio: number;
|
|
27
|
+
minimumCapacityRetention: number;
|
|
28
|
+
maximumFalseForgettingRate: number;
|
|
29
|
+
maximumRevocationLeakage: number;
|
|
30
|
+
maximumProjectIsolationLeakage: number;
|
|
31
|
+
}
|
|
32
|
+
export interface PromotionEvaluation {
|
|
33
|
+
passed: boolean;
|
|
34
|
+
reasons: string[];
|
|
35
|
+
baseline: BenchmarkMetrics;
|
|
36
|
+
candidate: BenchmarkMetrics;
|
|
37
|
+
thresholds: PromotionThresholds;
|
|
38
|
+
}
|
|
39
|
+
export declare const DEFAULT_PROMOTION_THRESHOLDS: Readonly<PromotionThresholds>;
|
|
40
|
+
export declare function benchmark(ranker: Ranker, cases: readonly BenchmarkCase[], options?: {
|
|
41
|
+
capacityRetention?: number;
|
|
42
|
+
forgottenRelevantIds?: ReadonlySet<string>;
|
|
43
|
+
}): BenchmarkMetrics;
|
|
44
|
+
export declare function evaluatePromotion(baseline: BenchmarkMetrics, candidate: BenchmarkMetrics, thresholds: PromotionThresholds): PromotionEvaluation;
|
|
45
|
+
export declare class ShadowRetrievalRunner {
|
|
46
|
+
private readonly primary;
|
|
47
|
+
private readonly shadow;
|
|
48
|
+
private readonly onObservation?;
|
|
49
|
+
constructor(primary: Ranker, shadow: Ranker, onObservation?: ((observation: ShadowObservation) => void) | undefined);
|
|
50
|
+
retrieve(query: RetrievalQuery, items: readonly RetrievalItem[]): RankedItem[];
|
|
51
|
+
}
|
|
52
|
+
export interface ShadowObservation {
|
|
53
|
+
primaryBackend: string;
|
|
54
|
+
shadowBackend: string;
|
|
55
|
+
primaryIds: string[];
|
|
56
|
+
shadowIds: string[];
|
|
57
|
+
primaryLatencyMs: number;
|
|
58
|
+
shadowLatencyMs: number;
|
|
59
|
+
error?: string;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=evaluation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"evaluation.d.ts","sourceRoot":"","sources":["../src/evaluation.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE5E,MAAM,WAAW,MAAM;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE,SAAS,aAAa,EAAE,GAAG,UAAU,EAAE,CAAC;CAC/E;AAED,MAAM,WAAW,aAAa;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,cAAc,CAAC;IACtB,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,WAAW,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,uBAAuB,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,mBAAmB;IACnC,gBAAgB,EAAE,MAAM,CAAC;IACzB,yBAAyB,EAAE,MAAM,CAAC;IAClC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,wBAAwB,EAAE,MAAM,CAAC;IACjC,0BAA0B,EAAE,MAAM,CAAC;IACnC,wBAAwB,EAAE,MAAM,CAAC;IACjC,8BAA8B,EAAE,MAAM,CAAC;CACvC;AAED,MAAM,WAAW,mBAAmB;IACnC,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,SAAS,EAAE,gBAAgB,CAAC;IAC5B,UAAU,EAAE,mBAAmB,CAAC;CAChC;AAED,eAAO,MAAM,4BAA4B,EAAE,QAAQ,CAAC,mBAAmB,CAStE,CAAC;AAEF,wBAAgB,SAAS,CACxB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,SAAS,aAAa,EAAE,EAC/B,OAAO,GAAE;IAAE,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAAC,oBAAoB,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;CAAO,GACtF,gBAAgB,CA2ClB;AAED,wBAAgB,iBAAiB,CAChC,QAAQ,EAAE,gBAAgB,EAC1B,SAAS,EAAE,gBAAgB,EAC3B,UAAU,EAAE,mBAAmB,GAC7B,mBAAmB,CAiBrB;AAED,qBAAa,qBAAqB;IAEhC,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC;IAHhC,YACkB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,aAAa,CAAC,wDAA0C,EACtE;IAEJ,QAAQ,CAAC,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE,SAAS,aAAa,EAAE,GAAG,UAAU,EAAE,CA2B7E;CACD;AAED,MAAM,WAAW,iBAAiB;IACjC,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;CACf","sourcesContent":["import { performance } from \"node:perf_hooks\";\nimport type { RankedItem, RetrievalItem, RetrievalQuery } from \"./types.js\";\n\nexport interface Ranker {\n\treadonly name: string;\n\tretrieve(query: RetrievalQuery, items: readonly RetrievalItem[]): RankedItem[];\n}\n\nexport interface BenchmarkCase {\n\tid: string;\n\tquery: RetrievalQuery;\n\titems: RetrievalItem[];\n\trelevantIds: string[];\n}\n\nexport interface BenchmarkMetrics {\n\trecallAtK: number;\n\tmeanReciprocalRank: number;\n\tp95LatencyMs: number;\n\tp99LatencyMs: number;\n\tcapacityRetention: number;\n\tfalseForgettingRate: number;\n\trevocationLeakage: number;\n\tprojectIsolationLeakage: number;\n}\n\nexport interface PromotionThresholds {\n\tminimumRecallAtK: number;\n\tminimumMeanReciprocalRank: number;\n\tmaximumLatencyRatio: number;\n\tmaximumP99LatencyRatio: number;\n\tminimumCapacityRetention: number;\n\tmaximumFalseForgettingRate: number;\n\tmaximumRevocationLeakage: number;\n\tmaximumProjectIsolationLeakage: number;\n}\n\nexport interface PromotionEvaluation {\n\tpassed: boolean;\n\treasons: string[];\n\tbaseline: BenchmarkMetrics;\n\tcandidate: BenchmarkMetrics;\n\tthresholds: PromotionThresholds;\n}\n\nexport const DEFAULT_PROMOTION_THRESHOLDS: Readonly<PromotionThresholds> = {\n\tminimumRecallAtK: 0.9,\n\tminimumMeanReciprocalRank: 0.85,\n\tmaximumLatencyRatio: 2,\n\tmaximumP99LatencyRatio: 2.5,\n\tminimumCapacityRetention: 0.95,\n\tmaximumFalseForgettingRate: 0.02,\n\tmaximumRevocationLeakage: 0,\n\tmaximumProjectIsolationLeakage: 0,\n};\n\nexport function benchmark(\n\tranker: Ranker,\n\tcases: readonly BenchmarkCase[],\n\toptions: { capacityRetention?: number; forgottenRelevantIds?: ReadonlySet<string> } = {},\n): BenchmarkMetrics {\n\tif (cases.length === 0) throw new Error(\"Benchmark requires frozen cases\");\n\tlet recall = 0;\n\tlet reciprocalRank = 0;\n\tlet revokedReturned = 0;\n\tlet crossProjectReturned = 0;\n\tlet totalReturned = 0;\n\tlet relevantCount = 0;\n\tlet forgotten = 0;\n\tconst latencies: number[] = [];\n\tfor (const fixture of cases) {\n\t\tconst started = performance.now();\n\t\tconst ranked = ranker.retrieve(fixture.query, fixture.items);\n\t\tlatencies.push(Math.max(0.001, performance.now() - started));\n\t\tconst ids = ranked.map((item) => item.id);\n\t\tconst relevant = new Set(fixture.relevantIds);\n\t\tconst hits = ids.filter((id) => relevant.has(id));\n\t\trecall += relevant.size === 0 ? 1 : hits.length / relevant.size;\n\t\tconst firstRelevant = ids.findIndex((id) => relevant.has(id));\n\t\treciprocalRank += firstRelevant < 0 ? 0 : 1 / (firstRelevant + 1);\n\t\trelevantCount += relevant.size;\n\t\tforgotten += fixture.relevantIds.filter((id) => options.forgottenRelevantIds?.has(id)).length;\n\t\tfor (const result of ranked) {\n\t\t\tconst item = fixture.items.find((candidate) => candidate.id === result.id);\n\t\t\tif (item?.revoked) revokedReturned++;\n\t\t\tif (item && (item.ownerId !== fixture.query.ownerId || item.projectId !== fixture.query.projectId))\n\t\t\t\tcrossProjectReturned++;\n\t\t\ttotalReturned++;\n\t\t}\n\t}\n\tlatencies.sort((left, right) => left - right);\n\tconst p95Index = Math.min(latencies.length - 1, Math.ceil(latencies.length * 0.95) - 1);\n\tconst p99Index = Math.min(latencies.length - 1, Math.ceil(latencies.length * 0.99) - 1);\n\treturn {\n\t\trecallAtK: recall / cases.length,\n\t\tmeanReciprocalRank: reciprocalRank / cases.length,\n\t\tp95LatencyMs: latencies[p95Index] ?? 0,\n\t\tp99LatencyMs: latencies[p99Index] ?? 0,\n\t\tcapacityRetention: options.capacityRetention ?? 1,\n\t\tfalseForgettingRate: relevantCount === 0 ? 0 : forgotten / relevantCount,\n\t\trevocationLeakage: totalReturned === 0 ? 0 : revokedReturned / totalReturned,\n\t\tprojectIsolationLeakage: totalReturned === 0 ? 0 : crossProjectReturned / totalReturned,\n\t};\n}\n\nexport function evaluatePromotion(\n\tbaseline: BenchmarkMetrics,\n\tcandidate: BenchmarkMetrics,\n\tthresholds: PromotionThresholds,\n): PromotionEvaluation {\n\tconst reasons: string[] = [];\n\tif (candidate.recallAtK < thresholds.minimumRecallAtK) reasons.push(\"recall_below_threshold\");\n\tif (candidate.meanReciprocalRank < thresholds.minimumMeanReciprocalRank) reasons.push(\"mrr_below_threshold\");\n\tconst latencyRatio = baseline.p95LatencyMs === 0 ? 1 : candidate.p95LatencyMs / baseline.p95LatencyMs;\n\tif (latencyRatio > thresholds.maximumLatencyRatio) reasons.push(\"latency_ratio_above_threshold\");\n\tconst p99LatencyRatio = baseline.p99LatencyMs === 0 ? 1 : candidate.p99LatencyMs / baseline.p99LatencyMs;\n\tif (p99LatencyRatio > thresholds.maximumP99LatencyRatio) reasons.push(\"p99_latency_ratio_above_threshold\");\n\tif (candidate.capacityRetention < thresholds.minimumCapacityRetention)\n\t\treasons.push(\"capacity_retention_below_threshold\");\n\tif (candidate.falseForgettingRate > thresholds.maximumFalseForgettingRate)\n\t\treasons.push(\"false_forgetting_above_threshold\");\n\tif (candidate.revocationLeakage > thresholds.maximumRevocationLeakage)\n\t\treasons.push(\"revocation_leakage_above_threshold\");\n\tif (candidate.projectIsolationLeakage > thresholds.maximumProjectIsolationLeakage)\n\t\treasons.push(\"project_isolation_leakage_above_threshold\");\n\treturn { passed: reasons.length === 0, reasons, baseline, candidate, thresholds };\n}\n\nexport class ShadowRetrievalRunner {\n\tconstructor(\n\t\tprivate readonly primary: Ranker,\n\t\tprivate readonly shadow: Ranker,\n\t\tprivate readonly onObservation?: (observation: ShadowObservation) => void,\n\t) {}\n\n\tretrieve(query: RetrievalQuery, items: readonly RetrievalItem[]): RankedItem[] {\n\t\tconst primaryStarted = performance.now();\n\t\tconst primary = this.primary.retrieve(query, items);\n\t\tconst primaryLatencyMs = performance.now() - primaryStarted;\n\t\ttry {\n\t\t\tconst shadowStarted = performance.now();\n\t\t\tconst shadow = this.shadow.retrieve(query, items);\n\t\t\tthis.onObservation?.({\n\t\t\t\tprimaryBackend: this.primary.name,\n\t\t\t\tshadowBackend: this.shadow.name,\n\t\t\t\tprimaryIds: primary.map((item) => item.id),\n\t\t\t\tshadowIds: shadow.map((item) => item.id),\n\t\t\t\tprimaryLatencyMs,\n\t\t\t\tshadowLatencyMs: performance.now() - shadowStarted,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tthis.onObservation?.({\n\t\t\t\tprimaryBackend: this.primary.name,\n\t\t\t\tshadowBackend: this.shadow.name,\n\t\t\t\tprimaryIds: primary.map((item) => item.id),\n\t\t\t\tshadowIds: [],\n\t\t\t\tprimaryLatencyMs,\n\t\t\t\tshadowLatencyMs: 0,\n\t\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\t});\n\t\t}\n\t\treturn primary;\n\t}\n}\n\nexport interface ShadowObservation {\n\tprimaryBackend: string;\n\tshadowBackend: string;\n\tprimaryIds: string[];\n\tshadowIds: string[];\n\tprimaryLatencyMs: number;\n\tshadowLatencyMs: number;\n\terror?: string;\n}\n"]}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { performance } from "node:perf_hooks";
|
|
2
|
+
export const DEFAULT_PROMOTION_THRESHOLDS = {
|
|
3
|
+
minimumRecallAtK: 0.9,
|
|
4
|
+
minimumMeanReciprocalRank: 0.85,
|
|
5
|
+
maximumLatencyRatio: 2,
|
|
6
|
+
maximumP99LatencyRatio: 2.5,
|
|
7
|
+
minimumCapacityRetention: 0.95,
|
|
8
|
+
maximumFalseForgettingRate: 0.02,
|
|
9
|
+
maximumRevocationLeakage: 0,
|
|
10
|
+
maximumProjectIsolationLeakage: 0,
|
|
11
|
+
};
|
|
12
|
+
export function benchmark(ranker, cases, options = {}) {
|
|
13
|
+
if (cases.length === 0)
|
|
14
|
+
throw new Error("Benchmark requires frozen cases");
|
|
15
|
+
let recall = 0;
|
|
16
|
+
let reciprocalRank = 0;
|
|
17
|
+
let revokedReturned = 0;
|
|
18
|
+
let crossProjectReturned = 0;
|
|
19
|
+
let totalReturned = 0;
|
|
20
|
+
let relevantCount = 0;
|
|
21
|
+
let forgotten = 0;
|
|
22
|
+
const latencies = [];
|
|
23
|
+
for (const fixture of cases) {
|
|
24
|
+
const started = performance.now();
|
|
25
|
+
const ranked = ranker.retrieve(fixture.query, fixture.items);
|
|
26
|
+
latencies.push(Math.max(0.001, performance.now() - started));
|
|
27
|
+
const ids = ranked.map((item) => item.id);
|
|
28
|
+
const relevant = new Set(fixture.relevantIds);
|
|
29
|
+
const hits = ids.filter((id) => relevant.has(id));
|
|
30
|
+
recall += relevant.size === 0 ? 1 : hits.length / relevant.size;
|
|
31
|
+
const firstRelevant = ids.findIndex((id) => relevant.has(id));
|
|
32
|
+
reciprocalRank += firstRelevant < 0 ? 0 : 1 / (firstRelevant + 1);
|
|
33
|
+
relevantCount += relevant.size;
|
|
34
|
+
forgotten += fixture.relevantIds.filter((id) => options.forgottenRelevantIds?.has(id)).length;
|
|
35
|
+
for (const result of ranked) {
|
|
36
|
+
const item = fixture.items.find((candidate) => candidate.id === result.id);
|
|
37
|
+
if (item?.revoked)
|
|
38
|
+
revokedReturned++;
|
|
39
|
+
if (item && (item.ownerId !== fixture.query.ownerId || item.projectId !== fixture.query.projectId))
|
|
40
|
+
crossProjectReturned++;
|
|
41
|
+
totalReturned++;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
latencies.sort((left, right) => left - right);
|
|
45
|
+
const p95Index = Math.min(latencies.length - 1, Math.ceil(latencies.length * 0.95) - 1);
|
|
46
|
+
const p99Index = Math.min(latencies.length - 1, Math.ceil(latencies.length * 0.99) - 1);
|
|
47
|
+
return {
|
|
48
|
+
recallAtK: recall / cases.length,
|
|
49
|
+
meanReciprocalRank: reciprocalRank / cases.length,
|
|
50
|
+
p95LatencyMs: latencies[p95Index] ?? 0,
|
|
51
|
+
p99LatencyMs: latencies[p99Index] ?? 0,
|
|
52
|
+
capacityRetention: options.capacityRetention ?? 1,
|
|
53
|
+
falseForgettingRate: relevantCount === 0 ? 0 : forgotten / relevantCount,
|
|
54
|
+
revocationLeakage: totalReturned === 0 ? 0 : revokedReturned / totalReturned,
|
|
55
|
+
projectIsolationLeakage: totalReturned === 0 ? 0 : crossProjectReturned / totalReturned,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function evaluatePromotion(baseline, candidate, thresholds) {
|
|
59
|
+
const reasons = [];
|
|
60
|
+
if (candidate.recallAtK < thresholds.minimumRecallAtK)
|
|
61
|
+
reasons.push("recall_below_threshold");
|
|
62
|
+
if (candidate.meanReciprocalRank < thresholds.minimumMeanReciprocalRank)
|
|
63
|
+
reasons.push("mrr_below_threshold");
|
|
64
|
+
const latencyRatio = baseline.p95LatencyMs === 0 ? 1 : candidate.p95LatencyMs / baseline.p95LatencyMs;
|
|
65
|
+
if (latencyRatio > thresholds.maximumLatencyRatio)
|
|
66
|
+
reasons.push("latency_ratio_above_threshold");
|
|
67
|
+
const p99LatencyRatio = baseline.p99LatencyMs === 0 ? 1 : candidate.p99LatencyMs / baseline.p99LatencyMs;
|
|
68
|
+
if (p99LatencyRatio > thresholds.maximumP99LatencyRatio)
|
|
69
|
+
reasons.push("p99_latency_ratio_above_threshold");
|
|
70
|
+
if (candidate.capacityRetention < thresholds.minimumCapacityRetention)
|
|
71
|
+
reasons.push("capacity_retention_below_threshold");
|
|
72
|
+
if (candidate.falseForgettingRate > thresholds.maximumFalseForgettingRate)
|
|
73
|
+
reasons.push("false_forgetting_above_threshold");
|
|
74
|
+
if (candidate.revocationLeakage > thresholds.maximumRevocationLeakage)
|
|
75
|
+
reasons.push("revocation_leakage_above_threshold");
|
|
76
|
+
if (candidate.projectIsolationLeakage > thresholds.maximumProjectIsolationLeakage)
|
|
77
|
+
reasons.push("project_isolation_leakage_above_threshold");
|
|
78
|
+
return { passed: reasons.length === 0, reasons, baseline, candidate, thresholds };
|
|
79
|
+
}
|
|
80
|
+
export class ShadowRetrievalRunner {
|
|
81
|
+
primary;
|
|
82
|
+
shadow;
|
|
83
|
+
onObservation;
|
|
84
|
+
constructor(primary, shadow, onObservation) {
|
|
85
|
+
this.primary = primary;
|
|
86
|
+
this.shadow = shadow;
|
|
87
|
+
this.onObservation = onObservation;
|
|
88
|
+
}
|
|
89
|
+
retrieve(query, items) {
|
|
90
|
+
const primaryStarted = performance.now();
|
|
91
|
+
const primary = this.primary.retrieve(query, items);
|
|
92
|
+
const primaryLatencyMs = performance.now() - primaryStarted;
|
|
93
|
+
try {
|
|
94
|
+
const shadowStarted = performance.now();
|
|
95
|
+
const shadow = this.shadow.retrieve(query, items);
|
|
96
|
+
this.onObservation?.({
|
|
97
|
+
primaryBackend: this.primary.name,
|
|
98
|
+
shadowBackend: this.shadow.name,
|
|
99
|
+
primaryIds: primary.map((item) => item.id),
|
|
100
|
+
shadowIds: shadow.map((item) => item.id),
|
|
101
|
+
primaryLatencyMs,
|
|
102
|
+
shadowLatencyMs: performance.now() - shadowStarted,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
this.onObservation?.({
|
|
107
|
+
primaryBackend: this.primary.name,
|
|
108
|
+
shadowBackend: this.shadow.name,
|
|
109
|
+
primaryIds: primary.map((item) => item.id),
|
|
110
|
+
shadowIds: [],
|
|
111
|
+
primaryLatencyMs,
|
|
112
|
+
shadowLatencyMs: 0,
|
|
113
|
+
error: error instanceof Error ? error.message : String(error),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return primary;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=evaluation.js.map
|