diff --git a/packages/context/src/ingest/adapters/historic-sql/buckets.test.ts b/packages/context/src/ingest/adapters/historic-sql/buckets.test.ts new file mode 100644 index 00000000..78dc2859 --- /dev/null +++ b/packages/context/src/ingest/adapters/historic-sql/buckets.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from 'vitest'; +import { + bucketDistinctUsers, + bucketErrorRate, + bucketExecutions, + bucketFrequency, + bucketP95Runtime, + bucketRecency, +} from './buckets.js'; + +describe('historic-sql bucket helpers', () => { + it('uses stable execution buckets', () => { + expect([0, 9, 10, 99, 100, 999, 1000, 4999, 5000, 49999, 50000].map(bucketExecutions)).toEqual([ + '<10', + '<10', + '10-100', + '10-100', + '100-1k', + '100-1k', + '1k-5k', + '1k-5k', + '5k-50k', + '5k-50k', + '>50k', + ]); + }); + + it('uses stable distinct-user, error-rate, runtime, and recency buckets', () => { + expect([0, 1, 2, 5, 6, 10, 11].map(bucketDistinctUsers)).toEqual([ + '0', + '1', + '2-5', + '2-5', + '5-10', + '5-10', + '>10', + ]); + expect([0, 0.01, 0.05, 0.2].map(bucketErrorRate)).toEqual(['none', 'low', 'low', 'high']); + expect([null, 99, 100, 999, 1000, 9999, 10000].map(bucketP95Runtime)).toEqual([ + 'unknown', + '<100ms', + '100ms-1s', + '100ms-1s', + '1s-10s', + '1s-10s', + '>10s', + ]); + expect(bucketRecency('2026-05-11T00:00:00.000Z', new Date('2026-05-11T12:00:00.000Z'))).toBe('current'); + expect(bucketRecency('2026-04-20T00:00:00.000Z', new Date('2026-05-11T12:00:00.000Z'))).toBe('recent'); + expect(bucketRecency('2026-01-01T00:00:00.000Z', new Date('2026-05-11T12:00:00.000Z'))).toBe('stale'); + }); + + it('maps frequency counts to high, mid, and low labels', () => { + expect(bucketFrequency(80, 100)).toBe('high'); + expect(bucketFrequency(20, 100)).toBe('mid'); + expect(bucketFrequency(1, 100)).toBe('low'); + expect(bucketFrequency(0, 0)).toBe('low'); + }); +}); diff --git a/packages/context/src/ingest/adapters/historic-sql/buckets.ts b/packages/context/src/ingest/adapters/historic-sql/buckets.ts new file mode 100644 index 00000000..8777f826 --- /dev/null +++ b/packages/context/src/ingest/adapters/historic-sql/buckets.ts @@ -0,0 +1,49 @@ +export function bucketExecutions(value: number): string { + if (value < 10) return '<10'; + if (value < 100) return '10-100'; + if (value < 1000) return '100-1k'; + if (value < 5000) return '1k-5k'; + if (value < 50000) return '5k-50k'; + return '>50k'; +} + +export function bucketDistinctUsers(value: number): string { + if (value <= 0) return '0'; + if (value === 1) return '1'; + if (value <= 5) return '2-5'; + if (value <= 10) return '5-10'; + return '>10'; +} + +export function bucketErrorRate(value: number): string { + if (value <= 0) return 'none'; + if (value < 0.1) return 'low'; + return 'high'; +} + +export function bucketP95Runtime(value: number | null): string { + if (value === null) return 'unknown'; + if (value < 100) return '<100ms'; + if (value < 1000) return '100ms-1s'; + if (value < 10000) return '1s-10s'; + return '>10s'; +} + +export function bucketRecency(lastSeen: string, now: Date): string { + const parsed = new Date(lastSeen); + if (Number.isNaN(parsed.getTime())) { + return 'unknown'; + } + const ageDays = (now.getTime() - parsed.getTime()) / (24 * 60 * 60 * 1000); + if (ageDays <= 7) return 'current'; + if (ageDays <= 45) return 'recent'; + return 'stale'; +} + +export function bucketFrequency(count: number, total: number): 'high' | 'mid' | 'low' { + if (total <= 0 || count <= 0) return 'low'; + const ratio = count / total; + if (ratio >= 0.5) return 'high'; + if (ratio >= 0.1) return 'mid'; + return 'low'; +} diff --git a/packages/context/src/ingest/index.ts b/packages/context/src/ingest/index.ts index f76befc3..a106bdd8 100644 --- a/packages/context/src/ingest/index.ts +++ b/packages/context/src/ingest/index.ts @@ -317,6 +317,7 @@ export type { export { NOTION_ORG_KNOWLEDGE_WARNING } from './adapters/notion/chunk.js'; export { NotionSourceAdapter, type NotionSourceAdapterDeps } from './adapters/notion/notion.adapter.js'; export { NotionClient, type NotionApi, type NotionBotInfo } from './adapters/notion/notion-client.js'; +export { bucketDistinctUsers, bucketErrorRate, bucketExecutions, bucketP95Runtime, bucketRecency } from './adapters/historic-sql/buckets.js'; export { chunkHistoricSqlStagedDir, describeHistoricSqlScope } from './adapters/historic-sql/chunk.js'; export { detectHistoricSqlStagedDir } from './adapters/historic-sql/detect.js'; export {