memonaut 0.1.0 → 0.2.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/src/search.ts CHANGED
@@ -70,6 +70,40 @@ export function quoteQuery(text: string): string {
70
70
  return quoted.join(' ');
71
71
  }
72
72
 
73
+ /**
74
+ * Which transcripts a query is allowed to see.
75
+ *
76
+ * Visibility is decided per THREAD, because a lineage can in principle span
77
+ * working directories; the lineage set is only a prefilter. Both the FTS and
78
+ * the regex path go through here, so `private` and the cwd/project filters
79
+ * cannot drift apart between them.
80
+ */
81
+ export function visibleSets(
82
+ files: Map<number, FileRecord>,
83
+ query: Pick<SearchQuery, 'cwd' | 'project' | 'includePrivate'>,
84
+ ): {files: Set<number>; lineages: Set<number>; totalLineages: Set<number>} {
85
+ const cwdMatch = query.cwd && query.cwd.length ? matcher(query.cwd) : null;
86
+ const projectMatch =
87
+ query.project && query.project.length ? new Set(query.project) : null;
88
+ const visibleFiles = new Set<number>();
89
+ const visibleLineages = new Set<number>();
90
+ const totalLineages = new Set<number>();
91
+ for (const file of files.values()) {
92
+ if (file.lineage_id !== null) totalLineages.add(Number(file.lineage_id));
93
+ if (!query.includePrivate && file.private) continue;
94
+ if (cwdMatch && !cwdMatch(file.cwd)) continue;
95
+ if (projectMatch && !(file.project && projectMatch.has(file.project)))
96
+ continue;
97
+ visibleFiles.add(Number(file.id));
98
+ if (file.lineage_id !== null) visibleLineages.add(Number(file.lineage_id));
99
+ }
100
+ return {
101
+ files: visibleFiles,
102
+ lineages: visibleLineages,
103
+ totalLineages,
104
+ };
105
+ }
106
+
73
107
  function inList(
74
108
  column: string,
75
109
  values: string[] | undefined,
@@ -109,25 +143,11 @@ export function search(
109
143
  const threadLimit = query.threadLimit ?? 3;
110
144
  const files = loadFiles(db);
111
145
 
112
- // Visibility is decided per THREAD, because a lineage can in principle span
113
- // working directories. The lineage set below is only a prefilter.
114
- const cwdMatch = query.cwd && query.cwd.length ? matcher(query.cwd) : null;
115
- const projectMatch =
116
- query.project && query.project.length ? new Set(query.project) : null;
117
- const visibleFiles = new Set<number>();
118
- const visibleLineages = new Set<number>();
119
- for (const file of files.values()) {
120
- if (!query.includePrivate && file.private) continue;
121
- if (cwdMatch && !cwdMatch(file.cwd)) continue;
122
- if (projectMatch && !(file.project && projectMatch.has(file.project)))
123
- continue;
124
- visibleFiles.add(Number(file.id));
125
- if (file.lineage_id !== null) visibleLineages.add(Number(file.lineage_id));
126
- }
127
-
128
- const totalLineages = new Set<number>();
129
- for (const file of files.values())
130
- if (file.lineage_id !== null) totalLineages.add(Number(file.lineage_id));
146
+ const {
147
+ files: visibleFiles,
148
+ lineages: visibleLineages,
149
+ totalLineages,
150
+ } = visibleSets(files, query);
131
151
 
132
152
  const clauses: string[] = [];
133
153
  const args: unknown[] = [];
@@ -259,16 +279,20 @@ export function search(
259
279
  };
260
280
  }
261
281
 
262
- function buildHit(
263
- row: RawHit,
264
- kind: ChunkKind,
265
- score: number,
282
+ /**
283
+ * Fan one entry back out to every visible thread that carries it.
284
+ *
285
+ * Shared by the FTS and the regex path, because they must produce identical
286
+ * thread lists: an entry is stored once and `membership` is what turns it back
287
+ * into "the twelve threads that inherited this".
288
+ */
289
+ export function collectThreads(
290
+ entryId: number,
266
291
  memberStmt: ReturnType<DB['prepare']>,
267
292
  files: Map<number, FileRecord>,
268
293
  visibleFiles: Set<number>,
269
- threadLimit: number,
270
- ): SearchHit | null {
271
- const members = memberStmt.all(row.entry_id) as unknown as Array<{
294
+ ): ThreadRef[] {
295
+ const members = memberStmt.all(entryId) as unknown as Array<{
272
296
  file_id: number;
273
297
  seq: number;
274
298
  }>;
@@ -297,6 +321,24 @@ function buildHit(
297
321
  threads.sort((a, b) =>
298
322
  (b.lastActivity ?? '').localeCompare(a.lastActivity ?? ''),
299
323
  );
324
+ return threads;
325
+ }
326
+
327
+ function buildHit(
328
+ row: RawHit,
329
+ kind: ChunkKind,
330
+ score: number,
331
+ memberStmt: ReturnType<DB['prepare']>,
332
+ files: Map<number, FileRecord>,
333
+ visibleFiles: Set<number>,
334
+ threadLimit: number,
335
+ ): SearchHit | null {
336
+ const threads = collectThreads(
337
+ Number(row.entry_id),
338
+ memberStmt,
339
+ files,
340
+ visibleFiles,
341
+ );
300
342
  if (threads.length === 0) return null;
301
343
 
302
344
  return {