All files / core/semantic-log/src/service embedding.ts

100% Statements 99/99
100% Branches 16/16
100% Functions 7/7
100% Lines 99/99

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 1001x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 140x 140x 140x 140x 140x 140x 140x 1x 1x 140x 140x 1x 1x 1x 7386x 7386x 2734x 2734x 4652x 4652x 4656x 7386x 4650x 1x 1x 1x 4651x 4651x 4651x 4649x 4649x 4649x 4649x 4649x 4651x 4651x 4651x 4651x 4651x 4651x 4651x 4651x 4651x 1x 1x 1x 8x 8x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 30x 30x 30x 1x 1x 1x 6x 6x 1x  
/**
 * Fingerprint-keyed embedding cache (PRD R3, SC3).
 *
 * This is the mechanism that makes cost scale with *distinct templates* rather
 * than with log volume: a known fingerprint is answered from the map and the
 * provider is never consulted. A provider failure must not be cached, or a
 * transient outage would become a permanent hole — so nothing is stored until
 * the provider has answered, and nothing is counted until it is stored.
 *
 * "At most once per distinct template" holds under concurrency too. A fastify
 * service serves overlapping requests, and a plain check-then-act would let two
 * simultaneous first arrivals both miss and both embed, so the in-flight
 * computation is memoised: concurrent callers for the same fingerprint await
 * one shared promise and the provider is consulted once. That promise is
 * dropped as soon as it settles, so a rejection is a retryable failure rather
 * than a remembered one (the "a failing provider does not poison the cache"
 * property) and no settled promise is retained.
 *
 * Ownership: `vectorFor` returns a vector the caller owns and may mutate
 * freely. The cache never hands out an array that it or another caller can
 * observe mutating — vectors are copied on store and copied again on every
 * read, so a caller's edits cannot corrupt the store or leak to a peer.
 */
 
import type {EmbeddingProvider} from './provider.ts';
 
export class EmbeddingCache {
    private readonly vectors = new Map<string, number[]>();
    /**
     * In-flight computations, keyed by fingerprint. Present only between the
     * start of an embed and its settlement, so it never grows with cache size.
     */
    private readonly pending = new Map<string, Promise<number[]>>();
    private providerCalls = 0;
    private readonly provider: EmbeddingProvider;
 
    constructor(provider: EmbeddingProvider) {
        this.provider = provider;
    }
 
    /** The vector for a fingerprint, computed at most once per distinct template. */
    async vectorFor(fingerprint: string, signature: string): Promise<number[]> {
        const cached = this.vectors.get(fingerprint);
        if (cached) {
            return [...cached];
        }
        // A concurrent caller for this fingerprint is already embedding: share
        // its computation instead of starting a second one.
        const computation = this.pending.get(fingerprint) ?? this.start(fingerprint, signature);
        return [...(await computation)];
    }
 
    /** Begin the one computation for a fingerprint and memoise it until it settles. */
    private start(fingerprint: string, signature: string): Promise<number[]> {
        const computation = this.provider
            .embed(signature)
            .then(vector => {
                // Copy on store: the provider's array is not aliased into the map.
                this.vectors.set(fingerprint, [...vector]);
                this.providerCalls++;
                return vector;
            })
            .finally(() => {
                // The computation is the only one running for this fingerprint
                // (a new one can only start after this runs), so an
                // unconditional delete is safe and cannot drop a successor.
                this.pending.delete(fingerprint);
            });
        this.pending.set(fingerprint, computation);
        return computation;
    }
 
    /** Number of provider invocations — the number the tests assert on. */
    calls(): number {
        return this.providerCalls;
    }
 
    /**
     * The vector already stored under a key, without embedding anything.
     *
     * For ranking over things that were embedded when they arrived — a retained
     * exemplar's vector is stored once, at ingest (D20) — and never as a way to avoid a
     * computation: nothing is computed here, so a key that was never embedded answers
     * `undefined` and is left out of the ranking rather than embedded on the spot. A
     * search that embedded its candidates would cost one provider call per candidate per
     * query, which is the cost model R3 exists to prevent.
     *
     * Copy-on-read, like `vectorFor`: the caller owns what it is handed.
     */
    vectorOf(key: string): number[] | undefined {
        const stored = this.vectors.get(key);
        return stored === undefined ? undefined : [...stored];
    }
 
    /** Estimated cache size, in stored vectors. */
    size(): number {
        return this.vectors.size;
    }
}