dna-sdk 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/memory/index.d.ts +1 -0
- package/dist/memory/index.js +1 -0
- package/dist/memory/semantic.d.ts +51 -0
- package/dist/memory/semantic.js +136 -0
- package/package.json +1 -1
package/dist/memory/index.d.ts
CHANGED
package/dist/memory/index.js
CHANGED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { EcphoryScore, EngramRef } from "./ecphory.js";
|
|
2
|
+
import { RecallPolicy } from "./policy.js";
|
|
3
|
+
/**
|
|
4
|
+
* The spec fields that carry an engram's semantic payload — the SAME planes
|
|
5
|
+
* the ecphory content paths score (`area` for Path 1; `summary`/`title`/`body`
|
|
6
|
+
* for Path 2). The cue-side cosine embeds exactly this text, NOT the index's
|
|
7
|
+
* full `documentText` blob: names, dates, affect labels and other metadata
|
|
8
|
+
* strings would dilute the similarity without carrying meaning.
|
|
9
|
+
*/
|
|
10
|
+
export declare const ENGRAM_TEXT_FIELDS: readonly ["area", "title", "summary", "body"];
|
|
11
|
+
/** The text a memory means, for cue-side embedding (see `ENGRAM_TEXT_FIELDS`). */
|
|
12
|
+
export declare function engramText(spec: Record<string, unknown>): string;
|
|
13
|
+
/**
|
|
14
|
+
* Cosine similarity between two vectors. 0.0 when either has no signal
|
|
15
|
+
* (all-zero — the fake embedder's honest "no tokens" vector). Accumulation
|
|
16
|
+
* order is index order, so the result is bit-identical to the Py twin.
|
|
17
|
+
*/
|
|
18
|
+
export declare function cosineSimilarity(a: readonly number[], b: readonly number[]): number;
|
|
19
|
+
/**
|
|
20
|
+
* Per-name cosine against the cue vector, in the shape `scoreEngram`'s Path 3
|
|
21
|
+
* consumes. Non-positive cosines are dropped (no signal, never a penalty).
|
|
22
|
+
* On a duplicate name the FIRST vector wins (deterministic).
|
|
23
|
+
*/
|
|
24
|
+
export declare function semanticScoresFromVectors(names: readonly string[], vectors: readonly (readonly number[])[], queryVector: readonly number[]): Record<string, number>;
|
|
25
|
+
/**
|
|
26
|
+
* The existing ecphory ranking over candidate engrams, with the semantic hook
|
|
27
|
+
* fed. `scoreEngram` (cue = the query) + Semon adjustments, gated by
|
|
28
|
+
* `RecallPolicy.directThreshold`, sorted (score desc, name asc — fully
|
|
29
|
+
* deterministic, Py↔TS identical). Pure — the shared parity core.
|
|
30
|
+
*/
|
|
31
|
+
export declare function ecphoryRank(engrams: readonly EngramRef[], query: string, semanticScores?: Record<string, number>, policy?: RecallPolicy, now?: number): EcphoryScore[];
|
|
32
|
+
/** A recall hit as the fusion sees it — `name` + `score` plus passthrough keys. */
|
|
33
|
+
export interface RecallHit {
|
|
34
|
+
name?: string;
|
|
35
|
+
score?: number;
|
|
36
|
+
[key: string]: unknown;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Fuse the existing recall ranking with the semantic ecphory ranking.
|
|
40
|
+
*
|
|
41
|
+
* `hits` is the recall ranking (best-first); `engrams` are the same candidates
|
|
42
|
+
* as `EngramRef` views. The two rank lists are fused with
|
|
43
|
+
* `reciprocalRankFusion` — a candidate below the ecphory threshold keeps its
|
|
44
|
+
* recall rank (one-list RRF), never disappears. Returns NEW hit objects in
|
|
45
|
+
* fused order, annotated: `score` = fused RRF score, `score_recall` = the
|
|
46
|
+
* pre-fusion score, `rank_recall` / `rank_ecphory` (1-based), `score_ecphory`
|
|
47
|
+
* and `semantic` (cue↔memory cosine) when present. Rankings are keyed by hit
|
|
48
|
+
* `name`; on a duplicate the first (best) hit wins. Pure; the caller owns
|
|
49
|
+
* truncation to top-k.
|
|
50
|
+
*/
|
|
51
|
+
export declare function fuseSemanticRecall(hits: readonly RecallHit[], engrams: readonly EngramRef[], query: string, semanticScores: Record<string, number>, policy?: RecallPolicy, now?: number, rrfK?: number): RecallHit[];
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic recall — embedding similarity fed into the EXISTING ecphory ranking
|
|
3
|
+
* (TS twin of `dna/memory/semantic.py`, s-memory-semantic-recall).
|
|
4
|
+
*
|
|
5
|
+
* Activates the inert semantic hook: `scoreEngram`'s Path 3 blends an embedding
|
|
6
|
+
* cosine into the primary content score (`RecallPolicy.cosineWeight`), and the
|
|
7
|
+
* two rankings — the recall verb's and ecphory's — are fused with the same
|
|
8
|
+
* `reciprocalRankFusion` the search provider uses for its dense + lexical
|
|
9
|
+
* planes. Pure: no kernel, no IO; parity is pinned by the shared
|
|
10
|
+
* `memory-scoring-parity.json` fixture.
|
|
11
|
+
*/
|
|
12
|
+
import { DEFAULT_RRF_K, reciprocalRankFusion } from "../adapters/search/rrf.js";
|
|
13
|
+
import { applySemonAdjustments, scoreEngram } from "./ecphory.js";
|
|
14
|
+
import { DEFAULT_RECALL_POLICY } from "./policy.js";
|
|
15
|
+
/**
|
|
16
|
+
* The spec fields that carry an engram's semantic payload — the SAME planes
|
|
17
|
+
* the ecphory content paths score (`area` for Path 1; `summary`/`title`/`body`
|
|
18
|
+
* for Path 2). The cue-side cosine embeds exactly this text, NOT the index's
|
|
19
|
+
* full `documentText` blob: names, dates, affect labels and other metadata
|
|
20
|
+
* strings would dilute the similarity without carrying meaning.
|
|
21
|
+
*/
|
|
22
|
+
export const ENGRAM_TEXT_FIELDS = ["area", "title", "summary", "body"];
|
|
23
|
+
/** The text a memory means, for cue-side embedding (see `ENGRAM_TEXT_FIELDS`). */
|
|
24
|
+
export function engramText(spec) {
|
|
25
|
+
return ENGRAM_TEXT_FIELDS
|
|
26
|
+
.filter((f) => spec[f])
|
|
27
|
+
.map((f) => String(spec[f]))
|
|
28
|
+
.join(" ")
|
|
29
|
+
.trim();
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Cosine similarity between two vectors. 0.0 when either has no signal
|
|
33
|
+
* (all-zero — the fake embedder's honest "no tokens" vector). Accumulation
|
|
34
|
+
* order is index order, so the result is bit-identical to the Py twin.
|
|
35
|
+
*/
|
|
36
|
+
export function cosineSimilarity(a, b) {
|
|
37
|
+
let dot = 0.0;
|
|
38
|
+
let normA = 0.0;
|
|
39
|
+
let normB = 0.0;
|
|
40
|
+
const n = Math.min(a.length, b.length);
|
|
41
|
+
for (let i = 0; i < n; i++) {
|
|
42
|
+
dot += a[i] * b[i];
|
|
43
|
+
normA += a[i] * a[i];
|
|
44
|
+
normB += b[i] * b[i];
|
|
45
|
+
}
|
|
46
|
+
if (normA === 0.0 || normB === 0.0)
|
|
47
|
+
return 0.0;
|
|
48
|
+
return dot / Math.sqrt(normA) / Math.sqrt(normB);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Per-name cosine against the cue vector, in the shape `scoreEngram`'s Path 3
|
|
52
|
+
* consumes. Non-positive cosines are dropped (no signal, never a penalty).
|
|
53
|
+
* On a duplicate name the FIRST vector wins (deterministic).
|
|
54
|
+
*/
|
|
55
|
+
export function semanticScoresFromVectors(names, vectors, queryVector) {
|
|
56
|
+
const scores = {};
|
|
57
|
+
const n = Math.min(names.length, vectors.length);
|
|
58
|
+
for (let i = 0; i < n; i++) {
|
|
59
|
+
const name = names[i];
|
|
60
|
+
if (name in scores)
|
|
61
|
+
continue;
|
|
62
|
+
const cos = cosineSimilarity(queryVector, vectors[i]);
|
|
63
|
+
if (cos > 0.0)
|
|
64
|
+
scores[name] = cos;
|
|
65
|
+
}
|
|
66
|
+
return scores;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The existing ecphory ranking over candidate engrams, with the semantic hook
|
|
70
|
+
* fed. `scoreEngram` (cue = the query) + Semon adjustments, gated by
|
|
71
|
+
* `RecallPolicy.directThreshold`, sorted (score desc, name asc — fully
|
|
72
|
+
* deterministic, Py↔TS identical). Pure — the shared parity core.
|
|
73
|
+
*/
|
|
74
|
+
export function ecphoryRank(engrams, query, semanticScores, policy = DEFAULT_RECALL_POLICY, now = Date.now()) {
|
|
75
|
+
const cueCtx = { query };
|
|
76
|
+
const ranked = [];
|
|
77
|
+
for (const engram of engrams) {
|
|
78
|
+
let s = scoreEngram(engram, cueCtx, semanticScores, policy);
|
|
79
|
+
s = applySemonAdjustments(s, now, policy);
|
|
80
|
+
if (s.score >= policy.directThreshold)
|
|
81
|
+
ranked.push(s);
|
|
82
|
+
}
|
|
83
|
+
ranked.sort((x, y) => y.score - x.score || (x.engram.name < y.engram.name ? -1 : x.engram.name > y.engram.name ? 1 : 0));
|
|
84
|
+
return ranked;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Fuse the existing recall ranking with the semantic ecphory ranking.
|
|
88
|
+
*
|
|
89
|
+
* `hits` is the recall ranking (best-first); `engrams` are the same candidates
|
|
90
|
+
* as `EngramRef` views. The two rank lists are fused with
|
|
91
|
+
* `reciprocalRankFusion` — a candidate below the ecphory threshold keeps its
|
|
92
|
+
* recall rank (one-list RRF), never disappears. Returns NEW hit objects in
|
|
93
|
+
* fused order, annotated: `score` = fused RRF score, `score_recall` = the
|
|
94
|
+
* pre-fusion score, `rank_recall` / `rank_ecphory` (1-based), `score_ecphory`
|
|
95
|
+
* and `semantic` (cue↔memory cosine) when present. Rankings are keyed by hit
|
|
96
|
+
* `name`; on a duplicate the first (best) hit wins. Pure; the caller owns
|
|
97
|
+
* truncation to top-k.
|
|
98
|
+
*/
|
|
99
|
+
export function fuseSemanticRecall(hits, engrams, query, semanticScores, policy = DEFAULT_RECALL_POLICY, now = Date.now(), rrfK = DEFAULT_RRF_K) {
|
|
100
|
+
if (!hits.length)
|
|
101
|
+
return [];
|
|
102
|
+
const recallOrder = [];
|
|
103
|
+
const firstByName = new Map();
|
|
104
|
+
for (const hit of hits) {
|
|
105
|
+
const name = String(hit.name ?? "");
|
|
106
|
+
if (!name || firstByName.has(name))
|
|
107
|
+
continue;
|
|
108
|
+
firstByName.set(name, hit);
|
|
109
|
+
recallOrder.push(name);
|
|
110
|
+
}
|
|
111
|
+
const directs = ecphoryRank(engrams, query, semanticScores, policy, now);
|
|
112
|
+
const ecphoryOrder = directs.map((s) => s.engram.name);
|
|
113
|
+
const fused = reciprocalRankFusion([recallOrder, ecphoryOrder], rrfK);
|
|
114
|
+
const recallPos = new Map(recallOrder.map((name, i) => [name, i + 1]));
|
|
115
|
+
const ecphoryPos = new Map(ecphoryOrder.map((name, i) => [name, i + 1]));
|
|
116
|
+
const ecphoryScore = new Map(directs.map((s) => [s.engram.name, s.score]));
|
|
117
|
+
const out = [];
|
|
118
|
+
for (const [name, fusedScore] of fused) {
|
|
119
|
+
const src = firstByName.get(name);
|
|
120
|
+
if (!src)
|
|
121
|
+
continue; // ecphory-only name — impossible when engrams ⊆ hits
|
|
122
|
+
const hit = { ...src };
|
|
123
|
+
hit.score_recall = Number(src.score ?? 0.0);
|
|
124
|
+
hit.score = fusedScore;
|
|
125
|
+
hit.rank_recall = recallPos.get(name);
|
|
126
|
+
if (ecphoryPos.has(name)) {
|
|
127
|
+
hit.rank_ecphory = ecphoryPos.get(name);
|
|
128
|
+
hit.score_ecphory = ecphoryScore.get(name);
|
|
129
|
+
}
|
|
130
|
+
const cos = semanticScores[name] ?? 0.0;
|
|
131
|
+
if (cos > 0.0)
|
|
132
|
+
hit.semantic = cos;
|
|
133
|
+
out.push(hit);
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|