/** * Per-kind attention providers. Each kind owns its counts, its bounded page * scan, and its wire item shape; the route layer k-way merges provider pages * on `occurredAt desc` with one resumable cursor per kind. */ import type { WorkItemCommentsStorage } from '../storage/domains/comments/base.js'; import type { FactoryAttentionIdentity, FactoryAttentionKind, FactoryAttentionReceiptRecord, FactoryDeferredDecisionRecord, FactoryDispatchStatus, WorkItemRow, WorkItemsStorage } from '../storage/domains/work-items/base.js'; export type FactoryAttentionView = 'open' | 'unread' | 'archived'; export interface AttentionScope { orgId: string; userId: string; factoryProjectId: string; } export interface AttentionStreamPosition { occurredAt: Date; id: string; } export interface AttentionEntry { occurredAt: Date; /** Position resuming the provider's stream right after this entry. */ resumeCursor: AttentionStreamPosition; item: Record; } export interface AttentionPageArgs { view: FactoryAttentionView; search?: string; before?: AttentionStreamPosition; limit: number; } export interface AttentionPageResult { entries: AttentionEntry[]; /** More rows behind the scan once every returned entry is consumed. */ hasMore: boolean; /** Resume point for a scan-budget stop past the last returned entry. */ continuation?: AttentionStreamPosition; } export interface AttentionCounts { open: number; unread: number; } export interface AttentionLatest { key: string; at: Date; unread: boolean; } export interface AttentionProvider { kind: FactoryAttentionKind; counts(scope: AttentionScope): Promise; latest(scope: AttentionScope): Promise; page(scope: AttentionScope, args: AttentionPageArgs): Promise; markAllRead(scope: AttentionScope, args: { before?: AttentionStreamPosition; now: Date; }): Promise<{ hasMore: boolean; continuation?: AttentionStreamPosition; }>; } export declare const SCAN_PAGE_SIZE = 50; interface ScanBatch { rows: T[]; /** Set only when the page budget stopped a scan that still had rows behind it. */ continuation?: AttentionStreamPosition; } /** * One bounded keyset scan, shared by every provider read. Walks newest-first * from `before` until the stream runs dry or the page budget is spent; the * final batch of a budget stop carries the position to resume from. */ export declare function scanBatches(fetchBatch: (before?: AttentionStreamPosition) => Promise<{ rows: T[]; hasMore: boolean; }>, positionOf: (row: T) => AttentionStreamPosition, before?: AttentionStreamPosition): AsyncGenerator>; /** Fills a page from the scan, stopping at `limit` entries or at the scan budget. */ export declare function collectPage(batches: AsyncGenerator>, limit: number, toEntries: (rows: T[]) => Promise): Promise; /** Marks every scanned batch read, reporting where a budget stop left off. */ export declare function markScanRead(batches: AsyncGenerator>, markBatch: (rows: T[]) => Promise): Promise<{ hasMore: boolean; continuation?: AttentionStreamPosition; }>; export declare function factoryDecisionType(decision: FactoryDeferredDecisionRecord): string; export declare function matchesView(view: FactoryAttentionView, receipt: FactoryAttentionReceiptRecord | undefined): boolean; export declare function workItemBoard(item: WorkItemRow): 'review' | 'work'; /** One deferred-decision status seen as attention: failures and parked runs differ only by this. */ export interface DecisionAttentionSpec { kind: FactoryAttentionKind; status: FactoryDispatchStatus; identity(decision: FactoryDeferredDecisionRecord): FactoryAttentionIdentity; occurredAt(decision: FactoryDeferredDecisionRecord): Date; title(decision: FactoryDeferredDecisionRecord, item: WorkItemRow | undefined): string; detail(decision: FactoryDeferredDecisionRecord, item: WorkItemRow | undefined): string; extra?(decision: FactoryDeferredDecisionRecord): Record; matches(decision: FactoryDeferredDecisionRecord, item: WorkItemRow | undefined, search: string): boolean; } export declare class DecisionAttentionProvider implements AttentionProvider { #private; readonly kind: FactoryAttentionKind; constructor({ workItems }: { workItems: WorkItemsStorage; }, spec: DecisionAttentionSpec); counts(scope: AttentionScope): Promise; latest(scope: AttentionScope): Promise; page(scope: AttentionScope, { view, search, before, limit }: AttentionPageArgs): Promise; markAllRead(scope: AttentionScope, { before, now }: { before?: AttentionStreamPosition; now: Date; }): Promise<{ hasMore: boolean; continuation?: AttentionStreamPosition; }>; } export declare const failedDecisionAttentionSpec: DecisionAttentionSpec; export declare class SupervisorFindingAttentionProvider implements AttentionProvider { #private; readonly kind: "supervisor-finding"; constructor({ workItems }: { workItems: WorkItemsStorage; }); counts(scope: AttentionScope): Promise; latest(scope: AttentionScope): Promise; page(scope: AttentionScope, args: AttentionPageArgs): Promise; markAllRead(scope: AttentionScope, args: { before?: AttentionStreamPosition; now: Date; }): Promise<{ hasMore: boolean; continuation?: AttentionStreamPosition; }>; } export declare class MentionAttentionProvider implements AttentionProvider { #private; readonly kind: "mention"; constructor({ workItems, comments }: { workItems: WorkItemsStorage; comments: WorkItemCommentsStorage; }); /** * Derived from the same bounded scan as `page`, never from raw aggregates: * the badge must count exactly what the views can show, down to stopping at * the same scan budget. Aggregates would count orphan mention rows the list * can never display or clear. */ counts(scope: AttentionScope): Promise; latest(scope: AttentionScope): Promise; page(scope: AttentionScope, { view, search, before, limit }: AttentionPageArgs): Promise; markAllRead(scope: AttentionScope, { before, now }: { before?: AttentionStreamPosition; now: Date; }): Promise<{ hasMore: boolean; continuation?: AttentionStreamPosition; }>; } export declare function receiptScope(scope: AttentionScope): { orgId: string; factoryProjectId: string; userId: string; }; export {}; //# sourceMappingURL=attention-providers.d.ts.map