flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t/services/context/src/visibility.ts

74 lines2,903 bytesCodeBlame
1/**
2 * Who may read what in the context hub. A workspace is the boundary: no
3 * search ever reads another's rows. Inside it, members (and the workspace's
4 * own agents, whose token makes them members of it and nothing else) read
5 * everything; anyone else reads only what comes from public projects, and
6 * never memory. Pure, so the rules are tested apart from the index.
7 */
8
9import type { SearchHit, SearchKind } from "@g1t/contracts";
10
11export type Reader = {
12 workspace: string;
13 member: boolean;
14 /** For someone who is not a member: the slugs of the projects they may see. */
15 visible: Set<string>;
16};
17
18/** The metadata every row of the search index carries. */
19export type IndexMeta = {
20 workspace: string;
21 kind: string;
22 project: string;
23 private: boolean;
24 title: string;
25 snippet: string;
26 url: string;
27 source: string;
28 by: string;
29 at: string;
30};
31
32/** The search index's filter for a reader: their workspace always, public rows unless a member. */
33export function indexFilter(
34 reader: Reader,
35 options: { project?: string | null; kinds?: SearchKind[] | null },
36): Record<string, unknown> {
37 const filter: Record<string, unknown> = { workspace: reader.workspace };
38 if (!reader.member) filter.private = false;
39 if (options.project) filter.project = options.project;
40 const kinds = allowedKinds(reader, options.kinds);
41 if (kinds) filter.kind = { $in: kinds };
42 return filter;
43}
44
45/** The kinds a reader may ask for: all of them, less memory for someone not a member. Null for all. */
46export function allowedKinds(reader: Reader, kinds?: SearchKind[] | null): SearchKind[] | null {
47 const wanted = kinds?.length ? kinds : null;
48 if (reader.member) return wanted;
49 const all: SearchKind[] = ["project", "app", "api", "package", "language", "owner", "environment", "integration", "doc", "issue", "pull"];
50 return (wanted ?? all).filter((kind) => kind !== "memory");
51}
52
53/** Whether a row from the index may be shown to a reader: checked again, whatever the filter did. */
54export function readable(meta: Pick<IndexMeta, "workspace" | "kind" | "project" | "private">, reader: Reader): boolean {
55 if (meta.workspace !== reader.workspace) return false;
56 if (reader.member) return true;
57 if (meta.kind === "memory" || meta.private) return false;
58 // A project made private since it was indexed is hidden at once.
59 return !meta.project || reader.visible.has(meta.project);
60}
61
62/** Semantic hits first, then text matches not already among them, at most `limit`. */
63export function merge(semantic: SearchHit[], text: SearchHit[], limit: number): SearchHit[] {
64 const seen = new Set<string>();
65 const out: SearchHit[] = [];
66 for (const hit of [...semantic.sort((a, b) => b.score - a.score), ...text]) {
67 const key = `${hit.kind}:${hit.id}`;
68 if (seen.has(key)) continue;
69 seen.add(key);
70 out.push(hit);
71 if (out.length >= limit) break;
72 }
73 return out;
74}