@patternbank/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +7978 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +919 -0
- package/dist/index.d.ts +919 -0
- package/dist/index.js +7826 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,919 @@
|
|
|
1
|
+
import { SupabaseClient } from '@supabase/supabase-js';
|
|
2
|
+
|
|
3
|
+
type Difficulty = "Easy" | "Medium" | "Hard";
|
|
4
|
+
type Confidence = 1 | 2 | 3 | 4 | 5;
|
|
5
|
+
type SyncStatus = "idle" | "syncing" | "pending" | "synced" | "offline" | "error";
|
|
6
|
+
type LeetCodeSyncStatus = "idle" | "syncing" | "synced" | "error" | "no_visible_submissions" | "rate_limited";
|
|
7
|
+
type LeetCodeSubmissionStatus = "detected" | "linked_existing" | "pending" | "imported" | "ignored" | "rated";
|
|
8
|
+
interface Problem {
|
|
9
|
+
id: string;
|
|
10
|
+
title: string;
|
|
11
|
+
leetcodeNumber: number | null;
|
|
12
|
+
url: string | null;
|
|
13
|
+
difficulty: Difficulty;
|
|
14
|
+
patterns: string[];
|
|
15
|
+
confidence: Confidence;
|
|
16
|
+
notes: string;
|
|
17
|
+
excludeFromReview: boolean;
|
|
18
|
+
dateAdded: string;
|
|
19
|
+
lastReviewed: string | null;
|
|
20
|
+
nextReviewDate: string;
|
|
21
|
+
fiveStarStreak?: number;
|
|
22
|
+
updatedAt: string;
|
|
23
|
+
}
|
|
24
|
+
interface LeetCodeProblem {
|
|
25
|
+
n: number;
|
|
26
|
+
t: string;
|
|
27
|
+
d: Difficulty;
|
|
28
|
+
s: string;
|
|
29
|
+
}
|
|
30
|
+
interface LeetCodeConnection {
|
|
31
|
+
userId: string;
|
|
32
|
+
leetcodeUsername: string;
|
|
33
|
+
leetcodeDisplayName?: string | null;
|
|
34
|
+
leetcodeAvatarUrl?: string | null;
|
|
35
|
+
leetcodeTotalSolved?: number | null;
|
|
36
|
+
lastSeenAcceptedCount?: number | null;
|
|
37
|
+
lastSyncedAt?: string | null;
|
|
38
|
+
lastSyncStartedAt?: string | null;
|
|
39
|
+
syncStatus: LeetCodeSyncStatus;
|
|
40
|
+
syncError?: string | null;
|
|
41
|
+
createdAt?: string;
|
|
42
|
+
updatedAt?: string;
|
|
43
|
+
}
|
|
44
|
+
interface LeetCodeSubmission {
|
|
45
|
+
id: string;
|
|
46
|
+
userId: string;
|
|
47
|
+
leetcodeUsername: string;
|
|
48
|
+
leetcodeSubmissionId: string;
|
|
49
|
+
titleSlug: string;
|
|
50
|
+
title: string;
|
|
51
|
+
leetcodeNumber: number | null;
|
|
52
|
+
difficulty: Difficulty | null;
|
|
53
|
+
submittedAt: string;
|
|
54
|
+
problemId?: string | null;
|
|
55
|
+
status: LeetCodeSubmissionStatus;
|
|
56
|
+
createdAt?: string;
|
|
57
|
+
updatedAt?: string;
|
|
58
|
+
}
|
|
59
|
+
interface LeetCodeIgnoredImport {
|
|
60
|
+
userId: string;
|
|
61
|
+
titleSlug: string;
|
|
62
|
+
leetcodeNumber: number | null;
|
|
63
|
+
ignoredAt?: string;
|
|
64
|
+
createdAt?: string;
|
|
65
|
+
}
|
|
66
|
+
interface PendingLeetCodeImport {
|
|
67
|
+
submissionDbId: string;
|
|
68
|
+
leetcodeSubmissionId?: string | null;
|
|
69
|
+
titleSlug: string;
|
|
70
|
+
title: string;
|
|
71
|
+
leetcodeNumber: number | null;
|
|
72
|
+
difficulty: Difficulty | null;
|
|
73
|
+
submittedAt: string;
|
|
74
|
+
firstSeenAt?: string;
|
|
75
|
+
suggestedPatterns: string[];
|
|
76
|
+
expired: boolean;
|
|
77
|
+
}
|
|
78
|
+
type TodayLeetCodeItem = (PendingLeetCodeImport & {
|
|
79
|
+
kind: "pending_import";
|
|
80
|
+
status: "detected";
|
|
81
|
+
matchedProblemId: null;
|
|
82
|
+
statusLabel: "Rate to add";
|
|
83
|
+
}) | {
|
|
84
|
+
kind: "linked_existing" | "imported" | "rated";
|
|
85
|
+
submissionDbId: string;
|
|
86
|
+
titleSlug: string;
|
|
87
|
+
title: string;
|
|
88
|
+
leetcodeNumber: number | null;
|
|
89
|
+
difficulty: Difficulty | null;
|
|
90
|
+
submittedAt: string;
|
|
91
|
+
suggestedPatterns: string[];
|
|
92
|
+
matchedProblemId: string | null;
|
|
93
|
+
status: "linked_existing" | "imported" | "rated";
|
|
94
|
+
statusLabel: "In library" | "Review due" | "Imported" | "Rated";
|
|
95
|
+
leetcodeSubmissionId?: string | null;
|
|
96
|
+
confidence: Confidence | null;
|
|
97
|
+
reviewedTodayConfidence: Confidence | null;
|
|
98
|
+
};
|
|
99
|
+
interface LeetCodeSyncSummary {
|
|
100
|
+
fetchedCount?: number;
|
|
101
|
+
insertedCount?: number;
|
|
102
|
+
existingCount?: number;
|
|
103
|
+
linkedExistingCount?: number;
|
|
104
|
+
lastSyncedAt?: string | null;
|
|
105
|
+
throttled?: boolean;
|
|
106
|
+
}
|
|
107
|
+
interface ReviewLogEntry {
|
|
108
|
+
date: string;
|
|
109
|
+
}
|
|
110
|
+
interface ReviewEvent {
|
|
111
|
+
date: string;
|
|
112
|
+
problemId: string;
|
|
113
|
+
confidence: number;
|
|
114
|
+
patterns: string[];
|
|
115
|
+
timestamp: string;
|
|
116
|
+
}
|
|
117
|
+
interface ProblemTombstone {
|
|
118
|
+
problemId: string;
|
|
119
|
+
deletedAt: string;
|
|
120
|
+
}
|
|
121
|
+
interface DataReset {
|
|
122
|
+
resetAt: string;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Preferences shared by every platform. Platforms extend this with their own
|
|
126
|
+
* fields (mobile adds notification settings) and alias it as `Preferences`.
|
|
127
|
+
* `updatedAt` powers newest-wins preference sync (F-6); it is optional because
|
|
128
|
+
* blobs persisted before the field existed lack it — sync treats absence as
|
|
129
|
+
* the epoch so legacy cloud data still wins the first merge.
|
|
130
|
+
*/
|
|
131
|
+
interface CorePreferences {
|
|
132
|
+
dailyReviewGoal: number;
|
|
133
|
+
hidePatternsDuringReview: boolean;
|
|
134
|
+
enabledExtraPatterns: string[];
|
|
135
|
+
updatedAt?: string;
|
|
136
|
+
}
|
|
137
|
+
interface BackupData {
|
|
138
|
+
exportedAt?: string;
|
|
139
|
+
problems: Problem[];
|
|
140
|
+
reviewLog?: ReviewLogEntry[];
|
|
141
|
+
reviewEvents?: ReviewEvent[];
|
|
142
|
+
}
|
|
143
|
+
interface PatternColor {
|
|
144
|
+
text: string;
|
|
145
|
+
bg: string;
|
|
146
|
+
}
|
|
147
|
+
interface ReviewHistoryEntry {
|
|
148
|
+
reviewDate: string;
|
|
149
|
+
newConfidence: number;
|
|
150
|
+
createdAt: string;
|
|
151
|
+
}
|
|
152
|
+
interface ProblemList {
|
|
153
|
+
id: string;
|
|
154
|
+
name: string;
|
|
155
|
+
nameZh: string;
|
|
156
|
+
description: string;
|
|
157
|
+
source: string;
|
|
158
|
+
numbers: number[];
|
|
159
|
+
}
|
|
160
|
+
interface ListSummary {
|
|
161
|
+
id: string;
|
|
162
|
+
name: string;
|
|
163
|
+
nameZh: string;
|
|
164
|
+
description: string;
|
|
165
|
+
total: number;
|
|
166
|
+
existing: number;
|
|
167
|
+
newCount: number;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
declare const CORE_PATTERNS: readonly ["Two Pointers", "Hash Table", "Sliding Window", "Binary Search", "Sorting", "Linked List", "Stack", "Queue", "Tree", "BFS", "DFS", "Heap", "Greedy", "Backtracking", "Graph", "Union Find", "Trie", "DP"];
|
|
171
|
+
declare const EXTRA_PATTERNS: readonly ["Intervals", "Mono Stack", "Prefix Sum", "Bit", "System Design", "OOD"];
|
|
172
|
+
declare function getVisiblePatterns(enabledExtras: string[]): string[];
|
|
173
|
+
declare const DIFFICULTIES: readonly Difficulty[];
|
|
174
|
+
declare const STORAGE_KEY = "patternbank-problems";
|
|
175
|
+
declare const REVIEW_LOG_KEY = "patternbank-review-log";
|
|
176
|
+
declare const REVIEW_EVENTS_KEY = "patternbank-review-events";
|
|
177
|
+
declare const PREFERENCES_KEY = "patternbank-preferences";
|
|
178
|
+
declare const PROBLEM_TOMBSTONES_KEY = "patternbank-problem-tombstones";
|
|
179
|
+
declare const DATA_RESET_KEY = "patternbank-data-reset";
|
|
180
|
+
/**
|
|
181
|
+
* ISO cutoff below which review events were locally pruned (F-3). Merge drops
|
|
182
|
+
* cloud events older than this watermark so pruned history is not resurrected.
|
|
183
|
+
*/
|
|
184
|
+
declare const REVIEW_EVENTS_PRUNED_BEFORE_KEY = "patternbank-review-events-pruned-before";
|
|
185
|
+
declare const DEFAULT_PREFERENCES: CorePreferences;
|
|
186
|
+
|
|
187
|
+
declare function formatLocalDate(date: Date): string;
|
|
188
|
+
declare function parseDateOnly(dateStr: string): {
|
|
189
|
+
year: number;
|
|
190
|
+
month: number;
|
|
191
|
+
day: number;
|
|
192
|
+
};
|
|
193
|
+
declare function dateOnlyToUtcMs(dateStr: string): number;
|
|
194
|
+
declare function todayStr(): string;
|
|
195
|
+
declare function formatDisplayDate(dateStr: string): string;
|
|
196
|
+
declare function utcToLocalDateStr(isoTimestamp: string | null | undefined): string | null;
|
|
197
|
+
declare function addDays(dateStr: string, days: number): string;
|
|
198
|
+
declare function formatRelativeDate(dateStr: string): string;
|
|
199
|
+
declare function generateId(): string;
|
|
200
|
+
/**
|
|
201
|
+
* Parse an ISO timestamp to epoch ms; missing or unparseable values collapse
|
|
202
|
+
* to 0 (the epoch). Sync merges rely on this guard (F-17) so a malformed
|
|
203
|
+
* timestamp deterministically loses to any valid one instead of winning a
|
|
204
|
+
* `NaN` comparison.
|
|
205
|
+
*/
|
|
206
|
+
declare function timestampMs(value: string | null | undefined): number;
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Platform callback hooks (plan: "Adapter contracts").
|
|
210
|
+
* Core never imports posthog/sentry/console; platforms inject these instead.
|
|
211
|
+
*/
|
|
212
|
+
interface CoreHooks {
|
|
213
|
+
analytics?: (event: string, props?: Record<string, unknown>) => void;
|
|
214
|
+
/** Web Today-LC snapshot logging. */
|
|
215
|
+
debugLog?: (message: string, data?: unknown) => void;
|
|
216
|
+
/** F-14 corrupt-row warnings. */
|
|
217
|
+
warn?: (message: string, data?: unknown) => void;
|
|
218
|
+
/** Injectable clock for testability. */
|
|
219
|
+
now?: () => Date;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
declare const INTERVALS: Record<Confidence, number>;
|
|
223
|
+
declare const FIVE_STAR_GRADUATION_INTERVALS: readonly [30, 60, 120, 240, 365];
|
|
224
|
+
declare function getIntervalDays(confidence: Confidence): number;
|
|
225
|
+
declare function getDefaultFiveStarStreak(confidence: number): number;
|
|
226
|
+
declare function getFiveStarGraduationIntervalDays(streak: number): number;
|
|
227
|
+
declare function getPreviousFiveStarStreak(problem: Problem): number;
|
|
228
|
+
declare function getNextFiveStarStreak(problem: Problem, newConfidence: Confidence): number;
|
|
229
|
+
declare function getReviewIntervalDays(problem: Problem, newConfidence: Confidence): number;
|
|
230
|
+
declare function prioritizeProblems(dueProblems: Problem[], limit: number, today?: string): Problem[];
|
|
231
|
+
|
|
232
|
+
interface ProgressConfidenceTint {
|
|
233
|
+
background: string;
|
|
234
|
+
border: string;
|
|
235
|
+
text: string;
|
|
236
|
+
}
|
|
237
|
+
declare const PROGRESS_EMPTY_CONFIDENCE_TINT: ProgressConfidenceTint;
|
|
238
|
+
declare const PROGRESS_HEATMAP_CONFIDENCE_TINTS: readonly ProgressConfidenceTint[];
|
|
239
|
+
declare const PROGRESS_CONFIDENCE_TINTS: readonly ProgressConfidenceTint[];
|
|
240
|
+
declare function getProgressConfidenceTint(avgConfidence: number, count: number): ProgressConfidenceTint;
|
|
241
|
+
declare function getProgressHeatmapTint(avgConfidence: number, count: number): ProgressConfidenceTint;
|
|
242
|
+
|
|
243
|
+
declare function calculateLongestStreak(log: ReviewLogEntry[]): number;
|
|
244
|
+
declare function buildReviewCountMap(reviewEvents: ReviewEvent[], reviewLog: ReviewLogEntry[]): Map<string, number>;
|
|
245
|
+
declare function getWeekStart(dateStr: string): string;
|
|
246
|
+
declare function groupEventsByWeek(events: ReviewEvent[], numWeeks: number, startDate: string): {
|
|
247
|
+
weekStart: string;
|
|
248
|
+
avg: number | null;
|
|
249
|
+
}[];
|
|
250
|
+
declare function getConfidenceDistribution(confidences: number[]): [number, number, number, number, number];
|
|
251
|
+
declare function getTopPatterns(patterns: string[][], limit: number): [string, number][];
|
|
252
|
+
|
|
253
|
+
interface ProblemRow {
|
|
254
|
+
id: string;
|
|
255
|
+
user_id: string;
|
|
256
|
+
title: string;
|
|
257
|
+
leetcode_number: number | null;
|
|
258
|
+
url: string | null;
|
|
259
|
+
difficulty: string;
|
|
260
|
+
patterns: string[];
|
|
261
|
+
confidence: number;
|
|
262
|
+
notes: string;
|
|
263
|
+
exclude_from_review: boolean;
|
|
264
|
+
date_added: string;
|
|
265
|
+
last_reviewed: string | null;
|
|
266
|
+
next_review_date: string;
|
|
267
|
+
five_star_streak?: number | null;
|
|
268
|
+
/** F-14: always a string on write; reads are validated with an epoch fallback. */
|
|
269
|
+
updated_at: string;
|
|
270
|
+
}
|
|
271
|
+
interface ProblemTombstoneRow {
|
|
272
|
+
user_id: string;
|
|
273
|
+
problem_id: string;
|
|
274
|
+
deleted_at: string;
|
|
275
|
+
created_at?: string;
|
|
276
|
+
updated_at?: string;
|
|
277
|
+
}
|
|
278
|
+
interface DataResetRow {
|
|
279
|
+
user_id: string;
|
|
280
|
+
reset_at: string;
|
|
281
|
+
created_at?: string;
|
|
282
|
+
updated_at?: string;
|
|
283
|
+
}
|
|
284
|
+
interface ReviewEventRow {
|
|
285
|
+
problem_id: string;
|
|
286
|
+
new_confidence: number;
|
|
287
|
+
patterns: string[] | null;
|
|
288
|
+
review_date: string;
|
|
289
|
+
created_at: string;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* The subset of preferences that syncs to Supabase. Platforms may extend
|
|
293
|
+
* their local Preferences (mobile adds notification fields) — only these
|
|
294
|
+
* fields travel to the cloud. `updatedAt` powers newest-wins merge (F-6).
|
|
295
|
+
*/
|
|
296
|
+
type CloudPreferences = Pick<CorePreferences, "dailyReviewGoal" | "hidePatternsDuringReview" | "enabledExtraPatterns" | "updatedAt">;
|
|
297
|
+
declare function toCloudPreferences(data: Record<string, unknown>): CloudPreferences;
|
|
298
|
+
/** F-8: deterministic dedupe key shared by logReview and batch review-log inserts. */
|
|
299
|
+
declare function reviewDedupeKey(userId: string, problemId: string, timestamp: string): string;
|
|
300
|
+
declare function toSnakeCase(problem: Problem): Omit<ProblemRow, "user_id">;
|
|
301
|
+
declare function toCamelCase(row: Record<string, unknown>, hooks?: Pick<CoreHooks, "warn">): Problem;
|
|
302
|
+
|
|
303
|
+
/** Compares only the cloud-synced content fields — platform extras (and `updatedAt`) are ignored. */
|
|
304
|
+
declare function preferencesEqual(a: Pick<CorePreferences, "dailyReviewGoal" | "hidePatternsDuringReview" | "enabledExtraPatterns">, b: Pick<CorePreferences, "dailyReviewGoal" | "hidePatternsDuringReview" | "enabledExtraPatterns">): boolean;
|
|
305
|
+
interface MergePreferencesResult<P extends CorePreferences> {
|
|
306
|
+
preferences: P;
|
|
307
|
+
winner: "local" | "cloud";
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Newest-wins preference merge (F-6). Blobs persisted before `updatedAt`
|
|
311
|
+
* existed get the epoch shim — anything stamped beats them, and two unstamped
|
|
312
|
+
* blobs preserve the legacy cloud-wins behavior (ties go to cloud). Platform
|
|
313
|
+
* extras on the local blob (e.g. mobile notification fields) always survive;
|
|
314
|
+
* only the cloud-synced subset is overwritten when cloud wins.
|
|
315
|
+
*/
|
|
316
|
+
declare function mergePreferences<P extends CorePreferences>(local: P, cloud: CloudPreferences | null): MergePreferencesResult<P>;
|
|
317
|
+
|
|
318
|
+
interface BuildNewProblemsOptions {
|
|
319
|
+
today: string;
|
|
320
|
+
now: string;
|
|
321
|
+
dailyGoal: number;
|
|
322
|
+
patternMap: Map<number, string[]> | null;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Filter out LC problems that already exist in the user's library.
|
|
326
|
+
*/
|
|
327
|
+
declare function filterExistingProblems(lcProblems: LeetCodeProblem[], existingProblems: Problem[]): {
|
|
328
|
+
newProblems: LeetCodeProblem[];
|
|
329
|
+
skippedCount: number;
|
|
330
|
+
};
|
|
331
|
+
/**
|
|
332
|
+
* Round-robin interleave problems by difficulty (Easy, Medium, Hard).
|
|
333
|
+
*/
|
|
334
|
+
declare function interleaveByDifficulty(lcProblems: LeetCodeProblem[]): LeetCodeProblem[];
|
|
335
|
+
/**
|
|
336
|
+
* Build full problem objects from LC problem data, distributing review dates.
|
|
337
|
+
*/
|
|
338
|
+
declare function buildNewProblems(lcProblems: LeetCodeProblem[], { today, now, dailyGoal, patternMap }: BuildNewProblemsOptions): Problem[];
|
|
339
|
+
/**
|
|
340
|
+
* Deduplicate problems by leetcodeNumber, keeping the entry with the most recent updatedAt.
|
|
341
|
+
* Problems without a leetcodeNumber are always kept.
|
|
342
|
+
*/
|
|
343
|
+
declare function deduplicateProblems(problems: Problem[]): {
|
|
344
|
+
problems: Problem[];
|
|
345
|
+
removedIds: string[];
|
|
346
|
+
};
|
|
347
|
+
/**
|
|
348
|
+
* Merge imported problems with existing ones by id, then by LeetCode number.
|
|
349
|
+
* When a cross-device LeetCode match has a different local id, keep the local
|
|
350
|
+
* canonical id so existing review history stays attached to the same problem.
|
|
351
|
+
*/
|
|
352
|
+
declare function mergeImportedProblems(existingProblems: Problem[], importedProblems: Problem[]): {
|
|
353
|
+
mergedProblems: Problem[];
|
|
354
|
+
addedCount: number;
|
|
355
|
+
updatedCount: number;
|
|
356
|
+
changedProblems: Problem[];
|
|
357
|
+
importedIdToCanonicalId: Map<string, string>;
|
|
358
|
+
};
|
|
359
|
+
/**
|
|
360
|
+
* Compute review progress toward the daily goal.
|
|
361
|
+
*/
|
|
362
|
+
declare function computeReviewProgress(problems: Problem[], dailyReviewGoal: number): {
|
|
363
|
+
currentReviewed: number;
|
|
364
|
+
totalDue: number;
|
|
365
|
+
effectiveGoal: number;
|
|
366
|
+
};
|
|
367
|
+
/**
|
|
368
|
+
* Build an updated problem after a review with new confidence and dates.
|
|
369
|
+
*/
|
|
370
|
+
declare function buildReviewedProblem(problem: Problem, newConfidence: Confidence): Problem;
|
|
371
|
+
declare function computeNextReviewDate(initialData: Problem | null, newConfidence: Confidence, today: string): string;
|
|
372
|
+
|
|
373
|
+
type ProjectionDistribution = [number, number, number, number, number];
|
|
374
|
+
interface ProjectionSnapshot {
|
|
375
|
+
day: number;
|
|
376
|
+
distribution: ProjectionDistribution;
|
|
377
|
+
}
|
|
378
|
+
type ProjectionDay = ProjectionSnapshot;
|
|
379
|
+
interface ProjectionSeriesOptions {
|
|
380
|
+
startDistribution: ProjectionDistribution;
|
|
381
|
+
dailyGoal: number;
|
|
382
|
+
newPerWeek: number;
|
|
383
|
+
days?: number;
|
|
384
|
+
seed?: number;
|
|
385
|
+
advanceRate?: number;
|
|
386
|
+
}
|
|
387
|
+
declare function simulateProjection(startDistribution: ProjectionDistribution, dailyGoal: number, newPerWeek: number, days?: number, seed?: number): ProjectionSnapshot[];
|
|
388
|
+
declare function simulateProjectionSeries({ startDistribution, dailyGoal, newPerWeek, days, seed, advanceRate, }: ProjectionSeriesOptions): ProjectionDay[];
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Platform storage abstraction (plan: "Adapter contracts").
|
|
392
|
+
* Web wraps localStorage in Promises; mobile passes AsyncStorage directly.
|
|
393
|
+
*/
|
|
394
|
+
interface StorageAdapter {
|
|
395
|
+
getItem(key: string): Promise<string | null>;
|
|
396
|
+
setItem(key: string, value: string): Promise<void>;
|
|
397
|
+
removeItem(key: string): Promise<void>;
|
|
398
|
+
/** AsyncStorage-native batch removal; web loops removeItem instead. */
|
|
399
|
+
multiRemove?(keys: string[]): Promise<void>;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
interface CreateCloudDataOptions {
|
|
403
|
+
/**
|
|
404
|
+
* Platform Supabase client. May be null (missing credentials) — every
|
|
405
|
+
* returned function then no-ops with its `{ data: null, error: null }`-style
|
|
406
|
+
* guard shape, exactly as both platforms behave today.
|
|
407
|
+
*/
|
|
408
|
+
supabase: SupabaseClient | null;
|
|
409
|
+
hooks?: CoreHooks;
|
|
410
|
+
/** Per-operation cloud timeout (F-9). */
|
|
411
|
+
timeoutMs?: number;
|
|
412
|
+
}
|
|
413
|
+
interface CloudData {
|
|
414
|
+
fetchProblems(userId: string): Promise<{
|
|
415
|
+
data: Problem[] | null;
|
|
416
|
+
error: unknown;
|
|
417
|
+
}>;
|
|
418
|
+
upsertProblem(userId: string, problem: Problem): Promise<{
|
|
419
|
+
data: Problem | null;
|
|
420
|
+
error: unknown;
|
|
421
|
+
}>;
|
|
422
|
+
upsertProblems(userId: string, problems: Problem[]): Promise<{
|
|
423
|
+
data: Problem[] | null;
|
|
424
|
+
error: unknown;
|
|
425
|
+
}>;
|
|
426
|
+
deleteProblem(problemId: string): Promise<{
|
|
427
|
+
data: null;
|
|
428
|
+
error: unknown;
|
|
429
|
+
}>;
|
|
430
|
+
deleteProblems(problemIds: string[]): Promise<{
|
|
431
|
+
error: unknown;
|
|
432
|
+
}>;
|
|
433
|
+
fetchProblemTombstones(userId: string): Promise<{
|
|
434
|
+
data: ProblemTombstone[] | null;
|
|
435
|
+
error: unknown;
|
|
436
|
+
}>;
|
|
437
|
+
upsertProblemTombstone(userId: string, tombstone: ProblemTombstone): Promise<{
|
|
438
|
+
error: unknown;
|
|
439
|
+
}>;
|
|
440
|
+
upsertProblemTombstones(userId: string, tombstones: ProblemTombstone[]): Promise<{
|
|
441
|
+
error: unknown;
|
|
442
|
+
}>;
|
|
443
|
+
fetchDataReset(userId: string): Promise<{
|
|
444
|
+
data: DataReset | null;
|
|
445
|
+
error: unknown;
|
|
446
|
+
}>;
|
|
447
|
+
upsertDataReset(userId: string, reset: DataReset): Promise<{
|
|
448
|
+
error: unknown;
|
|
449
|
+
}>;
|
|
450
|
+
fetchReviewLog(userId: string): Promise<{
|
|
451
|
+
data: ReviewLogEntry[] | null;
|
|
452
|
+
error: unknown;
|
|
453
|
+
}>;
|
|
454
|
+
logReview(userId: string, problemId: string, oldConfidence: Confidence, newConfidence: Confidence, patterns?: string[], timestamp?: string): Promise<{
|
|
455
|
+
data: unknown;
|
|
456
|
+
error: unknown;
|
|
457
|
+
}>;
|
|
458
|
+
replaceReviewLog(userId: string, problemId: string, oldConfidence: Confidence, newConfidence: Confidence, patterns: string[], timestamp?: string): Promise<{
|
|
459
|
+
data: unknown;
|
|
460
|
+
error: unknown;
|
|
461
|
+
}>;
|
|
462
|
+
fetchReviewEvents(userId: string, since?: string): Promise<{
|
|
463
|
+
data: ReviewEvent[] | null;
|
|
464
|
+
error: unknown;
|
|
465
|
+
}>;
|
|
466
|
+
batchInsertReviewLogs(userId: string, events: ReviewEvent[]): Promise<{
|
|
467
|
+
error: unknown;
|
|
468
|
+
}>;
|
|
469
|
+
fetchProblemReviewHistory(userId: string, problemId: string): Promise<{
|
|
470
|
+
data: ReviewHistoryEntry[] | null;
|
|
471
|
+
error: unknown;
|
|
472
|
+
}>;
|
|
473
|
+
fetchPreferences(userId: string): Promise<{
|
|
474
|
+
data: CloudPreferences | null;
|
|
475
|
+
error: unknown;
|
|
476
|
+
}>;
|
|
477
|
+
upsertPreferences(userId: string, prefs: CloudPreferences): Promise<{
|
|
478
|
+
data: CloudPreferences | null;
|
|
479
|
+
error: unknown;
|
|
480
|
+
}>;
|
|
481
|
+
submitFeedback(userId: string | null, message: string): Promise<{
|
|
482
|
+
error: unknown;
|
|
483
|
+
}>;
|
|
484
|
+
deleteAllUserProblems(userId: string): Promise<{
|
|
485
|
+
error: unknown;
|
|
486
|
+
}>;
|
|
487
|
+
deleteAllUserReviewLog(userId: string): Promise<{
|
|
488
|
+
error: unknown;
|
|
489
|
+
}>;
|
|
490
|
+
}
|
|
491
|
+
declare function createCloudData(options: CreateCloudDataOptions): CloudData;
|
|
492
|
+
|
|
493
|
+
/** The slice of the cloud-data surface performFullSync drives; the full CloudData satisfies it. */
|
|
494
|
+
type FullSyncCloud = Pick<CloudData, "fetchProblems" | "fetchProblemTombstones" | "fetchDataReset" | "fetchReviewLog" | "fetchReviewEvents" | "fetchPreferences" | "upsertDataReset" | "deleteAllUserProblems" | "deleteAllUserReviewLog" | "upsertProblemTombstones" | "deleteProblems" | "upsertProblems" | "batchInsertReviewLogs" | "upsertPreferences">;
|
|
495
|
+
interface FullSyncLocalState<P extends CorePreferences = CorePreferences> {
|
|
496
|
+
problems: Problem[];
|
|
497
|
+
reviewLog: ReviewLogEntry[];
|
|
498
|
+
reviewEvents: ReviewEvent[];
|
|
499
|
+
preferences: P;
|
|
500
|
+
problemTombstones: ProblemTombstone[];
|
|
501
|
+
dataReset: DataReset | null;
|
|
502
|
+
}
|
|
503
|
+
interface FullSyncDeps<P extends CorePreferences = CorePreferences> {
|
|
504
|
+
userId: string;
|
|
505
|
+
cloud: FullSyncCloud;
|
|
506
|
+
/**
|
|
507
|
+
* Used only for the prune watermark (F-3). Merged data is RETURNED, never
|
|
508
|
+
* written here — the platform persists it, so a failed sync leaves local
|
|
509
|
+
* state untouched (F-5).
|
|
510
|
+
*/
|
|
511
|
+
storage: StorageAdapter;
|
|
512
|
+
local: FullSyncLocalState<P>;
|
|
513
|
+
/** Local review-event retention: null never prunes (web); mobile passes 180. */
|
|
514
|
+
eventRetentionDays: number | null;
|
|
515
|
+
hooks?: CoreHooks;
|
|
516
|
+
}
|
|
517
|
+
interface FullSyncSuccess<P extends CorePreferences = CorePreferences> extends FullSyncLocalState<P> {
|
|
518
|
+
status: "success";
|
|
519
|
+
hasChanges: boolean;
|
|
520
|
+
}
|
|
521
|
+
interface FullSyncFailure {
|
|
522
|
+
status: "error";
|
|
523
|
+
error: unknown;
|
|
524
|
+
}
|
|
525
|
+
type FullSyncResult<P extends CorePreferences = CorePreferences> = FullSyncSuccess<P> | FullSyncFailure;
|
|
526
|
+
/**
|
|
527
|
+
* Full sign-in sync: pull everything, merge with the provided local snapshot,
|
|
528
|
+
* repair the cloud, and return the merged state for the platform to persist.
|
|
529
|
+
*
|
|
530
|
+
* Fail-closed (F-5): any critical fetch or any cloud write failure aborts with
|
|
531
|
+
* `{ status: "error" }` and no partial local writes. Tolerated degradations
|
|
532
|
+
* (kept from both platforms): review-event and preference fetch failures skip
|
|
533
|
+
* their backfill/merge instead of aborting.
|
|
534
|
+
*/
|
|
535
|
+
declare function performFullSync<P extends CorePreferences = CorePreferences>(deps: FullSyncDeps<P>): Promise<FullSyncResult<P>>;
|
|
536
|
+
|
|
537
|
+
interface MergeProblemsResult {
|
|
538
|
+
problems: Problem[];
|
|
539
|
+
cloudAdded: number;
|
|
540
|
+
cloudWon: number;
|
|
541
|
+
}
|
|
542
|
+
declare function mergeProblems(localProblems: Problem[], cloudProblems: Problem[]): MergeProblemsResult;
|
|
543
|
+
interface MergeProblemTombstonesResult {
|
|
544
|
+
tombstones: ProblemTombstone[];
|
|
545
|
+
addedFromCloud: number;
|
|
546
|
+
}
|
|
547
|
+
declare function mergeProblemTombstones(localTombstones: ProblemTombstone[], cloudTombstones: ProblemTombstone[]): MergeProblemTombstonesResult;
|
|
548
|
+
/**
|
|
549
|
+
* F-25: last-write-wins — a tombstone only kills rows not updated after it, so
|
|
550
|
+
* a problem restored from a backup (re-stamped past its tombstone) survives
|
|
551
|
+
* everywhere while stale pre-delete copies on other devices still die. Ties go
|
|
552
|
+
* to the delete.
|
|
553
|
+
*/
|
|
554
|
+
declare function filterTombstonedProblems(problems: Problem[], tombstones: ProblemTombstone[]): Problem[];
|
|
555
|
+
interface MergeReviewLogResult {
|
|
556
|
+
log: ReviewLogEntry[];
|
|
557
|
+
addedFromCloud: number;
|
|
558
|
+
}
|
|
559
|
+
declare function mergeReviewLog(localLog: ReviewLogEntry[], cloudLog: ReviewLogEntry[]): MergeReviewLogResult;
|
|
560
|
+
declare function dataResetTime(reset: DataReset | null | undefined): number;
|
|
561
|
+
declare function compareDataResets(localReset: DataReset | null, cloudReset: DataReset | null): "local" | "cloud" | "none";
|
|
562
|
+
declare function newestDataReset(localReset: DataReset | null, cloudReset: DataReset | null): DataReset | null;
|
|
563
|
+
declare function filterProblemsAfterDataReset(problems: Problem[], reset: DataReset | null): {
|
|
564
|
+
problems: Problem[];
|
|
565
|
+
removedIds: string[];
|
|
566
|
+
};
|
|
567
|
+
declare function filterReviewEventsAfterDataReset(events: ReviewEvent[], reset: DataReset | null): ReviewEvent[];
|
|
568
|
+
/**
|
|
569
|
+
* F-20: review-log entries are date-only, so compare against the LOCAL date of
|
|
570
|
+
* the reset. `>=` keeps legitimate same-day post-clear reviews; the plain
|
|
571
|
+
* date-string compare avoids rebuilding the log from events (which would churn
|
|
572
|
+
* under mobile's 180-day event retention).
|
|
573
|
+
*/
|
|
574
|
+
declare function filterReviewLogAfterDataReset(log: ReviewLogEntry[], reset: DataReset | null): ReviewLogEntry[];
|
|
575
|
+
declare function filterTombstonesAfterDataReset(tombstones: ProblemTombstone[], reset: DataReset | null): ProblemTombstone[];
|
|
576
|
+
declare function reviewLogFromEvents(events: ReviewEvent[]): ReviewLogEntry[];
|
|
577
|
+
/** F-7: review events whose problem did not survive the merge are orphans. */
|
|
578
|
+
declare function filterReviewEventsToProblems(events: ReviewEvent[], problemIds: Set<string>): ReviewEvent[];
|
|
579
|
+
|
|
580
|
+
declare function reviewEventKey(event: ReviewEvent): string;
|
|
581
|
+
/**
|
|
582
|
+
* Two events are the same review when their keys match exactly, or when they
|
|
583
|
+
* hit the same problem within 5 seconds ON THE SAME calendar date (legacy
|
|
584
|
+
* timestamp mismatch between platforms). The date gate is canonical core
|
|
585
|
+
* behavior: near-midnight events on different dates are distinct streak days
|
|
586
|
+
* and must both survive a merge.
|
|
587
|
+
*/
|
|
588
|
+
declare function reviewEventsMatch(a: ReviewEvent, b: ReviewEvent): boolean;
|
|
589
|
+
interface MergeReviewEventsOptions {
|
|
590
|
+
/**
|
|
591
|
+
* F-3 prune watermark (ISO): cloud events strictly older than this were
|
|
592
|
+
* pruned locally on purpose and are dropped instead of resurrected.
|
|
593
|
+
*/
|
|
594
|
+
prunedBefore?: string | null;
|
|
595
|
+
}
|
|
596
|
+
interface MergeReviewEventsResult {
|
|
597
|
+
events: ReviewEvent[];
|
|
598
|
+
addedFromCloud: number;
|
|
599
|
+
localOnlyEvents: ReviewEvent[];
|
|
600
|
+
}
|
|
601
|
+
declare function mergeReviewEvents(localEvents: ReviewEvent[], cloudEvents: ReviewEvent[], options?: MergeReviewEventsOptions): MergeReviewEventsResult;
|
|
602
|
+
|
|
603
|
+
declare const CLOUD_OPERATION_TIMEOUT_MS = 15000;
|
|
604
|
+
declare const FULL_SYNC_TIMEOUT_MS = 25000;
|
|
605
|
+
declare const LEETCODE_ACTIVITY_TIMEOUT_MS = 45000;
|
|
606
|
+
declare class SyncTimeoutError extends Error {
|
|
607
|
+
operation: string;
|
|
608
|
+
timeoutMs: number;
|
|
609
|
+
constructor(operation: string, timeoutMs: number);
|
|
610
|
+
}
|
|
611
|
+
declare function isSyncTimeoutError(error: unknown): error is SyncTimeoutError;
|
|
612
|
+
declare function isLikelyOfflineError(error: unknown): boolean;
|
|
613
|
+
declare function withTimeout<T>(promise: PromiseLike<T>, timeoutMs: number, operation: string): Promise<T>;
|
|
614
|
+
declare function withCloudOperationTimeout<T>(operation: string, promise: PromiseLike<T>): Promise<T>;
|
|
615
|
+
|
|
616
|
+
interface TodayReviewState {
|
|
617
|
+
todaysReviews: Problem[];
|
|
618
|
+
totalDueCount: number;
|
|
619
|
+
reviewedToday: number;
|
|
620
|
+
effectiveGoal: number;
|
|
621
|
+
remainingSlots: number;
|
|
622
|
+
}
|
|
623
|
+
interface DoneTodayFeedItem {
|
|
624
|
+
id: string;
|
|
625
|
+
problemId: string;
|
|
626
|
+
title: string;
|
|
627
|
+
leetcodeNumber: number | null;
|
|
628
|
+
difficulty: Difficulty;
|
|
629
|
+
confidence: Confidence;
|
|
630
|
+
timestamp: string;
|
|
631
|
+
}
|
|
632
|
+
type TodayActivityFeedItem = {
|
|
633
|
+
type: "pb_review";
|
|
634
|
+
id: string;
|
|
635
|
+
problemId: string;
|
|
636
|
+
title: string;
|
|
637
|
+
leetcodeNumber: number | null;
|
|
638
|
+
difficulty: Difficulty;
|
|
639
|
+
confidence: Confidence;
|
|
640
|
+
timestamp: string;
|
|
641
|
+
} | {
|
|
642
|
+
type: "leetcode_solve";
|
|
643
|
+
id: string;
|
|
644
|
+
submissionDbId: string;
|
|
645
|
+
problemId: string;
|
|
646
|
+
title: string;
|
|
647
|
+
leetcodeNumber: number | null;
|
|
648
|
+
difficulty: Difficulty;
|
|
649
|
+
submittedAt: string;
|
|
650
|
+
status: "linked_existing" | "imported" | "rated";
|
|
651
|
+
reviewDue: boolean;
|
|
652
|
+
canRate: boolean;
|
|
653
|
+
};
|
|
654
|
+
interface SolvedOnLeetCodeTodayIndex {
|
|
655
|
+
problemIds: Set<string>;
|
|
656
|
+
leetcodeNumbers: Set<number>;
|
|
657
|
+
}
|
|
658
|
+
interface ExitingTodayLeetCodeItem {
|
|
659
|
+
key: string;
|
|
660
|
+
item: TodayLeetCodeItem;
|
|
661
|
+
}
|
|
662
|
+
declare function buildTodayReviewState(problems: Problem[], dailyGoal: number, today?: string): TodayReviewState;
|
|
663
|
+
declare function buildDoneTodayFeedItems(problems: Problem[], reviewEvents: ReviewEvent[], today?: string): DoneTodayFeedItem[];
|
|
664
|
+
declare function buildSolvedOnLeetCodeTodayIndex(leetcodeSubmissions: LeetCodeSubmission[], today?: string): SolvedOnLeetCodeTodayIndex;
|
|
665
|
+
declare function buildTodayLeetCodeItemKey(item: TodayLeetCodeItem): string;
|
|
666
|
+
declare function buildRemovedTodayLeetCodeItems({ previousItems, currentItems, exitingKeys, }: {
|
|
667
|
+
previousItems: TodayLeetCodeItem[];
|
|
668
|
+
currentItems: TodayLeetCodeItem[];
|
|
669
|
+
exitingKeys: Set<string>;
|
|
670
|
+
}): ExitingTodayLeetCodeItem[];
|
|
671
|
+
declare function buildTodayActivityFeedItems({ problems, reviewEvents, leetcodeSubmissions, today, }: {
|
|
672
|
+
problems: Problem[];
|
|
673
|
+
reviewEvents: ReviewEvent[];
|
|
674
|
+
leetcodeSubmissions: LeetCodeSubmission[];
|
|
675
|
+
today?: string;
|
|
676
|
+
}): TodayActivityFeedItem[];
|
|
677
|
+
|
|
678
|
+
declare function calculateStreak(log: ReviewLogEntry[]): number;
|
|
679
|
+
declare function countReviewedToday(problems: Problem[]): number;
|
|
680
|
+
interface PruneOldEventsOptions {
|
|
681
|
+
/** Days of history to keep; null/undefined disables pruning entirely (web). */
|
|
682
|
+
retentionDays?: number | null;
|
|
683
|
+
today?: string;
|
|
684
|
+
}
|
|
685
|
+
interface PruneOldEventsResult {
|
|
686
|
+
kept: ReviewEvent[];
|
|
687
|
+
/**
|
|
688
|
+
* F-3 watermark: the YYYY-MM-DD cutoff (events with date >= cutoff are kept),
|
|
689
|
+
* or null when pruning is disabled. Platforms persist this under
|
|
690
|
+
* REVIEW_EVENTS_PRUNED_BEFORE_KEY so merges can drop pre-cutoff cloud events.
|
|
691
|
+
*/
|
|
692
|
+
cutoffIso: string | null;
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Pure prune (F-3): no persistence side effects; callers save `kept` and the
|
|
696
|
+
* watermark explicitly as a post-sync maintenance step, never on load.
|
|
697
|
+
*/
|
|
698
|
+
declare function pruneOldEvents(events: ReviewEvent[], { retentionDays, today }: PruneOldEventsOptions): PruneOldEventsResult;
|
|
699
|
+
|
|
700
|
+
declare const LEETCODE_RECENT_ACTIVITY_LIMIT = 100;
|
|
701
|
+
interface LeetCodeConnectionRow {
|
|
702
|
+
user_id: string;
|
|
703
|
+
leetcode_username: string;
|
|
704
|
+
leetcode_display_name?: string | null;
|
|
705
|
+
leetcode_avatar_url?: string | null;
|
|
706
|
+
leetcode_total_solved?: number | null;
|
|
707
|
+
last_seen_accepted_count?: number | null;
|
|
708
|
+
last_synced_at?: string | null;
|
|
709
|
+
last_sync_started_at?: string | null;
|
|
710
|
+
sync_status: string;
|
|
711
|
+
sync_error?: string | null;
|
|
712
|
+
created_at?: string;
|
|
713
|
+
updated_at?: string;
|
|
714
|
+
}
|
|
715
|
+
interface LeetCodeSubmissionRow {
|
|
716
|
+
id: string;
|
|
717
|
+
user_id: string;
|
|
718
|
+
leetcode_username: string;
|
|
719
|
+
leetcode_submission_id: string;
|
|
720
|
+
title_slug: string;
|
|
721
|
+
title: string;
|
|
722
|
+
leetcode_number?: number | null;
|
|
723
|
+
difficulty?: string | null;
|
|
724
|
+
submitted_at: string;
|
|
725
|
+
problem_id?: string | null;
|
|
726
|
+
status: string;
|
|
727
|
+
created_at?: string;
|
|
728
|
+
updated_at?: string;
|
|
729
|
+
}
|
|
730
|
+
interface LeetCodeIgnoredImportRow {
|
|
731
|
+
user_id: string;
|
|
732
|
+
title_slug: string;
|
|
733
|
+
leetcode_number?: number | null;
|
|
734
|
+
ignored_at?: string;
|
|
735
|
+
created_at?: string;
|
|
736
|
+
}
|
|
737
|
+
interface LeetCodeActivityFunctionResponse {
|
|
738
|
+
connection: LeetCodeConnection | null;
|
|
739
|
+
submissions: LeetCodeSubmission[];
|
|
740
|
+
ignoredImports: LeetCodeIgnoredImport[];
|
|
741
|
+
summary: LeetCodeSyncSummary;
|
|
742
|
+
error?: string;
|
|
743
|
+
}
|
|
744
|
+
interface LeetCodeActivityResult {
|
|
745
|
+
data: LeetCodeActivityFunctionResponse | null;
|
|
746
|
+
error: string | null;
|
|
747
|
+
}
|
|
748
|
+
declare function normalizeLeetCodeUsername(input: string): string;
|
|
749
|
+
declare function sanitizeLeetCodeActivityError(error: unknown): string;
|
|
750
|
+
declare function toLeetCodeConnection(row: LeetCodeConnectionRow): LeetCodeConnection;
|
|
751
|
+
declare function toLeetCodeSubmission(row: LeetCodeSubmissionRow): LeetCodeSubmission;
|
|
752
|
+
declare function toLeetCodeIgnoredImport(row: LeetCodeIgnoredImportRow): LeetCodeIgnoredImport;
|
|
753
|
+
interface CreateLeetCodeActivityDataOptions {
|
|
754
|
+
/** Platform Supabase client; null falls back to the no-op guard shapes. */
|
|
755
|
+
supabase: SupabaseClient | null;
|
|
756
|
+
/** Per-operation timeout for table reads (F-9). */
|
|
757
|
+
timeoutMs?: number;
|
|
758
|
+
/** Timeout for the sync-leetcode-activity Edge Function invoke. */
|
|
759
|
+
invokeTimeoutMs?: number;
|
|
760
|
+
}
|
|
761
|
+
interface LeetCodeActivityData {
|
|
762
|
+
fetchLeetCodeConnection(userId: string): Promise<{
|
|
763
|
+
data: LeetCodeConnection | null;
|
|
764
|
+
error: unknown;
|
|
765
|
+
}>;
|
|
766
|
+
fetchRecentLeetCodeSubmissions(userId: string, limit?: number): Promise<{
|
|
767
|
+
data: LeetCodeSubmission[] | null;
|
|
768
|
+
error: unknown;
|
|
769
|
+
}>;
|
|
770
|
+
fetchLeetCodeIgnoredImports(userId: string): Promise<{
|
|
771
|
+
data: LeetCodeIgnoredImport[] | null;
|
|
772
|
+
error: unknown;
|
|
773
|
+
}>;
|
|
774
|
+
connectLeetCodeActivity(username: string): Promise<LeetCodeActivityResult>;
|
|
775
|
+
syncLeetCodeActivity(force?: boolean): Promise<LeetCodeActivityResult>;
|
|
776
|
+
disconnectLeetCodeActivity(): Promise<LeetCodeActivityResult>;
|
|
777
|
+
markLeetCodeImportImported(submissionDbId: string, problemId: string): Promise<LeetCodeActivityResult>;
|
|
778
|
+
markLeetCodeImportLinkedExisting(submissionDbId: string, problemId: string): Promise<LeetCodeActivityResult>;
|
|
779
|
+
ignoreLeetCodeImport(submissionDbId: string): Promise<LeetCodeActivityResult>;
|
|
780
|
+
restoreIgnoredLeetCodeImport(titleSlug: string): Promise<LeetCodeActivityResult>;
|
|
781
|
+
markLeetCodeSubmissionRated(submissionDbId: string, problemId: string): Promise<LeetCodeActivityResult>;
|
|
782
|
+
}
|
|
783
|
+
declare function createLeetCodeActivityData(options: CreateLeetCodeActivityDataOptions): LeetCodeActivityData;
|
|
784
|
+
|
|
785
|
+
interface BuildPendingLeetCodeImportsArgs {
|
|
786
|
+
submissions: LeetCodeSubmission[];
|
|
787
|
+
problems: Problem[];
|
|
788
|
+
ignoredImports: LeetCodeIgnoredImport[];
|
|
789
|
+
reviewEvents?: ReviewEvent[];
|
|
790
|
+
today?: string;
|
|
791
|
+
}
|
|
792
|
+
interface BuildProblemOptions {
|
|
793
|
+
today?: string;
|
|
794
|
+
now?: string;
|
|
795
|
+
autoExpired?: boolean;
|
|
796
|
+
}
|
|
797
|
+
declare function buildPendingLeetCodeImports({ submissions, problems, ignoredImports, today, }: BuildPendingLeetCodeImportsArgs): PendingLeetCodeImport[];
|
|
798
|
+
declare function buildTodayLeetCodeItems({ submissions, problems, ignoredImports, reviewEvents, today, }: BuildPendingLeetCodeImportsArgs): TodayLeetCodeItem[];
|
|
799
|
+
declare function buildProblemFromLeetCodeImport(item: PendingLeetCodeImport, confidence: Confidence, options?: BuildProblemOptions): Problem;
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Get all available lists with computed counts.
|
|
803
|
+
*/
|
|
804
|
+
declare function getListSummaries(existingNumbers: Set<number>): ListSummary[];
|
|
805
|
+
/**
|
|
806
|
+
* Get the problems to add for a specific list.
|
|
807
|
+
* Returns only problems NOT already in the user's library.
|
|
808
|
+
* Each problem gets its pattern from PATTERN_MAP.
|
|
809
|
+
*/
|
|
810
|
+
declare function getListProblems(listId: string, existingNumbers: Set<number>): {
|
|
811
|
+
lcProblems: LeetCodeProblem[];
|
|
812
|
+
patternMap: Map<number, string[]>;
|
|
813
|
+
};
|
|
814
|
+
declare function getPatternsForProblemNumber(problemNumber: number | null | undefined): string[];
|
|
815
|
+
|
|
816
|
+
type TodayLeetCodeCompletionAction = "imported" | "linked_existing" | "rated";
|
|
817
|
+
interface LeetCodeCompletionIdentity {
|
|
818
|
+
submissionDbId?: string | null;
|
|
819
|
+
leetcodeSubmissionId?: string | null;
|
|
820
|
+
titleSlug?: string | null;
|
|
821
|
+
leetcodeNumber?: number | null;
|
|
822
|
+
problemId?: string | null;
|
|
823
|
+
}
|
|
824
|
+
interface TodayLeetCodeCompletion extends Required<Pick<LeetCodeCompletionIdentity, "submissionDbId">> {
|
|
825
|
+
key: string;
|
|
826
|
+
date: string;
|
|
827
|
+
leetcodeSubmissionId: string | null;
|
|
828
|
+
titleSlug: string | null;
|
|
829
|
+
leetcodeNumber: number | null;
|
|
830
|
+
problemId: string;
|
|
831
|
+
action: TodayLeetCodeCompletionAction;
|
|
832
|
+
completedAt: string;
|
|
833
|
+
}
|
|
834
|
+
declare function buildTodayLeetCodeCompletionsStorageKey(today?: string): string;
|
|
835
|
+
declare function buildLeetCodeCompletionKey(identity: LeetCodeCompletionIdentity): string;
|
|
836
|
+
declare function buildLeetCodeCompletionKeys(identity: LeetCodeCompletionIdentity): string[];
|
|
837
|
+
/** Pure raw-storage-value → records parse; platforms feed it their own reads. */
|
|
838
|
+
declare function parseTodayLeetCodeCompletions(raw: string | null, today?: string): TodayLeetCodeCompletion[];
|
|
839
|
+
/** Pure records → raw storage value; keeps only the given day's records. */
|
|
840
|
+
declare function serializeTodayLeetCodeCompletions(completions: TodayLeetCodeCompletion[], today?: string): string;
|
|
841
|
+
declare function loadTodayLeetCodeCompletions(storage: StorageAdapter, today?: string): Promise<TodayLeetCodeCompletion[]>;
|
|
842
|
+
declare function saveTodayLeetCodeCompletions(storage: StorageAdapter, completions: TodayLeetCodeCompletion[], today?: string, hooks?: CoreHooks): Promise<void>;
|
|
843
|
+
declare function mergeTodayLeetCodeCompletion(completions: TodayLeetCodeCompletion[], completion: LeetCodeCompletionIdentity & {
|
|
844
|
+
submissionDbId: string;
|
|
845
|
+
problemId: string;
|
|
846
|
+
action: TodayLeetCodeCompletionAction;
|
|
847
|
+
completedAt?: string;
|
|
848
|
+
}, today?: string, now?: string): TodayLeetCodeCompletion[];
|
|
849
|
+
declare function addTodayLeetCodeCompletion(storage: StorageAdapter, completion: LeetCodeCompletionIdentity & {
|
|
850
|
+
submissionDbId: string;
|
|
851
|
+
problemId: string;
|
|
852
|
+
action: TodayLeetCodeCompletionAction;
|
|
853
|
+
}, today?: string): Promise<TodayLeetCodeCompletion[]>;
|
|
854
|
+
declare function isLeetCodeSubmissionCompletedToday(identity: LeetCodeCompletionIdentity, completions: TodayLeetCodeCompletion[]): boolean;
|
|
855
|
+
declare function buildLeetCodeSubmissionsWithCompletions(submissions: LeetCodeSubmission[], completions: TodayLeetCodeCompletion[]): LeetCodeSubmission[];
|
|
856
|
+
|
|
857
|
+
interface RateLeetCodeReviewLocallyFirstParams {
|
|
858
|
+
submissionDbId: string;
|
|
859
|
+
problemId: string;
|
|
860
|
+
confidence: Confidence;
|
|
861
|
+
completionSource?: LeetCodeCompletionIdentity;
|
|
862
|
+
onReview: (problemId: string, confidence: Confidence, options?: {
|
|
863
|
+
replaceSameDayReviewEvent?: boolean;
|
|
864
|
+
}) => void;
|
|
865
|
+
markRated: (submissionDbId: string, problemId: string) => Promise<LeetCodeActivityResult>;
|
|
866
|
+
onLocalReviewRecorded?: (source: LeetCodeCompletionIdentity & {
|
|
867
|
+
submissionDbId: string;
|
|
868
|
+
}, problemId: string) => void;
|
|
869
|
+
onError?: (error: string) => void;
|
|
870
|
+
}
|
|
871
|
+
declare function rateLeetCodeReviewLocallyFirst({ submissionDbId, problemId, confidence, completionSource, onReview, markRated, onLocalReviewRecorded, onError, }: RateLeetCodeReviewLocallyFirstParams): Promise<LeetCodeActivityResult>;
|
|
872
|
+
|
|
873
|
+
interface ResolveTodayLeetCodeStateArgs {
|
|
874
|
+
problems: Problem[];
|
|
875
|
+
reviewEvents: ReviewEvent[];
|
|
876
|
+
leetcodeSubmissions: LeetCodeSubmission[];
|
|
877
|
+
ignoredImports: LeetCodeIgnoredImport[];
|
|
878
|
+
todayCompletions: TodayLeetCodeCompletion[];
|
|
879
|
+
today: string;
|
|
880
|
+
}
|
|
881
|
+
interface TodayLeetCodeResolvedState {
|
|
882
|
+
fromLeetCodeItems: TodayLeetCodeItem[];
|
|
883
|
+
doneTodayLeetCodeSubmissions: LeetCodeSubmission[];
|
|
884
|
+
effectiveCompletions: TodayLeetCodeCompletion[];
|
|
885
|
+
}
|
|
886
|
+
type CompletionInput = LeetCodeCompletionIdentity & {
|
|
887
|
+
submissionDbId: string;
|
|
888
|
+
problemId: string;
|
|
889
|
+
action: TodayLeetCodeCompletion["action"];
|
|
890
|
+
completedAt?: string;
|
|
891
|
+
};
|
|
892
|
+
declare function buildReviewedTodayLeetCodeCompletions({ submissions, problems, reviewEvents, today, }: {
|
|
893
|
+
submissions: LeetCodeSubmission[];
|
|
894
|
+
problems: Problem[];
|
|
895
|
+
reviewEvents: ReviewEvent[];
|
|
896
|
+
today: string;
|
|
897
|
+
}): CompletionInput[];
|
|
898
|
+
declare function mergeTodayLeetCodeCompletions(completions: TodayLeetCodeCompletion[], completionInputs: CompletionInput[], today: string): TodayLeetCodeCompletion[];
|
|
899
|
+
declare function resolveTodayLeetCodeState({ problems, reviewEvents, leetcodeSubmissions, ignoredImports, todayCompletions, today, }: ResolveTodayLeetCodeStateArgs): TodayLeetCodeResolvedState;
|
|
900
|
+
|
|
901
|
+
declare const LEETCODE_PROBLEMS: LeetCodeProblem[];
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
declare function searchProblems(query: string, limit?: number): LeetCodeProblem[];
|
|
905
|
+
declare function getProblemByNumber(num: number): LeetCodeProblem | null;
|
|
906
|
+
declare function buildLeetCodeUrl(slug: string): string;
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* @patternbank/core — shared domain logic for PatternBank web and mobile.
|
|
910
|
+
*
|
|
911
|
+
* Pure logic only: no UI, no Tailwind classes, no import-time side effects.
|
|
912
|
+
* Platform concerns (storage, Supabase client, analytics) are injected via
|
|
913
|
+
* adapters — see storage/adapter.ts and supabase/data.ts as they land.
|
|
914
|
+
*/
|
|
915
|
+
|
|
916
|
+
/** Placeholder export proving the workspace wiring; replaced as modules extract. */
|
|
917
|
+
declare const CORE_PACKAGE_NAME = "@patternbank/core";
|
|
918
|
+
|
|
919
|
+
export { type BackupData, CLOUD_OPERATION_TIMEOUT_MS, CORE_PACKAGE_NAME, CORE_PATTERNS, type CloudData, type CloudPreferences, type Confidence, type CoreHooks, type CorePreferences, type CreateCloudDataOptions, type CreateLeetCodeActivityDataOptions, DATA_RESET_KEY, DEFAULT_PREFERENCES, DIFFICULTIES, type DataReset, type DataResetRow, type Difficulty, type DoneTodayFeedItem, EXTRA_PATTERNS, type ExitingTodayLeetCodeItem, FIVE_STAR_GRADUATION_INTERVALS, FULL_SYNC_TIMEOUT_MS, type FullSyncCloud, type FullSyncDeps, type FullSyncFailure, type FullSyncLocalState, type FullSyncResult, type FullSyncSuccess, INTERVALS, LEETCODE_ACTIVITY_TIMEOUT_MS, LEETCODE_PROBLEMS, LEETCODE_RECENT_ACTIVITY_LIMIT, type LeetCodeActivityData, type LeetCodeActivityFunctionResponse, type LeetCodeActivityResult, type LeetCodeCompletionIdentity, type LeetCodeConnection, type LeetCodeConnectionRow, type LeetCodeIgnoredImport, type LeetCodeIgnoredImportRow, type LeetCodeProblem, type LeetCodeSubmission, type LeetCodeSubmissionRow, type LeetCodeSubmissionStatus, type LeetCodeSyncStatus, type LeetCodeSyncSummary, type ListSummary, type MergePreferencesResult, type MergeProblemTombstonesResult, type MergeProblemsResult, type MergeReviewEventsOptions, type MergeReviewEventsResult, type MergeReviewLogResult, PREFERENCES_KEY, PROBLEM_TOMBSTONES_KEY, PROGRESS_CONFIDENCE_TINTS, PROGRESS_EMPTY_CONFIDENCE_TINT, PROGRESS_HEATMAP_CONFIDENCE_TINTS, type PatternColor, type PendingLeetCodeImport, type Problem, type ProblemList, type ProblemRow, type ProblemTombstone, type ProblemTombstoneRow, type ProgressConfidenceTint, type ProjectionDay, type ProjectionDistribution, type ProjectionSeriesOptions, type ProjectionSnapshot, type PruneOldEventsOptions, type PruneOldEventsResult, REVIEW_EVENTS_KEY, REVIEW_EVENTS_PRUNED_BEFORE_KEY, REVIEW_LOG_KEY, type RateLeetCodeReviewLocallyFirstParams, type ReviewEvent, type ReviewEventRow, type ReviewHistoryEntry, type ReviewLogEntry, STORAGE_KEY, type SolvedOnLeetCodeTodayIndex, type StorageAdapter, type SyncStatus, SyncTimeoutError, type TodayActivityFeedItem, type TodayLeetCodeCompletion, type TodayLeetCodeCompletionAction, type TodayLeetCodeItem, type TodayLeetCodeResolvedState, type TodayReviewState, addDays, addTodayLeetCodeCompletion, buildDoneTodayFeedItems, buildLeetCodeCompletionKey, buildLeetCodeCompletionKeys, buildLeetCodeSubmissionsWithCompletions, buildLeetCodeUrl, buildNewProblems, buildPendingLeetCodeImports, buildProblemFromLeetCodeImport, buildRemovedTodayLeetCodeItems, buildReviewCountMap, buildReviewedProblem, buildReviewedTodayLeetCodeCompletions, buildSolvedOnLeetCodeTodayIndex, buildTodayActivityFeedItems, buildTodayLeetCodeCompletionsStorageKey, buildTodayLeetCodeItemKey, buildTodayLeetCodeItems, buildTodayReviewState, calculateLongestStreak, calculateStreak, compareDataResets, computeNextReviewDate, computeReviewProgress, countReviewedToday, createCloudData, createLeetCodeActivityData, dataResetTime, dateOnlyToUtcMs, deduplicateProblems, filterExistingProblems, filterProblemsAfterDataReset, filterReviewEventsAfterDataReset, filterReviewEventsToProblems, filterReviewLogAfterDataReset, filterTombstonedProblems, filterTombstonesAfterDataReset, formatDisplayDate, formatLocalDate, formatRelativeDate, generateId, getConfidenceDistribution, getDefaultFiveStarStreak, getFiveStarGraduationIntervalDays, getIntervalDays, getListProblems, getListSummaries, getNextFiveStarStreak, getPatternsForProblemNumber, getPreviousFiveStarStreak, getProblemByNumber, getProgressConfidenceTint, getProgressHeatmapTint, getReviewIntervalDays, getTopPatterns, getVisiblePatterns, getWeekStart, groupEventsByWeek, interleaveByDifficulty, isLeetCodeSubmissionCompletedToday, isLikelyOfflineError, isSyncTimeoutError, loadTodayLeetCodeCompletions, mergeImportedProblems, mergePreferences, mergeProblemTombstones, mergeProblems, mergeReviewEvents, mergeReviewLog, mergeTodayLeetCodeCompletion, mergeTodayLeetCodeCompletions, newestDataReset, normalizeLeetCodeUsername, parseDateOnly, parseTodayLeetCodeCompletions, performFullSync, preferencesEqual, prioritizeProblems, pruneOldEvents, rateLeetCodeReviewLocallyFirst, resolveTodayLeetCodeState, reviewDedupeKey, reviewEventKey, reviewEventsMatch, reviewLogFromEvents, sanitizeLeetCodeActivityError, saveTodayLeetCodeCompletions, searchProblems, serializeTodayLeetCodeCompletions, simulateProjection, simulateProjectionSeries, timestampMs, toCamelCase, toCloudPreferences, toLeetCodeConnection, toLeetCodeIgnoredImport, toLeetCodeSubmission, toSnakeCase, todayStr, utcToLocalDateStr, withCloudOperationTimeout, withTimeout };
|