fix(insight): bound graph look-ahead allocation

Clamp cluster look-ahead values at the shared core boundary so invalid inputs cannot create oversized candidate matrices. Add a regression test that keeps oversized values equivalent to the supported maximum.
This commit is contained in:
digua committed 2026-08-28 10:18:54 +08:00
1 parent 4a3a72e736
commit b7c38fb228
2 files changed
+28 -6

No files matched your search

@@ -213,6 +213,15 @@ describe('social analysis', () => {
)
})
it('caps oversized look-ahead values before allocating candidate indexes', () => {
const messages = Array.from({ length: 12 }, (_, index) => ({ senderId: index + 1, ts: 100 + index }))
assert.deepEqual(
accumulateCoOccurrencePairBatches([messages], { lookAhead: 100_000 }),
accumulateCoOccurrencePairBatches([messages], { lookAhead: 10 })
)
})
it('keeps large streamed aggregation identical to the in-memory algorithm', () => {
const senderPattern = [1, 1, 2, 3, 2, 4, 5]
const messages = Array.from({ length: 100_005 }, (_, index) => ({
+19 -6
View File
@@ -309,9 +309,22 @@ export interface SelectedCoOccurrencePairStats extends CoOccurrencePairStats {
}
const DEFAULT_CLUSTER_OPTIONS = { lookAhead: 3, decaySeconds: 120, topEdges: 100 }
const MAX_CLUSTER_LOOK_AHEAD = 10
const DEFAULT_SELECTED_PROXIMITY_MAX_GAP_SECONDS = 1800
const CO_OCCURRENCE_MESSAGE_BATCH_SIZE = 20_000
function resolveClusterOptions(options?: ClusterGraphOptions): Required<ClusterGraphOptions> {
const requestedLookAhead = options?.lookAhead ?? DEFAULT_CLUSTER_OPTIONS.lookAhead
const lookAhead = Number.isFinite(requestedLookAhead)
? Math.min(MAX_CLUSTER_LOOK_AHEAD, Math.max(0, Math.ceil(requestedLookAhead)))
: DEFAULT_CLUSTER_OPTIONS.lookAhead
return {
lookAhead,
decaySeconds: options?.decaySeconds ?? DEFAULT_CLUSTER_OPTIONS.decaySeconds,
topEdges: options?.topEdges ?? DEFAULT_CLUSTER_OPTIONS.topEdges,
}
}
function roundNum(value: number, digits = 4): number {
const factor = 10 ** digits
return Math.round(value * factor) / factor
@@ -325,7 +338,7 @@ export function accumulateCoOccurrencePairs(
messages: CoOccurrenceMessage[],
options?: ClusterGraphOptions
): CoOccurrencePairStats[] {
const opts = { ...DEFAULT_CLUSTER_OPTIONS, ...options }
const opts = resolveClusterOptions(options)
const pairStatsBySource = new Map<number, Map<number, CoOccurrencePairStats>>()
const pairs: CoOccurrencePairStats[] = []
@@ -398,8 +411,8 @@ function accumulateCoOccurrenceColumns(
timestamps: Float64Array,
options?: ClusterGraphOptions
): CoOccurrencePairStats[] {
const opts = { ...DEFAULT_CLUSTER_OPTIONS, ...options }
const lookAhead = Math.max(0, Math.ceil(opts.lookAhead))
const opts = resolveClusterOptions(options)
const lookAhead = opts.lookAhead
const candidateIndexes = buildDistinctSpeakerLookAhead(senderIds.length, (index) => senderIds[index], lookAhead)
const pairStatsBySource = new Map<number, Map<number, CoOccurrencePairStats>>()
const pairs: CoOccurrencePairStats[] = []
@@ -459,8 +472,8 @@ export function accumulateSelectedCoOccurrencePairs(
selectedPairs: Array<readonly [number, number]>,
options?: ClusterGraphOptions & { maxAnchorsPerPair?: number; maxGapSeconds?: number }
): SelectedCoOccurrencePairStats[] {
const opts = { ...DEFAULT_CLUSTER_OPTIONS, ...options }
const lookAhead = Math.max(0, Math.ceil(opts.lookAhead))
const opts = resolveClusterOptions(options)
const lookAhead = opts.lookAhead
const maxAnchorsPerPair = Math.max(0, Math.floor(options?.maxAnchorsPerPair ?? 2))
const maxGapSeconds = Math.max(0, options?.maxGapSeconds ?? DEFAULT_SELECTED_PROXIMITY_MAX_GAP_SECONDS)
const selectedKeys = new Set(selectedPairs.map(([left, right]) => clusterPairKey(left, right)))
@@ -615,7 +628,7 @@ export function getClusterGraph(
filter?: TimeFilter,
options?: ClusterGraphOptions
): ClusterGraphData {
const opts = { ...DEFAULT_CLUSTER_OPTIONS, ...options }
const opts = resolveClusterOptions(options)
const emptyResult: ClusterGraphData = {
nodes: [],
links: [],