---
title: @memhtml/domain
---

`@memhtml/domain`: the pure math under memhtml's retrieval and sleep phases: cosine similarity and pairwise neighbor selection, reciprocal-rank fusion and maximal marginal relevance, PageRank and communities over the memory graph, the frame key, the retention scorer with its triage bands and reprieve gate, confidence decay on a fixed-point grid, the reinforcement cooldown, and the near-duplicate merge guards. Every export is synchronous, deterministic and free of I/O, so a full index rebuild or a sleep re-run reproduces the same numbers, and the package's `dist` imports nothing but `effect`; `tests/layering.test.ts` is the standing proof.

The similarity space starts at `cosine`, which is total and clamped to `[-1, 1]`: a zero-magnitude vector, or one carrying a `NaN` or infinite component, reads `0` rather than `NaN`, because MMR folds a `max` over similarities and `Math.max(penalty, NaN)` disagrees with `NaN > penalty`. The clamp exists because subnormal magnitudes push the unclamped ratio to `1.000000106821595`, mismatched lengths walk the shorter vector, and the argument type is `ArrayLike` so a `Float32Array` decoded off a stored blob is accepted as is, since this function is the body of the `vector_distance_cos` SQL function and runs once per candidate row. `cosineDistance` is `1 - similarity` in `[0, 2]`, the space the vector arm's SQL works in, and `compensatedSum` is Neumaier summation, needed because naive addition of the retention weight profiles returns `0.9999999999999999` for two of the six. `neighbors.ts` is the n-by-n arm of the same space: the SQL boundary is priced per call and re-copies the corpus n times over, so consumers decode each vector once into a `KeyedVector` and `topNeighborPairs` computes every unordered pair's similarity once, offers it to both endpoints' neighborhoods (so `(a, b)` and `(b, a)` can both appear), keeps the per-source top `perSourceK` above `floor`, and orders the result `sim` descending, `src` ascending, `dst` ascending before the `limit`, the three knobs of `NeighborOptions`. Pair similarities are bit-identical to `cosine` because the square roots are hoisted but the operations run in the same order. `rankCandidatePairs` applies the same floor, per-source top-k and ordering to an enumerated `CandidatePair` set, the shape `sharedEntityPairs` in `packages/sleep/src/sql.ts` feeds it with for edge typing's shared-entity candidates, and `float32View` views a row's bytes as a `Float32Array` in place, copying only when the offset is misaligned and returning `undefined` for an empty or ragged blob rather than throwing. Each returned `NeighborPair` carries `src`, `dst` and `sim`.

Fusion is split between the production constants and a reference fold. `ranking.ts` holds `RRF_K` (60, the published default and the literal the retrieval SQL inlines), `MMR_LAMBDA` (0.5) and `REINFORCE_COOLDOWN_S` (900 seconds), plus `rrfContribution`, which returns an `Option` that is `None` for a zero-weight arm or an out-of-range rank so a disabled arm is absent from the fold rather than a neutral term. `rrf.ts` is the reference implementation, not the production path: `packages/index/src/retrieval-sql.ts` inlines the fusion arithmetic in SQL sharing only `RRF_K`, and this fold is what that SQL is checked against. An `ArmHit` carries a 1-based rank within its own arm's list, `rrfScore` is `weight / (rank + k)`, `fuseArms` sums contributions into a `path -> score` map (order-insensitive across arms, and a duplicate path inside one arm accumulates, matching the SQL's `SUM` over a `UNION ALL`), `rankFused` orders best first with ties broken on path ascending, and `fuseLateral` folds a graph walk's best-first order in as one more arm, returning the scores unchanged for an empty order or non-positive weight so the lateral arm is inert when dark; it is defined and tested here and has no production caller yet. `applyMmr` then reorders `MmrCandidate` rows greedily by `lambda * relevance - (1 - lambda) * maxSimilarityToSelected` with no embedding round-trip, since candidates carry their own vectors; at `lambda >= 1` it returns the input order truncated, a vectorless candidate takes penalty 0 because it cannot be shown to duplicate anything, and each round folds only the newest selection into a cached per-candidate max, so the cost is O(k times n) dot products rather than O(k squared times n).

`graph.ts` replaces two networkx calls with deterministic equivalents, because these scores feed the retention `pagerank` and `bridgeImportance` signals and a run-to-run reordering would change which memories are evicted on an unchanged corpus: nodes are sorted before iteration, parallel edges fold to their maximum strength, and label propagation uses sorted order with lexicographic tie-breaking instead of a random seed. `pagerank` is weighted power iteration over `GraphEdge` rows (strength in `[0, 1]`) with `PAGERANK_DAMPING` 0.85 (the networkx default), `PAGERANK_MAX_ITERATIONS` 100 and `PAGERANK_TOLERANCE` 1e-6; passing `seeds` makes it personalized, the form a query-conditioned graph walk would use (the one production caller, the sleep retention pass in `packages/sleep/src/retention.ts`, passes none), seeds naming absent nodes are dropped, and dangling mass is redistributed over the teleport distribution so the scores sum to 1. `labelPropagation` reads the edges as undirected, canonicalizes each community to its smallest member path, and collapses communities below `MIN_COMMUNITY_SIZE` (3) to `undefined` within `LABEL_PROPAGATION_MAX_SWEEPS` (100), since a pair passed off as a community would make every cross-pair edge look like a bridge; `bridgeCounts` then counts each node's edges that cross a community boundary, with a node in no community counting 0 rather than its full degree. `frameKeyOf` is the other structural key: it reads a claim as a frame (subject plus relation up to the last linking token among `of`, `is`, `in`, `to`, `by`, `as`) and a value, so "The capital of India is New Delhi" keys on `the capital of india is` and a later claim writing a different city into the same slot is recognizable without a model or an embedding. It fails closed to `null` unless the frame is at least three tokens and the value is one to six tokens, is case- and whitespace-insensitive, and is a verbatim port from the evaluation harness's consolidate adapter, where the rule was measured against the benchmark's conflict-resolution rows before it was believed; the tokens, thresholds and greedy match are fixed by that measurement, and `tests/frame.test.ts` carries the reference's own cases.

`retention.ts` is the eight-signal scorer ported from the predecessor's `domain/retention.py`. `scoreRetention` takes a `RetentionInput` of raw per-memory quantities (age in days, access count, confidence, PageRank and the run's maximum, bridge count, inbound reinforcement count, word count, and authored contradiction count), normalizes them with `computeSignals` into the `Signals` named by `SIGNAL_NAMES` (`recency`, `accessFrequency`, `confidence`, `pagerank`, `bridgeImportance`, `reinforcementCount`, `contentDensity`, `contestedStatus`), weights them with `compositeScore` under the type's `WeightProfile` from `weightsFor`, and bands the result with `bandFor` into a `RetentionScore`. `WEIGHT_PROFILES` has five typed profiles (`episodic`, `semantic`, `procedural`, `arc`, `error_pattern`) and every other type takes `DEFAULT_WEIGHTS`; each profile's eight weights sum to exactly 1.0 under `profileWeightSum`, which is what keeps the composite in `[0, 1]`, and `SCORE_PRECISION` rounds it to 4 places. `HALF_LIVES_DAYS` gives the recency half-life per type (`episodic` 10, `semantic` 90, `arc` 30, `error_pattern` 14, `procedural` and `task` `null` for no decay) with `DEFAULT_HALF_LIFE_DAYS` 30 for an unlisted type, and `LN2` is `Math.LN2` so the curve is exactly 0.5 at the half-life. The `TRIAGE_ACTIONS` are `keep`, `compress` and `evict`, with `KEEP_THRESHOLD` 0.7 and `EVICT_THRESHOLD` 0.3 and each boundary owned by the lower band, so exactly 0.7 compresses and exactly 0.3 evicts. The reprieve gate sits beside the scorer: `reprieveScore` sums four terms over a `ReprieveInput` with `REPRIEVE_W_IMPORTANCE` 0.4, `REPRIEVE_W_ACCESS` 0.3, `REPRIEVE_W_OUTCOME` 0.2 and `REPRIEVE_W_RECENCY` 0.1, decaying recency at `SALIENCE_DECAY_RATE` 0.01 per hour; the coefficients sum to 1.0 but the score is not convex because the `log1p(accessCount)` term is unbounded, and a negative outcome score contributes exactly 0 so a memory is not punished twice for one bad outcome. `shouldReprieve` is the boolean gate, with no side effect of its own: it is true when the score clears `REPRIEVE_FLOOR` (0.5) and the memory has been reprieved fewer than `MAX_REPRIEVES` (3) times, where 0 is the kill switch that restores pure-age TTL, and the sleep reprieve phase (`packages/sleep/src/phases/reprieve.ts`) is what then extends the memory's `memhtml-valid-until` by `REPRIEVE_DAYS` (14).

`decay.ts` puts confidence decay and the outcome EWMA on a fixed-point grid of `SCALE` 10^4 (`POS_ONE_FP` and `NEG_ONE_FP` are +1.0 and -1.0 on it, `toFp` and `fromFp` convert) so the boundary claims hold exactly rather than within an ulp: `ewmaStepFp` is one step with floor division, `applyOutcomesFp` folds signals with one rounding per step so an N-signal batch equals N single calls, and `applyNegativeHitsFp` decays toward -1.0 over a count of negative corrections and is a no-op for `hits <= 0`. The confidence half is the production path, called by `@memhtml/sleep`'s confidence-decay phase: `decayConfidenceFp` is an EWMA toward the floor followed by a `min` with the previous value, so decay is unconditionally non-increasing and never rehabilitates a claim an operator pushed below the floor, `decayConfidenceNFp` folds it over cycles, and `decayConfidence` and `decayConfidenceN` are the `[0, 1]` float wrappers defaulting to `DEFAULT_CONFIDENCE_DECAY_ALPHA` (0.1, a tenth of the remaining distance per sleep cycle) and `DEFAULT_CONFIDENCE_FLOOR` (0.2, because a decayed-to-zero memory would be indistinguishable from a retracted one). The outcome half is the reference implementation: `packages/index/src/reinforce.ts` computes the EWMA in float SQL with its own `OUTCOME_EWMA_ALPHA` twin of `DEFAULT_EWMA_ALPHA` (0.3), and both packages' tests pin the pair. `CONFIDENCE_COMMIT_DELTA` (0.005) and `isCommittableConfidenceChange` drop a sub-threshold change so the widest commit of a sleep run stays reviewable. `reinforce.ts` is the cooldown predicate whose twin is the salience arm's SQL guard: `shouldBumpAccess` is true when no stamp exists or the elapsed time is at least the cooldown (`>=`, matching the SQL, so a stamp exactly 900 seconds old is bumpable), `partitionByCooldown` splits paths into `bumped` and `cooledDown` in one order-preserving pass, and `REINFORCE_SIGNALS` (`positive`, `negative`, `neutral`) map through `signalValue` to 1, -1 and 0, where neutral bumps the access count without moving the outcome score because being read is evidence of relevance, not correctness. `merge.ts` guards the near-duplicate merge, which keeps the older file and so would fold a newer correction into an older wrong memory if it trusted cosine alone: `negationDivergent`, `numericTokenDivergent` and `variantQualifierDivergent` catch "safe" against "NOT safe", "retry 3" against "retry 13" and "M1" against "M1 Pro". Each memory is a `MergeText` of `gist` (the one-line claim) and `body` (the article), and the predicates read different ones: `mergeVetoReasons` runs the numeric predicate over the gist alone, because a body's citations (a date, a ticket id, an exit code) differ between two honest tellings of one fact, and runs negation and variant over `joinMergeText` of both, because a polarity flip stated only in the body still contradicts. `mergeVetoed` is true when any reason fires. `mergeOutcomes` classifies oriented `MergePair` rows into a `MergeStage` each (`cap`, `threshold`, `vetoed`, `self`, `role-guard`, `committed`) by applying in order the `MAX_MERGE_PAIRS` cap (100 per sleep cycle), the strict `NEAR_DUPLICATE_THRESHOLD` (0.92), the veto when both texts are present, a self-merge check, and an in-batch role guard under which a dropped path is never dropped again or kept, and a keeper is never dropped, so a transitive chain cannot supersede content into a file the same batch archives; a keeper may keep again, so one page absorbs several duplicates in one batch. `mergeCandidates` is the `committed` subset as `MergeDecision` rows. The guards are a post-filter over every proposal including a model's, so binding a model to dedup-merge cannot widen the committable set. `connectedComponents` is the order-invariant union-find whose components are dedup's units of work (the smaller root always wins, so the same edge set yields the same partition in any arrival order), and `excludeSelfSupersede` removes the canonical from a compress batch's member list so the file just folded into is not archived.

## Modules

The package publishes one import path, `@memhtml/domain`, which re-exports the eleven modules below plus a type-only re-export of `InvalidMemory` from `@memhtml/contracts/errors`, so the reference on this page is the whole exported surface.

| Module         | What it holds                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `cosine.ts`    | `cosine` (total, clamped similarity), `cosineDistance` (`1 - similarity`) and `compensatedSum` (Neumaier summation).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `decay.ts`     | The grid (`SCALE`, `POS_ONE_FP`, `NEG_ONE_FP`, `toFp`, `fromFp`), the defaults (`DEFAULT_EWMA_ALPHA`, `DEFAULT_CONFIDENCE_FLOOR`, `DEFAULT_CONFIDENCE_DECAY_ALPHA`), the EWMA steps (`ewmaStepFp`, `applyOutcomesFp`, `applyNegativeHitsFp`), confidence decay (`decayConfidenceFp`, `decayConfidenceNFp`, `decayConfidence`, `decayConfidenceN`), and the commit gate (`CONFIDENCE_COMMIT_DELTA`, `isCommittableConfidenceChange`).                                                                                                                                                                                                                                                                          |
| `frame.ts`     | `frameKeyOf`, the claim's slot key or `null`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `graph.ts`     | `GraphEdge`, the constants (`PAGERANK_DAMPING`, `PAGERANK_MAX_ITERATIONS`, `PAGERANK_TOLERANCE`, `MIN_COMMUNITY_SIZE`, `LABEL_PROPAGATION_MAX_SWEEPS`), `pagerank`, `labelPropagation` and `bridgeCounts`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `merge.ts`     | `NEAR_DUPLICATE_THRESHOLD`, `MAX_MERGE_PAIRS`, the shapes `MergeText`, `MergePair`, `MergeDecision`, `MergeOutcome` and `MergeStage`, `joinMergeText`, the guards (`negationDivergent`, `numericTokenDivergent`, `variantQualifierDivergent`, `mergeVetoReasons`, `mergeVetoed`), `mergeOutcomes`, `mergeCandidates`, `connectedComponents` and `excludeSelfSupersede`.                                                                                                                                                                                                                                                                                                                                       |
| `mmr.ts`       | `MmrCandidate` and `applyMmr`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `neighbors.ts` | The shapes `KeyedVector`, `NeighborPair`, `NeighborOptions` and `CandidatePair`, `float32View`, `topNeighborPairs` and `rankCandidatePairs`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `ranking.ts`   | `RRF_K`, `MMR_LAMBDA`, `REINFORCE_COOLDOWN_S` and `rrfContribution`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `reinforce.ts` | `shouldBumpAccess`, `REINFORCE_SIGNALS`, `ReinforceSignal`, `signalValue` and `partitionByCooldown`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `retention.ts` | The signal and profile types (`SIGNAL_NAMES`, `SignalName`, `Signals`, `WeightProfile`, `TRIAGE_ACTIONS`, `TriageAction`), the tables (`WEIGHT_PROFILES`, `DEFAULT_WEIGHTS`, `HALF_LIVES_DAYS`, `DEFAULT_HALF_LIFE_DAYS`, `LN2`, `KEEP_THRESHOLD`, `EVICT_THRESHOLD`, `SCORE_PRECISION`), the scorer (`RetentionInput`, `RetentionScore`, `weightsFor`, `halfLifeFor`, `profileWeightSum`, `computeSignals`, `compositeScore`, `bandFor`, `scoreRetention`), and the reprieve gate (`REPRIEVE_FLOOR`, `REPRIEVE_DAYS`, `MAX_REPRIEVES`, `SALIENCE_DECAY_RATE`, `REPRIEVE_W_IMPORTANCE`, `REPRIEVE_W_ACCESS`, `REPRIEVE_W_OUTCOME`, `REPRIEVE_W_RECENCY`, `ReprieveInput`, `reprieveScore`, `shouldReprieve`). |
| `rrf.ts`       | `ArmHit`, `rrfScore`, `fuseArms`, `rankFused` and `fuseLateral`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `index.ts`     | The eleven re-exports above plus the type-only re-export of `InvalidMemory` from `@memhtml/contracts/errors`, erased by `verbatimModuleSyntax` so `dist` still imports nothing but `effect`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |

## Interfaces

### ArmHit

One arm's hit. `rank` is a **1-based position within that one arm's own candidate list**,
not a global rank and not a score. That is what lets RRF work across arms whose scores
are incomparable.

#### Properties

##### path

```ts
readonly path: string;
```

##### rank

```ts
readonly rank: number;
```

***

### CandidatePair

One enumerated candidate pair, oriented by the caller. Pairs must be distinct.

#### Properties

##### dst

```ts
readonly dst: string;
```

##### src

```ts
readonly src: string;
```

***

### GraphEdge

One directed weighted edge. `strength` is unitless in `[0, 1]`.

#### Properties

##### dst

```ts
readonly dst: string;
```

##### src

```ts
readonly src: string;
```

##### strength

```ts
readonly strength: number;
```

***

### InvalidMemory

A memory that violates the file format or the type/placement vocabulary.

#### Extends

* `InvalidMemory_base`

#### Properties

##### reason

```ts
readonly reason: string;
```

###### Inherited from

```ts
InvalidMemory_base.reason
```

***

### KeyedVector

One decoded vector under the key the ranking reports. Keys must be unique.

#### Properties

##### key

```ts
readonly key: string;
```

##### vec

```ts
readonly vec: Float32Array;
```

***

### MergeDecision

A committed merge: fold `dropPath` into `keepPath`.

#### Properties

##### dropPath

```ts
readonly dropPath: string;
```

##### keepPath

```ts
readonly keepPath: string;
```

##### similarity

```ts
readonly similarity: number;
```

***

### MergeOutcome

One candidate pair's fate. `committed` is the only stage that produces a [MergeDecision](/api/domain/#mergedecision).

#### Properties

##### pair

```ts
readonly pair: MergePair;
```

##### stage

```ts
readonly stage: MergeStage;
```

***

### MergePair

One oriented candidate pair. `keepPath` is already the canonical (the older file). The
orientation is the caller's, because it depends on commit dates this module does not read.
The texts are optional; when either is absent the divergence guards are skipped.

#### Properties

##### dropPath

```ts
readonly dropPath: string;
```

##### dropText?

```ts
readonly optional dropText?: MergeText;
```

##### keepPath

```ts
readonly keepPath: string;
```

##### keepText?

```ts
readonly optional keepText?: MergeText;
```

##### similarity

```ts
readonly similarity: number;
```

***

### MergeText

The two texts the veto reads for one memory: its one-line claim (the `<mark>` gist) and the whole
article's text. The predicates do not all read the same one, and that is the point of the split.

`numericTokenDivergent` reads the GIST alone. A body is where a memory cites its evidence — a date, a
pull-request or ticket id, an exit code, a cosine — and two honest tellings of one fact almost never
cite the same numbers, so a numeric set-equality over the whole article vetoed nearly every true
duplicate the store proposed (349 of 349 candidates over two consecutive runs, every review task
naming this predicate). A number in the CLAIM is the fact itself: "retry 3" against "retry 13" is two
claims, and the gist is where that difference lives.

`negationDivergent` and `variantQualifierDivergent` read the JOINED text, gist and body together.
Their markers are rare, load-bearing words ("not", "never", "pro", "deprecated") that a body does not
carry incidentally the way it carries a citation, and a polarity flip stated only in the body — the
claim "run the cutover during business hours" over a body that says draining does NOT complete — is a
contradiction the veto has to see. The two all-veto runs named neither predicate once.

#### Properties

##### body

```ts
readonly body: string;
```

##### gist

```ts
readonly gist: string;
```

***

### MmrCandidate

A candidate for diversification. `score` is its fused relevance (unitless, higher is
better); `vector` is its embedding, or `undefined` when the chunk has no vector yet, which
happens for a lexical-floor hit or a deferred embed.

#### Properties

##### path

```ts
readonly path: string;
```

##### score

```ts
readonly score: number;
```

##### vector?

```ts
readonly optional vector?: readonly number[];
```

***

### NeighborOptions

The selection knobs every pair consumer states.

#### Properties

##### floor

```ts
readonly floor: number;
```

Pairs below this similarity never enter ranking.

##### limit

```ts
readonly limit: number;
```

Global cap, applied after the final `sim` DESC, `src` ASC, `dst` ASC ordering.

##### perSourceK

```ts
readonly perSourceK: number;
```

Nearest neighbors kept per `src`, ordered `sim` DESC then `dst` ASC.

***

### NeighborPair

One selected pair. `sim` is cosine similarity, unitless in `[-1, 1]`.

#### Properties

##### dst

```ts
readonly dst: string;
```

##### sim

```ts
readonly sim: number;
```

##### src

```ts
readonly src: string;
```

***

### ReprieveInput

What the reprieve score reads. Units and scopes:

* `importance`: 1-based ordinal in `[1, 10]`, clamped; divided by 10 before use.
* `accessCount`: a quantity, per-memory lifetime, 0-based.
* `outcomeScore`: unitless in `[-1, 1]`. A negative value contributes 0.
* `hoursSinceAccess`: fractional hours, non-negative.

#### Properties

##### accessCount

```ts
readonly accessCount: number;
```

##### decayRate?

```ts
readonly optional decayRate?: number;
```

##### hoursSinceAccess

```ts
readonly hoursSinceAccess: number;
```

##### importance

```ts
readonly importance: number;
```

##### outcomeScore

```ts
readonly outcomeScore: number;
```

***

### RetentionInput

The raw per-memory inputs the SQL phase gathers. Kept raw rather than pre-normalized so
the normalization curves live in exactly one place, [computeSignals](/api/domain/#computesignals).

Units and scopes, per field:

* `memoryType` selects the weight profile and the half-life.
* `ageDays`: fractional days since the memory's own last update. Non-negative.
* `accessCount`: a quantity, per-memory lifetime, 0-based.
* `confidence`: unitless in `[0, 1]`; clamped rather than rejected.
* `graphRank` / `maxGraphRank`: PageRank scores in the same run's scale;
  `maxGraphRank` is that run's maximum, so the ratio is corpus-relative, not absolute.
* `bridgeCount`: a quantity of cross-community memory edges for this memory.
* `reinforcementCount`: a quantity of inbound `supports`/`reinforces` edges.
* `wordCount`: a quantity of words in the memory's body.
* `contradictionCount`: a quantity of **authored** (`derived: false`) contradictions;
  a sleep-mined suspicion is excluded upstream, so a machine guess cannot evict.

#### Properties

##### accessCount

```ts
readonly accessCount: number;
```

##### ageDays

```ts
readonly ageDays: number;
```

##### bridgeCount

```ts
readonly bridgeCount: number;
```

##### confidence

```ts
readonly confidence: number;
```

##### contradictionCount

```ts
readonly contradictionCount: number;
```

##### graphRank

```ts
readonly graphRank: number;
```

##### maxGraphRank

```ts
readonly maxGraphRank: number;
```

##### memoryType

```ts
readonly memoryType: string;
```

##### reinforcementCount

```ts
readonly reinforcementCount: number;
```

##### wordCount

```ts
readonly wordCount: number;
```

***

### RetentionScore

The scoring result: the composite, its band, and the normalized signals behind it.

#### Properties

##### action

```ts
readonly action: "keep" | "compress" | "evict";
```

##### score

```ts
readonly score: number;
```

##### signals

```ts
readonly signals: Signals;
```

## Type Aliases

### MergeStage

```ts
type MergeStage = "cap" | "threshold" | "vetoed" | "self" | "role-guard" | "committed";
```

Where a candidate pair stopped in [mergeOutcomes](/api/domain/#mergeoutcomes), in the filter's own order.

***

### MergeVetoReason

```ts
type MergeVetoReason = "negation" | "numeric" | "variant";
```

The three divergence families, in the veto's own disjunction order.

***

### ReinforceSignal

```ts
type ReinforceSignal = typeof REINFORCE_SIGNALS[number];
```

***

### SignalName

```ts
type SignalName = typeof SIGNAL_NAMES[number];
```

***

### Signals

```ts
type Signals = { readonly [K in SignalName]: number };
```

A full set of normalized signal values, each unitless in `[0, 1]`.

***

### TriageAction

```ts
type TriageAction = typeof TRIAGE_ACTIONS[number];
```

***

### WeightProfile

```ts
type WeightProfile = { readonly [K in SignalName]: number };
```

A weight profile: one coefficient per signal, the eight summing to exactly 1.0.

## Variables

### CONFIDENCE\_COMMIT\_DELTA

```ts
const CONFIDENCE_COMMIT_DELTA: 0.005 = 0.005;
```

The smallest confidence change worth committing. Confidence decay is the widest commit in
a sleep run, one meta line across many files, so a sub-threshold delta is dropped rather
than committed, keeping the night's diff reviewable.

***

### DEFAULT\_CONFIDENCE\_DECAY\_ALPHA

```ts
const DEFAULT_CONFIDENCE_DECAY_ALPHA: 0.1 = 0.1;
```

The per-sleep-cycle confidence decay weight. Gentler than [DEFAULT\_EWMA\_ALPHA](/api/domain/#default_ewma_alpha) on
purpose: confidence should erode over many unreinforced nights rather than collapse in
one, so a single missed reinforcement is forgiving. At 0.1 a claim closes a tenth of its
distance to the floor per cycle.

***

### DEFAULT\_CONFIDENCE\_FLOOR

```ts
const DEFAULT_CONFIDENCE_FLOOR: 0.2 = 0.2;
```

The floor confidence erodes toward but never past. A claim that stops being reinforced
loses weight without vanishing: an old uncorroborated claim is weak evidence, not absent
evidence, and a memory decayed to 0 would be indistinguishable from a retracted one.

***

### DEFAULT\_EWMA\_ALPHA

```ts
const DEFAULT_EWMA_ALPHA: 0.3 = 0.3;
```

The outcome EWMA's weight on an incoming signal. Its production twin is
`OUTCOME_EWMA_ALPHA` in `packages/index/src/reinforce.ts`, the value the reinforce SQL
binds; both packages' tests pin their constant to 0.3 so a fork fails loudly.

***

### DEFAULT\_HALF\_LIFE\_DAYS

```ts
const DEFAULT_HALF_LIFE_DAYS: 30 = 30;
```

Half-life in days for a type with no listed one.

***

### DEFAULT\_WEIGHTS

```ts
const DEFAULT_WEIGHTS: WeightProfile;
```

The profile for a type with no dedicated one. Also sums to exactly 1.0.

***

### EVICT\_THRESHOLD

```ts
const EVICT_THRESHOLD: 0.3 = 0.3;
```

***

### HALF\_LIVES\_DAYS

```ts
const HALF_LIVES_DAYS: Readonly<Record<string, number | null>>;
```

Recency half-lives in days. `null` means no time decay. An unlisted type takes
[DEFAULT\_HALF\_LIFE\_DAYS](/api/domain/#default_half_life_days).

***

### KEEP\_THRESHOLD

```ts
const KEEP_THRESHOLD: 0.7 = 0.7;
```

Band edges. KEEP is `> 0.7`, EVICT is `<= 0.3`, and COMPRESS is the open interval between.
**Each boundary is owned by the lower band**: exactly 0.7 compresses, exactly 0.3
evicts. The three bands partition `[0, 1]` with no gap and no overlap.

***

### LABEL\_PROPAGATION\_MAX\_SWEEPS

```ts
const LABEL_PROPAGATION_MAX_SWEEPS: 100 = 100;
```

Label-propagation sweep cap. Sorted-order propagation settles well inside it.

***

### LN2

```ts
const LN2: number = Math.LN2;
```

The recency decay constant. `Math.LN2` rather than a `0.693` literal, so
`exp(-LN2 * age / halfLife)` is **exactly** 0.5 at `age == halfLife`. The half-life is
then the definition of the curve rather than an approximation of it, and a property test
can assert the equality instead of a tolerance.

***

### MAX\_MERGE\_PAIRS

```ts
const MAX_MERGE_PAIRS: 100 = 100;
```

Merge decisions applied per sleep cycle, so a flood cannot fan the phase out unboundedly.

***

### MAX\_REPRIEVES

```ts
const MAX_REPRIEVES: 3 = 3;
```

Reprieves a memory may earn before it is forced to expire. `0` is the kill switch that
restores pure-age TTL: the floor alone can never force expiry, because the reprieve score
is a sum of non-negative terms and is therefore always above 0.

***

### MIN\_COMMUNITY\_SIZE

```ts
const MIN_COMMUNITY_SIZE: 3 = 3;
```

Communities smaller than this are not communities; compress batching ignores them.

***

### MMR\_LAMBDA

```ts
const MMR_LAMBDA: 0.5 = 0.5;
```

Maximal-marginal-relevance's relevance/diversity split.

***

### NEAR\_DUPLICATE\_THRESHOLD

```ts
const NEAR_DUPLICATE_THRESHOLD: 0.92 = 0.92;
```

Cosine similarity above which two bodies are the same content. Strict.

***

### NEG\_ONE\_FP

```ts
const NEG_ONE_FP: number = -SCALE;
```

***

### PAGERANK\_DAMPING

```ts
const PAGERANK_DAMPING: 0.85 = 0.85;
```

PageRank teleport factor. The networkx default, kept from the predecessor memory system.

***

### PAGERANK\_MAX\_ITERATIONS

```ts
const PAGERANK_MAX_ITERATIONS: 100 = 100;
```

Power-iteration cap. Convergence at this graph's scale is well inside it.

***

### PAGERANK\_TOLERANCE

```ts
const PAGERANK_TOLERANCE: 0.000001 = 1e-6;
```

L1 convergence tolerance per node.

***

### POS\_ONE\_FP

```ts
const POS_ONE_FP: 10000 = SCALE;
```

`+1.0` and `-1.0` on the grid. The outcome EWMA's domain is `[NEG_ONE_FP, POS_ONE_FP]`.

***

### REINFORCE\_COOLDOWN\_S

```ts
const REINFORCE_COOLDOWN_S: 900 = 900;
```

Seconds an access bump waits before it counts again. Stated once here and once
in the salience arm's SQL; a property test pins the two to agree at the boundary.

***

### REINFORCE\_SIGNALS

```ts
const REINFORCE_SIGNALS: readonly ["positive", "negative", "neutral"];
```

The signal a reinforcement carries. `negative` is what drives the outcome EWMA down.

***

### REPRIEVE\_DAYS

```ts
const REPRIEVE_DAYS: 14 = 14;
```

Days a reprieve extends `memhtml-valid-until` by.

***

### REPRIEVE\_FLOOR

```ts
const REPRIEVE_FLOOR: 0.5 = 0.5;
```

The reprieve gate's floor. A TTL-passed memory scoring at least this, under the reprieve
cap, has its `memhtml-valid-until` extended instead of being archived.

***

### REPRIEVE\_W\_ACCESS

```ts
const REPRIEVE_W_ACCESS: 0.3 = 0.3;
```

***

### REPRIEVE\_W\_IMPORTANCE

```ts
const REPRIEVE_W_IMPORTANCE: 0.4 = 0.4;
```

The four reprieve coefficients. They sum to 1.0 but the score is NOT convex.

***

### REPRIEVE\_W\_OUTCOME

```ts
const REPRIEVE_W_OUTCOME: 0.2 = 0.2;
```

***

### REPRIEVE\_W\_RECENCY

```ts
const REPRIEVE_W_RECENCY: 0.1 = 0.1;
```

***

### RRF\_K

```ts
const RRF_K: 60 = 60;
```

Reciprocal-rank-fusion's rank offset. 60 is the published default and the value
the retrieval SQL inlines as a literal, so it lives here once and the assembler
reads it rather than restating it.

***

### SALIENCE\_DECAY\_RATE

```ts
const SALIENCE_DECAY_RATE: 0.01 = 0.01;
```

Salience decay rate per hour for the reprieve score's recency term.

***

### SCALE

```ts
const SCALE: 10000 = 10_000;
```

The fixed-point scale: 10^4, so the grid step is the 4th decimal place.

***

### SCORE\_PRECISION

```ts
const SCORE_PRECISION: 4 = 4;
```

Decimal places the composite is rounded to, matching the predecessor's grain.

***

### SIGNAL\_NAMES

```ts
const SIGNAL_NAMES: readonly ["recency", "accessFrequency", "confidence", "pagerank", "bridgeImportance", "reinforcementCount", "contentDensity", "contestedStatus"];
```

The eight signals, in the fixed order every weight profile keys on.

***

### TRIAGE\_ACTIONS

```ts
const TRIAGE_ACTIONS: readonly ["keep", "compress", "evict"];
```

The triage verdict for one memory.

***

### WEIGHT\_PROFILES

```ts
const WEIGHT_PROFILES: Readonly<Record<string, WeightProfile>>;
```

Per-type weight profiles. Every profile's eight weights sum to exactly 1.0 under
compensated summation, which is what makes the composite a convex combination of eight
`[0, 1]` signals and therefore itself in `[0, 1]`.

The five profiles below are the typed ones; every other memory type falls back to
[DEFAULT\_WEIGHTS](/api/domain/#default_weights). Recency carries the most weight for `episodic` (time is that
type's identity) and zero for `procedural` (a working procedure does not stale).

## Functions

### applyMmr()

```ts
function applyMmr(
   candidates, 
   limit, 
   lambda?
): readonly MmrCandidate[];
```

Greedily reorder candidates by `lambda * relevance - (1 - lambda) * maxSimilarityToSelected`.

`lambda` is unitless in `[0, 1]`: 1 is pure relevance, 0 is pure diversity. At `lambda >= 1`
the function short-circuits and returns the input order truncated, because the penalty term
is multiplied by zero and the greedy pass would otherwise burn O(n^2) cosines to reproduce
the order it was given.

A candidate with no vector takes penalty 0, which is how "unknown similarity" reads here. A
vectorless candidate cannot be shown to duplicate anything, so it is not penalized for it.
Vectorless candidates therefore keep their relative fusion order among themselves rather
than being shuffled by a fabricated distance.

The output is always a duplicate-free subsequence of the input by membership: each candidate
is selected at most once and nothing is invented.

#### Parameters

##### candidates

readonly [`MmrCandidate`](/api/domain/#mmrcandidate)\[]

##### limit

`number`

##### lambda?

`number` = `MMR_LAMBDA`

#### Returns

readonly [`MmrCandidate`](/api/domain/#mmrcandidate)\[]

***

### applyNegativeHitsFp()

```ts
function applyNegativeHitsFp(
   alphaFp, 
   prevFp, 
   hits
): number;
```

Decay a score toward -1.0 over `hits` negative corrections. `hits <= 0` is a no-op, so
re-running a phase over an already-drained watermark writes the same value.

#### Parameters

##### alphaFp

`number`

##### prevFp

`number`

##### hits

`number`

#### Returns

`number`

***

### applyOutcomesFp()

```ts
function applyOutcomesFp(
   alphaFp, 
   prevFp, 
   signalsFp
): number;
```

Fold signals through [ewmaStepFp](/api/domain/#ewmastepfp), one rounding per step. Rounding inside the fold
rather than once at the end is what makes an N-signal batch agree with N single-signal
calls. Sleep may process a memory's corrections in one batch or across several nights,
and both paths must reach the same score.

#### Parameters

##### alphaFp

`number`

##### prevFp

`number`

##### signalsFp

readonly `number`\[]

#### Returns

`number`

***

### bandFor()

```ts
function bandFor(score): "keep" | "compress" | "evict";
```

The band a composite falls in. Boundaries belong to the lower band: 0.7 compresses and
0.3 evicts, so the bands partition `[0, 1]` and no score is ever unbanded.

#### Parameters

##### score

`number`

#### Returns

`"keep"` | `"compress"` | `"evict"`

***

### bridgeCounts()

```ts
function bridgeCounts(
   nodes, 
   edges, 
   communities
): ReadonlyMap<string, number>;
```

Per-node count of incident memory edges that cross a community boundary. This is the
`bridgeImportance` signal's raw input. A node in no community (below the size floor) has no
boundary to cross, so its bridge count is 0 rather than its full degree; counting those
would make every isolated pair look maximally structural.

#### Parameters

##### nodes

readonly `string`\[]

##### edges

readonly [`GraphEdge`](/api/domain/#graphedge)\[]

##### communities

`ReadonlyMap`\<`string`, `string` | `undefined`>

#### Returns

`ReadonlyMap`\<`string`, `number`>

***

### compensatedSum()

```ts
function compensatedSum(values): number;
```

Neumaier compensated summation. Naive left-to-right addition of the retention weight
profiles gives `0.9999999999999999` for two of the six (verified in node), which would
make a convexity assertion fail against a profile that is correct by construction. This
is the `math.fsum` the ported scorer relies on.

#### Parameters

##### values

`Iterable`\<`number`>

#### Returns

`number`

***

### compositeScore()

```ts
function compositeScore(signals, profile): number;
```

The weighted composite, unitless in `[0, 1]`, rounded to [SCORE\_PRECISION](/api/domain/#score_precision) places.
Compensated summation, so the fold does not accumulate the drift that makes a
by-construction-convex profile score marginally above 1.

#### Parameters

##### signals

[`Signals`](/api/domain/#signals-1)

##### profile

[`WeightProfile`](/api/domain/#weightprofile)

#### Returns

`number`

***

### computeSignals()

```ts
function computeSignals(input): Signals;
```

Normalize the raw inputs to their `[0, 1]` signal values.

#### Parameters

##### input

[`RetentionInput`](/api/domain/#retentioninput)

#### Returns

[`Signals`](/api/domain/#signals-1)

***

### connectedComponents()

```ts
function connectedComponents(edges): readonly readonly string[][];
```

Connected components over an undirected edge list, as sorted member lists.

The near-duplicate graph's components are dedup's units of work. A component is what "these
memories might all be one memory" looks like before anything has judged them, and it is the right
unit because near-duplication is transitive in practice: three rewordings of one fact produce
three edges, and folding them one pair at a time would ask the same question three times and could
answer it three different ways.

**The partition is order-INVARIANT, not merely order-stable.** A union always keeps the
lexicographically smaller root, so every set's root is the smallest key it holds no matter which
order the edges arrive in. Members come back sorted, and components come back ordered by root,
which is each component's own smallest member. So the same edge SET produces the same output
whether it arrives mined-first, frame-first, mirrored, or shuffled. That is stronger than sorting
the input would be, and it is why no sort happens here: a caller cannot make this disagree with
itself by changing how it enumerates.

A "larger root wins" or "first root seen wins" rule would break exactly that, because both make
the surviving root a fact about arrival order rather than about the set.

Each pair is normalized before it is unioned, so `(a, b)` and `(b, a)` are one edge. A self-edge
introduces its key and joins nothing. Cost is near-linear in the edge count, and no step here ever
enumerates a pair the caller did not hand over.

#### Parameters

##### edges

readonly readonly \[`string`, `string`]\[]

#### Returns

readonly readonly `string`\[]\[]

***

### cosine()

```ts
function cosine(a, b): number;
```

Cosine similarity of two vectors, unitless and clamped to `[-1, 1]`.

Total: every input maps to a number in range, and `NaN` is never returned. A zero-magnitude
input yields `0`, and so does a vector carrying a `NaN` or infinite component — the two
conditions the arithmetic below cannot answer. `0` is the reading a vectorless candidate
already gets in MMR: no usable direction, so no demonstrable duplication, so no penalty.

Returning `NaN` would be worse than returning a wrong number. MMR folds a `max` over the
similarities to the already-selected set, and the two equivalent ways of writing that fold
disagree on `NaN` — `Math.max(penalty, NaN)` is `NaN` while `NaN > penalty` is `false` — so a
single degenerate embedding would make the result depend on which fold runs, not on the
corpus.

The result is clamped because the unclamped ratio does not stay in range. Verified in node
2026-08-02: two vectors whose squared magnitudes fall into the subnormal range return
`1.000000106821595`. The squares underflow, so `sqrt` divides by a magnitude smaller than
the true one. Similarity is also the input to `1 - similarity` distance and to the MMR
penalty, both of which state a range, so the clamp belongs here rather than at each reader.

Length mismatch is handled by walking the shorter vector rather than failing. The only way
two stored vectors differ in length is a half-migrated embedding model, a condition the
index refuses at the `embed_model` watermark, so it does not reach here.

`ArrayLike` rather than `ReadonlyArray` so a `Float32Array` decoded straight off a stored
blob is an argument: this is the `vector_distance_cos` SQL function's own body, called once
per candidate row, and materialising two 1024-element arrays per call to satisfy a narrower
type would allocate more than the arithmetic costs. Every array caller still fits.

#### Parameters

##### a

`ArrayLike`\<`number`>

##### b

`ArrayLike`\<`number`>

#### Returns

`number`

***

### cosineDistance()

```ts
function cosineDistance(a, b): number;
```

Cosine *distance*, `1 - similarity`, unitless in `[0, 2]`. This is the space the vector
arm's SQL works in (`vector_distance_cos`), so a threshold stated as a similarity is
converted once here rather than inverted at each call site.

#### Parameters

##### a

`ArrayLike`\<`number`>

##### b

`ArrayLike`\<`number`>

#### Returns

`number`

***

### decayConfidence()

```ts
function decayConfidence(
   confidence, 
   alpha?, 
   floor?
): number;
```

One confidence-decay step in the `[0, 1]` float space the HTML `memhtml-confidence` meta uses.
`alpha` is the fraction of the remaining distance to `floor` closed per cycle, both
unitless in `[0, 1]`.

#### Parameters

##### confidence

`number`

##### alpha?

`number` = `DEFAULT_CONFIDENCE_DECAY_ALPHA`

##### floor?

`number` = `DEFAULT_CONFIDENCE_FLOOR`

#### Returns

`number`

***

### decayConfidenceFp()

```ts
function decayConfidenceFp(
   alphaFp, 
   confFp, 
   floorFp
): number;
```

One confidence-decay step on the grid: an EWMA toward the floor, then a `min` with the
previous value.

The `min` is what makes the step *unconditionally* non-increasing. Without it, a claim
already below the floor (one an operator correction pushed down, say) would be pulled
back *up* toward the floor by the same convex combination that erodes a healthy claim, so
decay would rehabilitate a discredited memory. The floor is a resting place for a claim
that stops being reinforced. A refuted claim is not pulled back up to it.

#### Parameters

##### alphaFp

`number`

##### confFp

`number`

##### floorFp

`number`

#### Returns

`number`

***

### decayConfidenceN()

```ts
function decayConfidenceN(
   confidence, 
   cycles, 
   alpha?, 
   floor?
): number;
```

[decayConfidence](/api/domain/#decayconfidence) folded over `cycles` unreinforced sleep cycles.

#### Parameters

##### confidence

`number`

##### cycles

`number`

##### alpha?

`number` = `DEFAULT_CONFIDENCE_DECAY_ALPHA`

##### floor?

`number` = `DEFAULT_CONFIDENCE_FLOOR`

#### Returns

`number`

***

### decayConfidenceNFp()

```ts
function decayConfidenceNFp(
   alphaFp, 
   confFp, 
   floorFp, 
   cycles
): number;
```

Fold `cycles` decay steps. `cycles <= 0` is a no-op, so a phase re-run within one night
writes the same value.

#### Parameters

##### alphaFp

`number`

##### confFp

`number`

##### floorFp

`number`

##### cycles

`number`

#### Returns

`number`

***

### ewmaStepFp()

```ts
function ewmaStepFp(
   alphaFp, 
   prevFp, 
   signalFp
): number;
```

One EWMA step on the grid: `alpha * signal + (1 - alpha) * prev`, divided back down with
**floor** division. Floor rather than round-half-away because it is the one rounding mode
whose N-fold composition is reproducible without a rounding-mode argument, and the
residual drift against an unrounded fold is bounded by one grid step.

For `alphaFp` in `[0, SCALE]` and both values in `[NEG_ONE_FP, POS_ONE_FP]` the result
stays in that domain and lies between `prevFp` and `signalFp`, so a negative signal can
never raise the score.

#### Parameters

##### alphaFp

`number`

##### prevFp

`number`

##### signalFp

`number`

#### Returns

`number`

***

### excludeSelfSupersede()

```ts
function excludeSelfSupersede(canonicalPath, memberPaths): readonly string[];
```

The compress-path exclusion: the members of a batch to supersede and archive, with the
canonical removed and order preserved. When a batch folds into a pre-existing canonical, a
member can *be* that canonical, and archiving it would destroy the file just folded into.

#### Parameters

##### canonicalPath

`string`

##### memberPaths

readonly `string`\[]

#### Returns

readonly `string`\[]

***

### float32View()

```ts
function float32View(bytes): Float32Array<ArrayBufferLike> | undefined;
```

A `Float32Array` over stored bytes, copying only when it must.

`Float32Array` requires a 4-byte-aligned `byteOffset`, and a driver row's `Uint8Array` may be
a view into a pooled buffer at any offset. Viewing in place is the common case and costs
nothing. A misaligned or ragged blob is copied rather than rejected, because the vector arm's
job is to rank and a throw here would fail a whole search over one row.

#### Parameters

##### bytes

`Uint8Array`

#### Returns

`Float32Array`\<`ArrayBufferLike`> | `undefined`

***

### frameKeyOf()

```ts
function frameKeyOf(gist): string | null;
```

The frame key for one claim, or `null` when the claim states no frame+value shape this rule
trusts. Pure and synchronous, with no clock, no randomness, no model, and no I/O, so a full
index rebuild reproduces byte-identical keys by construction.

Case- and whitespace-insensitive, because a restated fact varies in both: `"The Capital of India
is  X"` and `"the capital of India is Y"` collide.

#### Parameters

##### gist

`string`

The claim text. In memhtml, this is a memory's `<mark>` claim.

#### Returns

`string` | `null`

The lowercased frame, or `null` for no-frame-shape (stored as SQL NULL).

***

### fromFp()

```ts
function fromFp(valueFp): number;
```

A grid value back to a float. Exact.

#### Parameters

##### valueFp

`number`

#### Returns

`number`

***

### fuseArms()

```ts
function fuseArms(
   armResults, 
   weights, 
   k?
): ReadonlyMap<string, number>;
```

Fuse per-arm ranked lists into one `path -> score` map by summing contributions.

`armResults[i]` pairs with `weights[i]`; an arm with no weight scores 0 throughout. Addition
commutes, so the result is order-insensitive across arms and the arm registry's order
is a presentation detail rather than a ranking input. A duplicate path inside one arm's
list accumulates, matching the SQL's `SUM` over a `UNION ALL`.

#### Parameters

##### armResults

readonly readonly [`ArmHit`](/api/domain/#armhit)\[]\[]

##### weights

readonly `number`\[]

##### k?

`number` = `RRF_K`

#### Returns

`ReadonlyMap`\<`string`, `number`>

***

### fuseLateral()

```ts
function fuseLateral(
   fused, 
   order, 
   weight, 
   k?
): ReadonlyMap<string, number>;
```

Fold a graph-walk result into existing fused scores, treating the walk's best-first order as
an arm's ranked list. A node absent from `fused` enters at its lateral-only contribution,
which is how a memory the lexical and vector arms both missed can still surface.

An empty `order` or a non-positive `weight` returns the scores unchanged, so the lateral arm
is inert when dark and cannot fail on a cold graph.

#### Parameters

##### fused

`ReadonlyMap`\<`string`, `number`>

##### order

readonly `string`\[]

##### weight

`number`

##### k?

`number` = `RRF_K`

#### Returns

`ReadonlyMap`\<`string`, `number`>

***

### halfLifeFor()

```ts
function halfLifeFor(memoryType): number | null;
```

The recency half-life in days for a memory type; `null` means no time decay.

#### Parameters

##### memoryType

`string`

#### Returns

`number` | `null`

***

### isCommittableConfidenceChange()

```ts
function isCommittableConfidenceChange(before, after): boolean;
```

True when a decayed confidence differs enough from the stored one to be worth a commit.

#### Parameters

##### before

`number`

##### after

`number`

#### Returns

`boolean`

***

### joinMergeText()

```ts
function joinMergeText(text): string;
```

The joined text the negation and variant predicates read: the claim, a newline, the article.

#### Parameters

##### text

[`MergeText`](/api/domain/#mergetext)

#### Returns

`string`

***

### labelPropagation()

```ts
function labelPropagation(
   nodes, 
   edges, 
   options?
): ReadonlyMap<string, string | undefined>;
```

Communities by label propagation over the edge list read as undirected.

Deterministic in place of a seed: nodes start labelled by themselves, sweeps visit them in
sorted order, and a node adopts the highest-weighted neighboring label with the
lexicographically smallest label breaking a tie. The returned labels are canonicalized to
the smallest member path of each community, so the partition itself is reproducible and not
only the grouping.

Communities below `minCommunitySize` collapse to `undefined`: they only feed compress
batching and the bridge count, and a pair passed off as a community would make every
cross-pair edge look like a bridge.

#### Parameters

##### nodes

readonly `string`\[]

##### edges

readonly [`GraphEdge`](/api/domain/#graphedge)\[]

##### options?

###### maxSweeps?

`number`

###### minCommunitySize?

`number`

#### Returns

`ReadonlyMap`\<`string`, `string` | `undefined`>

***

### mergeCandidates()

```ts
function mergeCandidates(pairs, options?): readonly MergeDecision[];
```

Filter oriented candidate pairs into an in-batch-consistent decision list: the `committed` outcomes
of [mergeOutcomes](/api/domain/#mergeoutcomes), in input order, as decisions.

#### Parameters

##### pairs

readonly [`MergePair`](/api/domain/#mergepair)\[]

##### options?

###### maxPairs?

`number`

###### threshold?

`number`

#### Returns

readonly [`MergeDecision`](/api/domain/#mergedecision)\[]

***

### mergeOutcomes()

```ts
function mergeOutcomes(pairs, options?): readonly MergeOutcome[];
```

Classify every oriented candidate pair, applying in order: the per-cycle cap, the strict similarity
threshold, the divergence veto (only when both texts are present), a self-merge check, and the
in-batch role guard. [mergeCandidates](/api/domain/#mergecandidates) is the committed subset; this form exists so a caller
can COUNT the refusals by cause. A phase that reported `candidates - decisions` as "vetoed" told an
operator that a predicate fired on pairs no predicate had seen, which is how two all-refusing runs
read as a divergence problem when half of them were role-guard skips.

The **in-batch role guard** needs the most explanation of the five. Every committed decision fixes
its drop path as DROPPED and its keep path as KEPT for the rest of the batch, and three later uses
are refused, each ruling out a distinct corruption on a transitive chain:

* A path already **dropped** cannot be dropped again (two keepers would each believe they absorbed
  it) nor become a keeper (content folded into a file this same batch archives).
* A path already a **keeper** cannot later be dropped. Given `(gf, a)` then `(b, gf)`, both would
  commit, `gf` absorbs `a` and is then archived into `b`, so `a`'s content is superseded into a file
  that no longer exists. That is the loss the guard exists to prevent.
  Verified against the input `[(gf → a), (b → gf)]`.

A path already a **keeper** MAY keep again. `(k, a)` then `(k, b)` is one page absorbing two
duplicates in one batch, and nothing on that chain is archived twice or archived after being folded
into: `k` survives with two `supersedes` links and both drops point at it. The earlier rule claimed
BOTH roles of a committed pair, which made a model group of N members fold at most ONE of them per
run — every pair a group implies names the same keeper — and reported the other N-2 as vetoes. The
guard's job is the two corruptions above, not a fold budget per keeper; the per-cycle cap is that.

Fixing roles rather than memberships keeps the output a function of input order alone. The first
decision claiming a path in a role wins, matching the SQL result-set iteration upstream, and a later
pair contradicting it is skipped rather than reordering anything.

#### Parameters

##### pairs

readonly [`MergePair`](/api/domain/#mergepair)\[]

##### options?

###### maxPairs?

`number`

###### threshold?

`number`

#### Returns

readonly [`MergeOutcome`](/api/domain/#mergeoutcome)\[]

***

### mergeVetoed()

```ts
function mergeVetoed(textA, textB): boolean;
```

The veto: the disjunction of the three divergence predicates, each over its own text. Symmetric,
pure, total. A vetoed pair is never a duplicate no matter its cosine.

#### Parameters

##### textA

[`MergeText`](/api/domain/#mergetext)

##### textB

[`MergeText`](/api/domain/#mergetext)

#### Returns

`boolean`

***

### mergeVetoReasons()

```ts
function mergeVetoReasons(textA, textB): readonly MergeVetoReason[];
```

Which divergence predicates fire on a pair, each over the text it reads (see [MergeText](/api/domain/#mergetext)):
negation and variant over the joined text, numeric over the gist alone. Symmetric, pure, total.
Empty exactly when [mergeVetoed](/api/domain/#mergevetoed) is false. Exported so a caller that must NAME the refusal
(the review task a vetoed pair becomes, the acceptance arm's per-pair stage) reads the same scoping
the filter applied, instead of re-deriving which text each predicate saw.

#### Parameters

##### textA

[`MergeText`](/api/domain/#mergetext)

##### textB

[`MergeText`](/api/domain/#mergetext)

#### Returns

readonly [`MergeVetoReason`](/api/domain/#mergevetoreason)\[]

***

### negationDivergent()

```ts
function negationDivergent(textA, textB): boolean;
```

True when exactly one side carries a negation marker: "X is safe" against "X is NOT safe".
Symmetric. Both sides negating, or neither, is not divergent.

#### Parameters

##### textA

`string`

##### textB

`string`

#### Returns

`boolean`

***

### numericTokenDivergent()

```ts
function numericTokenDivergent(textA, textB): boolean;
```

True when the two texts carry different numeric tokens: "retry 3 times" against "retry 13
times". Two texts with no numbers at all, or with identical numbers, do not trip it.
Symmetric. [mergeVetoReasons](/api/domain/#mergevetoreasons) hands it the GIST of each memory, not the article — see
[MergeText](/api/domain/#mergetext) for why.

#### Parameters

##### textA

`string`

##### textB

`string`

#### Returns

`boolean`

***

### pagerank()

```ts
function pagerank(
   nodes, 
   edges, 
   options?
): ReadonlyMap<string, number>;
```

PageRank by power iteration, weighted, with a uniform or personalized teleport prior.

`seeds` present makes this personalized PageRank: the teleport distribution is the seed
weights rather than uniform, so the scores describe reachability *from the query's own
hits*. That is what keeps the lateral retrieval arm query-conditioned. A uniform prior
would inject the same hub memories into every query's results regardless of
what was asked. Seeds naming absent nodes are dropped, and an all-absent seed set falls
back to the uniform prior, which the caller reads as "no lateral result".

Dangling nodes (no outbound edges) have their mass redistributed over the teleport
distribution, so the scores sum to 1 rather than leaking.

#### Parameters

##### nodes

readonly `string`\[]

##### edges

readonly [`GraphEdge`](/api/domain/#graphedge)\[]

##### options?

###### damping?

`number`

###### maxIterations?

`number`

###### seeds?

`ReadonlyMap`\<`string`, `number`>

###### tolerance?

`number`

#### Returns

`ReadonlyMap`\<`string`, `number`>

***

### partitionByCooldown()

```ts
function partitionByCooldown(
   entries, 
   now, 
   cooldownSeconds?
): object;
```

Split paths into those whose access stamp may be bumped now and those still cooling down.
One pass, order-preserving, so `memory_reinforce` can answer both lists from one call.

#### Parameters

##### entries

readonly `object`\[]

##### now

`Date`

##### cooldownSeconds?

`number` = `REINFORCE_COOLDOWN_S`

#### Returns

`object`

##### bumped

```ts
readonly bumped: readonly string[];
```

##### cooledDown

```ts
readonly cooledDown: readonly string[];
```

***

### profileWeightSum()

```ts
function profileWeightSum(profile): number;
```

A profile's weight sum under compensated summation. Every shipped profile returns exactly
`1`; this is the convexity fact the composite's `[0, 1]` range rests on.

#### Parameters

##### profile

[`WeightProfile`](/api/domain/#weightprofile)

#### Returns

`number`

***

### rankCandidatePairs()

```ts
function rankCandidatePairs(
   pairs, 
   vectors, 
   options
): readonly NeighborPair[];
```

Rank an ENUMERATED pair set: similarity, floor, per-source top-`k`, final ordering, cap.

This is the shape for a consumer whose candidate pairs come from a selective predicate — the
conflict scan's shared-entity join — rather than from the whole pair space. The predicate runs
BEFORE ranking, exactly as a `WHERE` inside the ranking CTE would, so per-source top-`k` is
computed over passing pairs only. A pair naming a key with no vector contributes nothing, the
same outcome the SQL join's missing-embedding row produces.

#### Parameters

##### pairs

readonly [`CandidatePair`](/api/domain/#candidatepair)\[]

##### vectors

readonly [`KeyedVector`](/api/domain/#keyedvector)\[]

##### options

[`NeighborOptions`](/api/domain/#neighboroptions)

#### Returns

readonly [`NeighborPair`](/api/domain/#neighborpair)\[]

***

### rankFused()

```ts
function rankFused(fused): readonly object[];
```

Fused scores as a ranked list, best first. Ties break on path ascending, so the ordering is
total and reproducible. Two runs over the same corpus produce the same list, which is what
the discrimination gate compares against.

#### Parameters

##### fused

`ReadonlyMap`\<`string`, `number`>

#### Returns

readonly `object`\[]

***

### reprieveScore()

```ts
function reprieveScore(input): number;
```

The four-term reprieve score. Deliberately **not** convex: the `log1p(accessCount)` term
is unbounded, so the score can exceed 1. It is proven only monotone and sign-clamped.

A negative `outcomeScore` contributes exactly 0 and does not subtract, mirroring the
salience arm's `max(coalesce(outcome_score, 0.0), 0.0)`. Without that clamp a memory that
once produced a bad outcome would be punished twice, once by the outcome EWMA that already
lowered its salience, and again here by having its reprieve pushed below the floor.

#### Parameters

##### input

[`ReprieveInput`](/api/domain/#reprieveinput)

#### Returns

`number`

***

### rrfContribution()

```ts
function rrfContribution(rank, weight): Option<number>;
```

One arm's contribution to a fused score. `rank` is 1-based within that arm's
candidate list; `weight` is the arm's configured multiplier.

A zero-weight arm and an out-of-range rank both yield `None` rather than 0, so a
disabled arm is structurally absent from the fold instead of silently adding a
neutral term that later arithmetic could mistake for a real score.

#### Parameters

##### rank

`number`

##### weight

`number`

#### Returns

`Option`\<`number`>

***

### rrfScore()

```ts
function rrfScore(
   rank, 
   weight, 
   k?
): number;
```

One arm's contribution: `weight / (rank + k)`, unitless.

Strictly decreasing in `rank` and linear in `weight`, so a weight-0 arm contributes exactly
0 and is inert in the fold. `k` damps the difference between the top ranks, which is what
stops one arm's confident first place from dominating four other arms' consensus.

#### Parameters

##### rank

`number`

##### weight

`number`

##### k?

`number` = `RRF_K`

#### Returns

`number`

***

### scoreRetention()

```ts
function scoreRetention(input): RetentionScore;
```

Score one memory: normalize, weight by its type's profile, band the composite.

#### Parameters

##### input

[`RetentionInput`](/api/domain/#retentioninput)

#### Returns

[`RetentionScore`](/api/domain/#retentionscore)

***

### shouldBumpAccess()

```ts
function shouldBumpAccess(
   lastAccessedAt, 
   now, 
   cooldownSeconds?
): boolean;
```

The reinforcement cooldown predicate. Its twin is the salience arm's SQL guard:

```sql
WHERE last_accessed_at IS NULL
   OR unixepoch('now') - unixepoch(last_accessed_at) >= 900
```

SQL cannot call this function, so the shared source of truth is the window constant
[REINFORCE\_COOLDOWN\_S](/api/domain/#reinforce_cooldown_s) and the boundary behavior is pinned by a property test on both
sides. The `>=` here matches the SQL's `>=`: a stamp exactly `cooldownSeconds` old **is**
bumpable.

The cooldown exists because `access_count` feeds the salience RRF arm. Without it, replaying
one query ten times would inflate that memory's salience tenfold and let a loop in an agent
rewrite the corpus's ranking.

#### Parameters

##### lastAccessedAt

`Date` | `undefined`

##### now

`Date`

##### cooldownSeconds?

`number` = `REINFORCE_COOLDOWN_S`

#### Returns

`boolean`

***

### shouldReprieve()

```ts
function shouldReprieve(input): boolean;
```

The bounded reprieve gate. A TTL-passed memory is reprieved iff its score clears `floor`
AND it has been reprieved fewer than `maxReprieves` times. `maxReprieves: 0` forces every
TTL-passed memory to expire regardless of score.

#### Parameters

##### input

###### floor?

`number`

###### maxReprieves?

`number`

###### reprieveCount

`number`

###### score

`number`

#### Returns

`boolean`

***

### signalValue()

```ts
function signalValue(signal): number;
```

The outcome-EWMA signal value for a reinforcement, unitless in `[-1, 1]`. `neutral` is 0, so
a neutral reinforcement bumps the access count without moving the outcome score. A memory
being read is evidence of relevance, not of correctness.

#### Parameters

##### signal

`"positive"` | `"negative"` | `"neutral"`

#### Returns

`number`

***

### toFp()

```ts
function toFp(value): number;
```

A float in `[-1, 1]` onto the grid, rounded to the nearest grid point.

#### Parameters

##### value

`number`

#### Returns

`number`

***

### topNeighborPairs()

```ts
function topNeighborPairs(vectors, options): readonly NeighborPair[];
```

Per-source top-`k` nearest neighbors above a similarity floor, over every unordered pair.

Each pair's similarity is computed ONCE and offered to BOTH endpoints' neighborhoods, so the
output can hold `(a, b)` and `(b, a)` — each is a fact about a different source's neighborhood,
and a consumer folding pairs must dedup the mirror itself (dedup-merge does, with its `seen`
set). A floor comparison a NaN similarity cannot pass keeps a vector carrying NaN bytes out of
every neighborhood rather than poisoning an ordering.

#### Parameters

##### vectors

readonly [`KeyedVector`](/api/domain/#keyedvector)\[]

##### options

[`NeighborOptions`](/api/domain/#neighboroptions)

#### Returns

readonly [`NeighborPair`](/api/domain/#neighborpair)\[]

***

### variantQualifierDivergent()

```ts
function variantQualifierDivergent(textA, textB): boolean;
```

True when the two bodies carry different variant qualifiers: "M1" against "M1 Pro".
Symmetric.

#### Parameters

##### textA

`string`

##### textB

`string`

#### Returns

`boolean`

***

### weightsFor()

```ts
function weightsFor(memoryType): WeightProfile;
```

The weight profile for a memory type.

#### Parameters

##### memoryType

`string`

#### Returns

[`WeightProfile`](/api/domain/#weightprofile)