/** * LC2-E3 — learned memory-value scorer, wired into the sleep decay pass as a * rescue-only veto (design D1/D2, docs/plans/2026-08-10-lc2-e3-mv-wiring.md). * * computeMvFeatures mirrors benchmarks/memory-value/extract.mjs's * computeFeatures for the 8 live dims the E2 fitter optimized over * (FIT_DIMS) — the only dims MEMORY_VALUE_WEIGHTS carries a weight for. * Any future edit to either side must keep them byte-equivalent; the parity * test in tests/memory-value-wiring.test.ts enforces this. * * scoreEntries mirrors benchmarks/memory-value/evaluate.mjs's per-store * min-max normalization + weighted scorer (no additional orientation * multiply — the frozen weights already encode sign/orientation). * * rescueSet implements D1's rescue-only semantics: a condemned entry is * rescued iff its learned score ranks in the top 30% (RESCUE_BUDGET, the E2 * keep-budget operating point) of its own tenant's non-pinned candidate set * (D2). Deletes(flag-on) subset Deletes(flag-off) by construction — this * function can only ever shrink the condemned set, never grow it. */ import { type MemoryEntry } from './memory.js'; /** The 8 live feature dims (FIT_DIMS) — canonical order for iteration. */ export declare const MV_FEATURE_NAMES: ReadonlyArray; export interface MvFeatureVector { age_days: number; half_life_days: number; strength: number; retrieval_count: number; outcome_positive: number; outcome_negative: number; outcome_ratio: number; content_length: number; } /** * Blind v1 feature dict for one MemoryEntry, restricted to the 8 dims the * frozen weights carry (mirrors extract.mjs's computeFeatures). * * CRITICAL: `strength` is CLOCK-BASIS `calculateStrength(entry, now)` with * NO DecayOptions — that is how the frozen weights' training features were * computed (extract.mjs never passes decayOpts). Passing the production * decay basis (config.decayBasis via consolidate.ts's decayOpts) into this * feature would silently break parity with the frozen weight vector. This * is an intentional divergence from the condemnation TRIGGER in * consolidate.ts, which keeps using decayOpts as today — only the rescue * FEATURE is clock-basis. */ export declare function computeMvFeatures(entry: MemoryEntry, now: Date): MvFeatureVector; export declare function validateWeights(weights?: Readonly>, digest?: string): void; /** * Min-max normalize each of the 8 features over the given entry set * (constant feature -> 0, matching evaluate.mjs), then score = dot(weights, * normalized). The normalization context is exactly the entries passed in — * callers control the bounded scope (D2: per-tenant, non-pinned). * * `weights` defaults to the real frozen singleton; parameterized (like * validateWeights) so callers/tests can score against an explicit vector * without touching the module singleton. * * Review-round F2 (non-finite features): Date.parse on a malformed `created` * string yields NaN, and NaN would silently corrupt every OTHER entry's * min-max in the same group. An entry with ANY non-finite computed feature * is excluded from the normalization context entirely (its raw values never * touch min/max) and always scores -Infinity — the lowest possible score, so * it sorts to the bottom of its tenant deterministically and (via rescueSet's * explicit finite-score guard below) can never be rescued. Conservative * direction: deletes(flag-on) subset deletes(flag-off) still holds. */ export declare function scoreEntries(entries: MemoryEntry[], now: Date, weights?: Readonly>): Map; /** Per-entry rank context within its tenant's non-pinned candidate set — * the score-rank basis both rescueSet's rescue decision and consolidate.ts's * audit-row metadata read from. */ export interface MvRankInfo { tenantId: string; score: number; /** 1-based rank by score DESC within the tenant's non-pinned candidate set. */ rank: number; /** Size of the tenant's non-pinned candidate set (D2). */ totalNonPinned: number; /** ceil(RESCUE_BUDGET * totalNonPinned) — the rescue cutoff; rank <= keepN rescues. */ keepN: number; } /** * Groups non-pinned entries by tenantId (D2), scores + ranks each tenant's * group independently, and returns per-entry rank context for every * non-pinned entry (not just condemned ones) — the shared basis for both * rescueSet's rescue decision and consolidate.ts's audit-row rank context, * so the two never compute the ranking differently. * * `weights`/`digest` default to the real frozen singleton — parameterized * (like validateWeights/scoreEntries) purely for direct unit-testability of * the fail-loud path, never overridden by production callers. */ export declare function rankNonPinnedByTenant(entries: MemoryEntry[], now: Date, weights?: Readonly>, digest?: string): Map; /** * D1 rescue decision: a condemned entry is rescued iff it ranks in the top * 30% of its tenant's non-pinned candidate set by learned score. Returns the * subset of condemnedIds that are rescued — the caller filters commits * (rescued -> survivors) and threads the same set into detectConflicts. * * `weights`/`digest` default to the real frozen singleton; production * callers (consolidate.ts) never pass overrides — the params exist purely so * "flag on + a broken constant throws" is directly testable end-to-end * through this function without mutating the frozen module singleton. * * `precomputedRanks` (round-2 code-review P2-2): when the caller has already * computed the per-tenant ranking (e.g. consolidate.ts needs it separately * for detail/audit rank context), pass it here to skip the internal * rankNonPinnedByTenant call — the whole-store ranking pass then runs * exactly once per sleep instead of twice. Omitted (the default), rescueSet * computes it internally as before — existing callers/tests are unaffected. */ export declare function rescueSet(entries: MemoryEntry[], condemnedIds: Set, now: Date, weights?: Readonly>, digest?: string, precomputedRanks?: Map): Set; //# sourceMappingURL=memory-value.d.ts.map