@100xprompt/brain 0.0.0-stage → 0.1.1
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/LICENSE +48 -0
- package/README.md +235 -2
- package/package.json +48 -4
- package/src/adapters/aider/index.ts +8 -0
- package/src/adapters/aider/reader.ts +129 -0
- package/src/adapters/aider/records.ts +220 -0
- package/src/adapters/aider/writer.ts +269 -0
- package/src/adapters/amazon-q-cli/index.ts +8 -0
- package/src/adapters/amazon-q-cli/reader.ts +84 -0
- package/src/adapters/amazon-q-cli/records.ts +191 -0
- package/src/adapters/amazon-q-cli/writer.ts +370 -0
- package/src/adapters/amazon-q-ide/index.ts +9 -0
- package/src/adapters/amazon-q-ide/reader.ts +131 -0
- package/src/adapters/amazon-q-ide/records.ts +141 -0
- package/src/adapters/amazon-q-ide/writer.ts +325 -0
- package/src/adapters/antigravity/index.ts +9 -0
- package/src/adapters/antigravity/proto.ts +194 -0
- package/src/adapters/antigravity/reader.ts +424 -0
- package/src/adapters/antigravity/records.ts +236 -0
- package/src/adapters/antigravity/writer.ts +463 -0
- package/src/adapters/auggie/index.ts +36 -0
- package/src/adapters/claude-code/index.ts +8 -0
- package/src/adapters/claude-code/reader.ts +639 -0
- package/src/adapters/claude-code/records.ts +88 -0
- package/src/adapters/claude-code/writer.ts +329 -0
- package/src/adapters/cline/index.ts +12 -0
- package/src/adapters/cline/reader.ts +432 -0
- package/src/adapters/cline/records.ts +151 -0
- package/src/adapters/cline/vscdb.ts +56 -0
- package/src/adapters/cline/writer.ts +656 -0
- package/src/adapters/cline/xml.ts +177 -0
- package/src/adapters/cline4/index.ts +9 -0
- package/src/adapters/cline4/reader.ts +179 -0
- package/src/adapters/cline4/records.ts +92 -0
- package/src/adapters/cline4/writer.ts +361 -0
- package/src/adapters/codebuff/index.ts +8 -0
- package/src/adapters/codebuff/project.ts +207 -0
- package/src/adapters/codebuff/reader.ts +215 -0
- package/src/adapters/codebuff/records.ts +173 -0
- package/src/adapters/codebuff/writer.ts +383 -0
- package/src/adapters/codex/index.ts +8 -0
- package/src/adapters/codex/native.ts +239 -0
- package/src/adapters/codex/reader.ts +747 -0
- package/src/adapters/codex/records.ts +114 -0
- package/src/adapters/codex/writer.ts +479 -0
- package/src/adapters/continue/index.ts +9 -0
- package/src/adapters/continue/reader.ts +68 -0
- package/src/adapters/continue/records.ts +418 -0
- package/src/adapters/continue/writer.ts +162 -0
- package/src/adapters/copilot-cli/index.ts +9 -0
- package/src/adapters/copilot-cli/reader.ts +186 -0
- package/src/adapters/copilot-cli/records.ts +241 -0
- package/src/adapters/copilot-cli/writer.ts +353 -0
- package/src/adapters/crush/index.ts +8 -0
- package/src/adapters/crush/reader.ts +180 -0
- package/src/adapters/crush/records.ts +169 -0
- package/src/adapters/crush/writer.ts +333 -0
- package/src/adapters/cursor-cli/index.ts +405 -0
- package/src/adapters/cursor-ide/agentkv.ts +95 -0
- package/src/adapters/cursor-ide/index.ts +501 -0
- package/src/adapters/cursor-ide/transcripts.ts +122 -0
- package/src/adapters/cursor-ide/workspaces.ts +119 -0
- package/src/adapters/cursor-ide/writer.ts +170 -0
- package/src/adapters/devin/index.ts +8 -0
- package/src/adapters/devin/reader.ts +305 -0
- package/src/adapters/devin/records.ts +277 -0
- package/src/adapters/devin/writer.ts +523 -0
- package/src/adapters/factory-droid/index.ts +12 -0
- package/src/adapters/factory-droid/reader.ts +184 -0
- package/src/adapters/factory-droid/records.ts +79 -0
- package/src/adapters/factory-droid/writer.ts +350 -0
- package/src/adapters/gemini-cli/exchanges.ts +228 -0
- package/src/adapters/gemini-cli/index.ts +396 -0
- package/src/adapters/gemini-cli/records.ts +521 -0
- package/src/adapters/gemini-cli/writer.ts +354 -0
- package/src/adapters/goose/index.ts +8 -0
- package/src/adapters/goose/reader.ts +138 -0
- package/src/adapters/goose/records.ts +199 -0
- package/src/adapters/goose/writer.ts +340 -0
- package/src/adapters/grok-cli/image.ts +44 -0
- package/src/adapters/grok-cli/index.ts +8 -0
- package/src/adapters/grok-cli/reader.ts +302 -0
- package/src/adapters/grok-cli/records.ts +172 -0
- package/src/adapters/grok-cli/writer.ts +329 -0
- package/src/adapters/hermes/index.ts +8 -0
- package/src/adapters/hermes/reader.ts +249 -0
- package/src/adapters/hermes/records.ts +285 -0
- package/src/adapters/hermes/writer.ts +452 -0
- package/src/adapters/iflow-cli/index.ts +72 -0
- package/src/adapters/iflow-cli/records.ts +215 -0
- package/src/adapters/iflow-cli/writer.ts +204 -0
- package/src/adapters/index.ts +112 -0
- package/src/adapters/kilo-cli/index.ts +21 -0
- package/src/adapters/kilo-code/index.ts +11 -0
- package/src/adapters/kilo-code/writer.ts +92 -0
- package/src/adapters/kimi/index.ts +9 -0
- package/src/adapters/kimi/reader.ts +92 -0
- package/src/adapters/kimi/records.ts +221 -0
- package/src/adapters/kimi/writer.ts +367 -0
- package/src/adapters/kiro-cli/index.ts +8 -0
- package/src/adapters/kiro-cli/reader.ts +87 -0
- package/src/adapters/kiro-cli/records.ts +260 -0
- package/src/adapters/kiro-cli/writer.ts +305 -0
- package/src/adapters/kiro-ide/index.ts +8 -0
- package/src/adapters/kiro-ide/writer.ts +52 -0
- package/src/adapters/letta/index.ts +8 -0
- package/src/adapters/letta/reader.ts +177 -0
- package/src/adapters/letta/records.ts +119 -0
- package/src/adapters/letta/writer.ts +413 -0
- package/src/adapters/mastracode/index.ts +8 -0
- package/src/adapters/mastracode/reader.ts +323 -0
- package/src/adapters/mastracode/records.ts +336 -0
- package/src/adapters/mastracode/writer.ts +385 -0
- package/src/adapters/mistral-vibe/index.ts +9 -0
- package/src/adapters/mistral-vibe/reader.ts +62 -0
- package/src/adapters/mistral-vibe/records.ts +104 -0
- package/src/adapters/mistral-vibe/writer.ts +359 -0
- package/src/adapters/muse/index.ts +10 -0
- package/src/adapters/muse/reader.ts +332 -0
- package/src/adapters/muse/records.ts +104 -0
- package/src/adapters/muse/writer.ts +387 -0
- package/src/adapters/omp/index.ts +13 -0
- package/src/adapters/omp/reader.ts +314 -0
- package/src/adapters/omp/records.ts +153 -0
- package/src/adapters/omp/writer.ts +216 -0
- package/src/adapters/opencode/ids.ts +52 -0
- package/src/adapters/opencode/index.ts +31 -0
- package/src/adapters/opencode/note.ts +8 -0
- package/src/adapters/opencode/reader.ts +452 -0
- package/src/adapters/opencode/tools.ts +174 -0
- package/src/adapters/opencode/v2.ts +367 -0
- package/src/adapters/opencode/writer.ts +379 -0
- package/src/adapters/openhands/index.ts +8 -0
- package/src/adapters/openhands/reader.ts +222 -0
- package/src/adapters/openhands/records.ts +136 -0
- package/src/adapters/openhands/writer.ts +404 -0
- package/src/adapters/pearai/index.ts +10 -0
- package/src/adapters/pi/index.ts +28 -0
- package/src/adapters/pi/reader.ts +261 -0
- package/src/adapters/pi/records.ts +99 -0
- package/src/adapters/pi/writer.ts +362 -0
- package/src/adapters/qoder-cli/index.ts +8 -0
- package/src/adapters/qoder-cli/reader.ts +83 -0
- package/src/adapters/qoder-cli/records.ts +45 -0
- package/src/adapters/qoder-cli/writer.ts +72 -0
- package/src/adapters/qwen-code/index.ts +84 -0
- package/src/adapters/qwen-code/records.ts +194 -0
- package/src/adapters/qwen-code/writer.ts +224 -0
- package/src/adapters/tabnine-cli/index.ts +8 -0
- package/src/adapters/tabnine-cli/reader.ts +106 -0
- package/src/adapters/tabnine-cli/records.ts +33 -0
- package/src/adapters/tabnine-cli/writer.ts +156 -0
- package/src/adapters/types.ts +73 -0
- package/src/adapters/void/index.ts +9 -0
- package/src/adapters/void/reader.ts +48 -0
- package/src/adapters/void/records.ts +285 -0
- package/src/adapters/void/writer.ts +93 -0
- package/src/adapters/vscode-copilot/index.ts +9 -0
- package/src/adapters/vscode-copilot/reader.ts +212 -0
- package/src/adapters/vscode-copilot/records.ts +388 -0
- package/src/adapters/vscode-copilot/shared.ts +68 -0
- package/src/adapters/vscode-copilot/writer.ts +546 -0
- package/src/adapters/zed/index.ts +8 -0
- package/src/adapters/zed/reader.ts +198 -0
- package/src/adapters/zed/records.ts +87 -0
- package/src/adapters/zed/writer.ts +341 -0
- package/src/brief-writers.ts +71 -0
- package/src/brief.ts +238 -0
- package/src/cli.ts +351 -0
- package/src/dashboard/cli.ts +148 -0
- package/src/dashboard/conversions.ts +170 -0
- package/src/dashboard/report.ts +261 -0
- package/src/dashboard/send.ts +213 -0
- package/src/dashboard/state.ts +95 -0
- package/src/fsutil.ts +133 -0
- package/src/gateway/common.ts +232 -0
- package/src/gateway/connect.ts +847 -0
- package/src/gateway/connectors/aider.ts +356 -0
- package/src/gateway/connectors/amazon-q-cli.ts +6 -0
- package/src/gateway/connectors/amazon-q-ide.ts +6 -0
- package/src/gateway/connectors/antigravity.ts +6 -0
- package/src/gateway/connectors/auggie.ts +6 -0
- package/src/gateway/connectors/cline-sdk.ts +85 -0
- package/src/gateway/connectors/cline.ts +34 -0
- package/src/gateway/connectors/codebuff.ts +6 -0
- package/src/gateway/connectors/continue.ts +579 -0
- package/src/gateway/connectors/copilot-cli.ts +138 -0
- package/src/gateway/connectors/crush.ts +313 -0
- package/src/gateway/connectors/cursor-cli.ts +6 -0
- package/src/gateway/connectors/cursor.ts +6 -0
- package/src/gateway/connectors/devin.ts +6 -0
- package/src/gateway/connectors/factory-droid.ts +308 -0
- package/src/gateway/connectors/goose.ts +79 -0
- package/src/gateway/connectors/grok-cli.ts +82 -0
- package/src/gateway/connectors/hermes.ts +230 -0
- package/src/gateway/connectors/iflow-cli.ts +50 -0
- package/src/gateway/connectors/index.ts +86 -0
- package/src/gateway/connectors/kilo-cli.ts +434 -0
- package/src/gateway/connectors/kilo-code.ts +32 -0
- package/src/gateway/connectors/kimi-code.ts +241 -0
- package/src/gateway/connectors/kiro-cli.ts +6 -0
- package/src/gateway/connectors/kiro-ide.ts +6 -0
- package/src/gateway/connectors/letta-code.ts +170 -0
- package/src/gateway/connectors/mastracode.ts +91 -0
- package/src/gateway/connectors/mistral-vibe.ts +495 -0
- package/src/gateway/connectors/muse.ts +27 -0
- package/src/gateway/connectors/omp.ts +279 -0
- package/src/gateway/connectors/openhands.ts +296 -0
- package/src/gateway/connectors/pearai.ts +52 -0
- package/src/gateway/connectors/pi.ts +357 -0
- package/src/gateway/connectors/qoder-cli.ts +6 -0
- package/src/gateway/connectors/qwen-code.ts +411 -0
- package/src/gateway/connectors/roo.ts +43 -0
- package/src/gateway/connectors/tabnine-cli.ts +6 -0
- package/src/gateway/connectors/void.ts +6 -0
- package/src/gateway/connectors/vscode-copilot.ts +286 -0
- package/src/gateway/connectors/windsurf.ts +6 -0
- package/src/gateway/connectors/zed.ts +29 -0
- package/src/gateway/signin.ts +323 -0
- package/src/handoff.ts +104 -0
- package/src/hooks.ts +455 -0
- package/src/memory/audit.ts +514 -0
- package/src/memory/blobs.ts +94 -0
- package/src/memory/cli.ts +117 -0
- package/src/memory/distill.ts +381 -0
- package/src/memory/externalize.ts +435 -0
- package/src/memory/lookups.ts +70 -0
- package/src/memory/mcp-install.ts +401 -0
- package/src/memory/mcp-server.ts +296 -0
- package/src/memory/prepare.ts +179 -0
- package/src/memory/preview.ts +154 -0
- package/src/memory/project.ts +35 -0
- package/src/memory/redact.ts +72 -0
- package/src/memory/relevance.ts +187 -0
- package/src/memory/repomap.ts +516 -0
- package/src/memory/store.ts +270 -0
- package/src/memory/sync.ts +166 -0
- package/src/memory/tidy.ts +193 -0
- package/src/memory/types.ts +118 -0
- package/src/org/admin-ops.ts +96 -0
- package/src/org/auth.ts +112 -0
- package/src/org/backup.ts +115 -0
- package/src/org/cli.ts +438 -0
- package/src/org/client.ts +404 -0
- package/src/org/config.ts +44 -0
- package/src/org/crypto.ts +537 -0
- package/src/org/db.ts +435 -0
- package/src/org/graph.ts +318 -0
- package/src/org/oidc.ts +435 -0
- package/src/org/policy.ts +398 -0
- package/src/org/server.ts +920 -0
- package/src/org/sso.ts +316 -0
- package/src/org/tls.ts +66 -0
- package/src/org/types.ts +193 -0
- package/src/self.ts +68 -0
- package/src/session/attachments.ts +53 -0
- package/src/session/peekcache.ts +50 -0
- package/src/session/types.ts +104 -0
- package/src/session/util.ts +130 -0
- package/src/sqlite.ts +48 -0
package/src/org/graph.ts
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
// brain org: an item's entities and relations, kept WITHOUT a leak, plus the per-(user, project) principals that make project-aware
|
|
2
|
+
// ranking possible. Everything here is server-side and used by server.ts; none of it changes src/org/types.ts.
|
|
3
|
+
//
|
|
4
|
+
// ENTITIES AND RELATIONS
|
|
5
|
+
// Chitta's entity graph is shared by everybody in the store (names fold onto one canonical entity across records), so feeding it
|
|
6
|
+
// from private items would be a side channel. The server therefore never gives Chitta a graph (`extractGraph: false`). Entities
|
|
7
|
+
// and relations live in two tables of the organisation database, keyed by the item (`rid`):
|
|
8
|
+
// org_item_entities (rid, ekey, name, type) ekey = the normalised name (see normKey)
|
|
9
|
+
// org_item_relations (rid, from_key, from_name, to_key, to_name, type)
|
|
10
|
+
// They have no meaning without their item row: a trigger on org_items (db.ts) deletes them with the item, whatever removes it
|
|
11
|
+
// (forget, retention, an admin). They carry no visibility of their own: EVERY read joins `org_items` and applies the same
|
|
12
|
+
// predicate recall uses (db.VISIBLE_SQL) in the same statement, so promotion, demotion, team and membership changes apply at
|
|
13
|
+
// once, and a row of an item the caller may not see is not counted, ranked, named or typed. Display names and types are taken
|
|
14
|
+
// from VISIBLE rows only (an entity spelled "ACME Corp" in a private item and "Acme Corp" in a team item shows the team spelling
|
|
15
|
+
// to everybody else).
|
|
16
|
+
//
|
|
17
|
+
// WHAT USES THEM
|
|
18
|
+
// * recall: after the direct (Chitta-ranked) hits, other visible items of the same project that share an entity (or a relation
|
|
19
|
+
// endpoint) with the top hits are pulled in, ranked by how many entities they share, with a score below every direct hit. They
|
|
20
|
+
// fill free slots of `limit` and the token budget, never more (see server.ts).
|
|
21
|
+
// * GET /v1/graph?project=<id>[&entity=<name>] (a new endpoint; the existing shapes are untouched)
|
|
22
|
+
// without `entity`: {project, entities: [{name, type?, items}]} the most-used entities of the project (at most 100)
|
|
23
|
+
// `items` counts the VISIBLE items that mention the entity (as entity or relation endpoint)
|
|
24
|
+
// with `entity`: {project, entity: {name, type?, items} | null,
|
|
25
|
+
// neighbours: [{name, relation, direction: "out"|"in", items}], (at most 100)
|
|
26
|
+
// items: [{id, name, kind, scope, owner, team?, source}]} (visible items that mention it, newest first, at most 50)
|
|
27
|
+
// An entity that does not exist, and one that only exists in items the caller may not see, give the SAME answer
|
|
28
|
+
// (`entity: null`, empty lists). Item text is not part of this answer: use recall for text.
|
|
29
|
+
//
|
|
30
|
+
// PROJECT-AWARE RANKING (principals, all names made of ids that contain no ':')
|
|
31
|
+
// h = first 32 hex of sha256(project id)
|
|
32
|
+
// sy:<user>:<h> the SYNTHETIC searching principal of one user in one project. Chitta ranks only what this principal may see.
|
|
33
|
+
// pj:<h> members of the project (the synthetic principal belongs to it only while the user is a member)
|
|
34
|
+
// tm:<h>:<team> a team's items in the project (belongs to it for each team the user is in)
|
|
35
|
+
// og:<h> org-wide items in the project (every synthetic principal belongs to it)
|
|
36
|
+
// An item's permission edges: the owner (`u:<owner>`, kept so the owner can always delete it, and `sy:<owner>:<h>`), plus the
|
|
37
|
+
// group of its scope. The synthetic principal's `belongsTo` edges are diffed against the user's CURRENT memberships at every
|
|
38
|
+
// recall (a few small queries), so a membership or team change applies on the next call and nothing is rebuilt in bulk.
|
|
39
|
+
import type { Database } from "bun:sqlite"
|
|
40
|
+
import { sha256Hex } from "./auth.ts"
|
|
41
|
+
import { VISIBLE_SQL, type ItemRow, type OrgDb } from "./db.ts"
|
|
42
|
+
import { redact } from "../memory/redact.ts"
|
|
43
|
+
import type { Scope } from "./types.ts"
|
|
44
|
+
|
|
45
|
+
// ---- sanitising what a client sends ----------------------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
export interface Entity {
|
|
48
|
+
name: string
|
|
49
|
+
type?: string
|
|
50
|
+
}
|
|
51
|
+
export interface Relation {
|
|
52
|
+
from: string
|
|
53
|
+
to: string
|
|
54
|
+
type: string
|
|
55
|
+
}
|
|
56
|
+
export interface Extras {
|
|
57
|
+
entities: Entity[]
|
|
58
|
+
relations: Relation[]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const MAX_ENTITY_NAME = 200
|
|
62
|
+
const MAX_ENTITIES = 100
|
|
63
|
+
const MAX_RELATIONS = 100
|
|
64
|
+
|
|
65
|
+
/** The comparison key of an entity name: Unicode-normalised, lower-cased, whitespace collapsed. "" means unusable. */
|
|
66
|
+
export function normKey(name: string): string {
|
|
67
|
+
return name.normalize("NFKC").toLowerCase().replace(/\s+/g, " ").trim().slice(0, MAX_ENTITY_NAME)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// eslint-disable-next-line no-control-regex
|
|
71
|
+
const CONTROL = /[\u0000-\u001f\u007f]/
|
|
72
|
+
const cleanName = (x: unknown): string | undefined => {
|
|
73
|
+
if (typeof x !== "string") return undefined
|
|
74
|
+
const s = redact(x).replace(/\s+/g, " ").trim()
|
|
75
|
+
// a name that held a secret says nothing useful and would link unrelated items together
|
|
76
|
+
if (!s || s.length > MAX_ENTITY_NAME || CONTROL.test(s) || s.includes("[redacted]")) return undefined
|
|
77
|
+
return normKey(s) ? s : undefined
|
|
78
|
+
}
|
|
79
|
+
const cleanType = (x: unknown): string | undefined => {
|
|
80
|
+
if (typeof x !== "string") return undefined
|
|
81
|
+
const s = x.trim().toLowerCase()
|
|
82
|
+
return /^[a-z0-9][a-z0-9_ -]{0,39}$/.test(s) ? s : undefined
|
|
83
|
+
}
|
|
84
|
+
const cleanPredicate = (x: unknown): string | undefined => {
|
|
85
|
+
if (typeof x !== "string") return undefined
|
|
86
|
+
const s = x.trim().toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 60)
|
|
87
|
+
return s || undefined
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The usable, redacted entities and relations of a request item. Malformed entries are dropped, never an error (they were always
|
|
91
|
+
* accepted and ignored before), and the lists are capped. */
|
|
92
|
+
export function cleanExtras(entities: unknown, relations: unknown): Extras {
|
|
93
|
+
const es = new Map<string, Entity>()
|
|
94
|
+
if (Array.isArray(entities)) {
|
|
95
|
+
for (const e of entities) {
|
|
96
|
+
if (es.size >= MAX_ENTITIES) break
|
|
97
|
+
if (typeof e !== "object" || e === null) continue
|
|
98
|
+
const name = cleanName((e as Record<string, unknown>).name)
|
|
99
|
+
if (!name) continue
|
|
100
|
+
const type = cleanType((e as Record<string, unknown>).type)
|
|
101
|
+
const k = normKey(name)
|
|
102
|
+
if (!es.has(k)) es.set(k, { name, ...(type ? { type } : {}) })
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const rs = new Map<string, Relation>()
|
|
106
|
+
if (Array.isArray(relations)) {
|
|
107
|
+
for (const r of relations) {
|
|
108
|
+
if (rs.size >= MAX_RELATIONS) break
|
|
109
|
+
if (typeof r !== "object" || r === null) continue
|
|
110
|
+
const o = r as Record<string, unknown>
|
|
111
|
+
const from = cleanName(o.from)
|
|
112
|
+
const to = cleanName(o.to)
|
|
113
|
+
const type = cleanPredicate(o.type)
|
|
114
|
+
if (!from || !to || !type) continue
|
|
115
|
+
rs.set(`${normKey(from)}\u0000${type}\u0000${normKey(to)}`, { from, to, type })
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return { entities: [...es.values()], relations: [...rs.values()] }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---- principals ------------------------------------------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
export const projectHash = (project: string): string => sha256Hex(project).slice(0, 32)
|
|
124
|
+
export const synthPrincipal = (user: string, project: string): string => `sy:${user}:${projectHash(project)}`
|
|
125
|
+
const groupOf = {
|
|
126
|
+
project: (h: string) => `pj:${h}`,
|
|
127
|
+
team: (h: string, team: string) => `tm:${h}:${team}`,
|
|
128
|
+
org: (h: string) => `og:${h}`,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Who gets a permission edge to an item: the owner (two principals) and the group of its scope. */
|
|
132
|
+
export function aclPrincipals(row: Pick<ItemRow, "owner" | "project" | "scope" | "team">): string[] {
|
|
133
|
+
const h = projectHash(row.project)
|
|
134
|
+
const out = [`u:${row.owner}`, `sy:${row.owner}:${h}`]
|
|
135
|
+
if (row.scope === "project") out.push(groupOf.project(h))
|
|
136
|
+
else if (row.scope === "team" && row.team) out.push(groupOf.team(h, row.team))
|
|
137
|
+
else if (row.scope === "org") out.push(groupOf.org(h))
|
|
138
|
+
return out
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
interface StoreLike {
|
|
142
|
+
db: Database
|
|
143
|
+
addEdge(src: string, dst: string, label: string): void
|
|
144
|
+
bumpVersion(): void
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const marks = (n: number): string => Array.from({ length: n }, () => "?").join(",")
|
|
148
|
+
|
|
149
|
+
export class OrgGraph {
|
|
150
|
+
constructor(
|
|
151
|
+
private readonly org: OrgDb,
|
|
152
|
+
private readonly store: StoreLike,
|
|
153
|
+
/** Registers a principal as a Chitta user (Chitta's search looks the searching principal up). */
|
|
154
|
+
private readonly registerPrincipal: (id: string) => void = () => {},
|
|
155
|
+
) {}
|
|
156
|
+
private readonly registered = new Set<string>()
|
|
157
|
+
|
|
158
|
+
private get db(): Database {
|
|
159
|
+
return this.org.db
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ---- project-aware ranking -----------------------------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
/** Makes the synthetic principal of (user, project) belong to exactly the groups the user's CURRENT memberships give.
|
|
165
|
+
* Synchronous; returns the principal to search as. */
|
|
166
|
+
syncSynthetic(userId: string, project: string): string {
|
|
167
|
+
const h = projectHash(project)
|
|
168
|
+
const s = `sy:${userId}:${h}`
|
|
169
|
+
if (!this.registered.has(s)) (this.registerPrincipal(s), this.registered.add(s))
|
|
170
|
+
const want = new Set<string>([groupOf.org(h)])
|
|
171
|
+
if (this.org.isProjectMember(userId, project)) want.add(groupOf.project(h))
|
|
172
|
+
for (const t of this.org.teamsOf(userId)) want.add(groupOf.team(h, t))
|
|
173
|
+
const have = new Set((this.store.db.query("SELECT dst FROM edges WHERE src = ? AND label = 'belongsTo'").all(s) as Array<{ dst: string }>).map((r) => r.dst).filter((d) => /^(?:pj|tm|og):/.test(d)))
|
|
174
|
+
let changed = false
|
|
175
|
+
for (const g of want) if (!have.has(g)) (this.store.addEdge(s, g, "belongsTo"), (changed = true))
|
|
176
|
+
for (const g of have) {
|
|
177
|
+
if (want.has(g)) continue
|
|
178
|
+
this.store.db.query("DELETE FROM edges WHERE src = ? AND dst = ? AND label = 'belongsTo'").run(s, g)
|
|
179
|
+
changed = true
|
|
180
|
+
}
|
|
181
|
+
if (changed) this.store.bumpVersion()
|
|
182
|
+
return s
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** True while the database still carries the pre-1.1 (global team/project/anyone group) ACL. */
|
|
186
|
+
aclIsLegacy(): boolean {
|
|
187
|
+
return (this.db.query("SELECT value FROM org_meta WHERE key = 'acl_scheme'").get() as { value: string } | null)?.value !== "2"
|
|
188
|
+
}
|
|
189
|
+
markAclCurrent(): void {
|
|
190
|
+
this.db.query("INSERT INTO org_meta (key, value) VALUES ('acl_scheme', '2') ON CONFLICT(key) DO UPDATE SET value = excluded.value").run()
|
|
191
|
+
}
|
|
192
|
+
/** Removes what the old scheme left in Chitta's tables: the global `team:`/`proj:` group nodes and edges. */
|
|
193
|
+
dropLegacyGroups(): void {
|
|
194
|
+
const q = (sql: string) => this.store.db.query(sql).run()
|
|
195
|
+
q("DELETE FROM edges WHERE dst LIKE 'team:%' OR dst LIKE 'proj:%' OR src LIKE 'team:%' OR src LIKE 'proj:%'")
|
|
196
|
+
q("DELETE FROM nodes WHERE coll = 'groups' AND (id LIKE 'team:%' OR id LIKE 'proj:%')")
|
|
197
|
+
this.store.bumpVersion()
|
|
198
|
+
}
|
|
199
|
+
allItemRows(): ItemRow[] {
|
|
200
|
+
return this.db.query("SELECT * FROM org_items").all() as ItemRow[]
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ---- storing entities and relations ---------------------------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
/** Replaces the item's entities and relations with these (an item stands for what its latest ingest said). */
|
|
206
|
+
saveExtras(rid: string, x: Extras): void {
|
|
207
|
+
this.org.tx(() => {
|
|
208
|
+
this.db.query("DELETE FROM org_item_entities WHERE rid = ?").run(rid)
|
|
209
|
+
this.db.query("DELETE FROM org_item_relations WHERE rid = ?").run(rid)
|
|
210
|
+
for (const e of x.entities) this.db.query("INSERT OR IGNORE INTO org_item_entities (rid, ekey, name, type) VALUES (?,?,?,?)").run(rid, normKey(e.name), e.name, e.type ?? null)
|
|
211
|
+
for (const r of x.relations) {
|
|
212
|
+
this.db.query("INSERT OR IGNORE INTO org_item_relations (rid, from_key, from_name, to_key, to_name, type) VALUES (?,?,?,?,?,?)").run(rid, normKey(r.from), r.from, normKey(r.to), r.to, r.type)
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
/** What an item stored (for re-checking the policy on promote). Callers must already have the right to this row. */
|
|
217
|
+
loadExtras(rid: string): Extras {
|
|
218
|
+
const entities = (this.db.query("SELECT name, type FROM org_item_entities WHERE rid = ? ORDER BY ekey").all(rid) as Array<{ name: string; type: string | null }>).map((e) => ({ name: e.name, ...(e.type ? { type: e.type } : {}) }))
|
|
219
|
+
const relations = (this.db.query("SELECT from_name AS \"from\", to_name AS \"to\", type FROM org_item_relations WHERE rid = ? ORDER BY from_key, type, to_key").all(rid) as Relation[])
|
|
220
|
+
return { entities, relations }
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ---- recall expansion ----------------------------------------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Other items of `project` that `user` may see and that share an entity or relation endpoint with the items `tops` (which the
|
|
227
|
+
* caller already may see), best overlap first. `exclude` are rids already in the answer. At most `max` rows.
|
|
228
|
+
* The visibility predicate is part of the SAME statement that counts the overlap, so an invisible item contributes nothing.
|
|
229
|
+
*/
|
|
230
|
+
expansion(user: string, project: string, tops: ItemRow[], exclude: Set<string>, max: number): Array<{ rid: string; overlap: number }> {
|
|
231
|
+
if (max <= 0 || !tops.length) return []
|
|
232
|
+
const topIds = tops.map((t) => t.rid)
|
|
233
|
+
const skip = new Set(tops.map((t) => normKey(t.label)).concat(normKey(project)))
|
|
234
|
+
const keys = new Set<string>()
|
|
235
|
+
const add = (k: string) => k && !skip.has(k) && keys.add(k)
|
|
236
|
+
for (const r of this.db.query(`SELECT ekey FROM org_item_entities WHERE rid IN (${marks(topIds.length)})`).all(...(topIds as never[])) as Array<{ ekey: string }>) add(r.ekey)
|
|
237
|
+
for (const r of this.db.query(`SELECT from_key f, to_key t FROM org_item_relations WHERE rid IN (${marks(topIds.length)})`).all(...(topIds as never[])) as Array<{ f: string; t: string }>) (add(r.f), add(r.t))
|
|
238
|
+
if (!keys.size) return []
|
|
239
|
+
const kl = [...keys].slice(0, 200)
|
|
240
|
+
const params: Record<string, string | number> = { $u: user, $p: project, $n: max }
|
|
241
|
+
const kn = kl.map((k, i) => ((params[`$k${i}`] = k), `$k${i}`)).join(",")
|
|
242
|
+
const ex = [...new Set([...exclude, ...topIds])]
|
|
243
|
+
const en = ex.map((r, i) => ((params[`$x${i}`] = r), `$x${i}`)).join(",")
|
|
244
|
+
const sql = `SELECT org_items.rid AS rid, count(DISTINCT x.k) AS overlap FROM (
|
|
245
|
+
SELECT rid, ekey AS k FROM org_item_entities WHERE ekey IN (${kn})
|
|
246
|
+
UNION ALL SELECT rid, from_key FROM org_item_relations WHERE from_key IN (${kn})
|
|
247
|
+
UNION ALL SELECT rid, to_key FROM org_item_relations WHERE to_key IN (${kn})
|
|
248
|
+
) x JOIN org_items ON org_items.rid = x.rid
|
|
249
|
+
WHERE org_items.project = $p AND ${ex.length ? `org_items.rid NOT IN (${en}) AND ` : ""}${VISIBLE_SQL}
|
|
250
|
+
GROUP BY org_items.rid ORDER BY overlap DESC, org_items.stored_at DESC, org_items.rid LIMIT $n`
|
|
251
|
+
return this.db.query(sql).all(params as never) as Array<{ rid: string; overlap: number }>
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ---- GET /v1/graph -------------------------------------------------------------------------------------------------------
|
|
255
|
+
|
|
256
|
+
view(user: string, project: string, entity?: string): GraphView | GraphEntityView {
|
|
257
|
+
const p = { $u: user, $p: project }
|
|
258
|
+
if (entity === undefined) {
|
|
259
|
+
const rows = this.db
|
|
260
|
+
.query(
|
|
261
|
+
`SELECT ekey, count(DISTINCT org_item_entities.rid) AS items, min(org_item_entities.name) AS name, min(org_item_entities.type) AS type
|
|
262
|
+
FROM org_item_entities JOIN org_items ON org_items.rid = org_item_entities.rid
|
|
263
|
+
WHERE org_items.project = $p AND ${VISIBLE_SQL} GROUP BY ekey ORDER BY items DESC, ekey LIMIT 100`,
|
|
264
|
+
)
|
|
265
|
+
.all(p as never) as Array<{ items: number; name: string; type: string | null }>
|
|
266
|
+
return { project, entities: rows.map((r) => ({ name: redact(r.name), ...(r.type ? { type: r.type } : {}), items: r.items })) }
|
|
267
|
+
}
|
|
268
|
+
const key = normKey(entity)
|
|
269
|
+
const kp = { ...p, $k: key }
|
|
270
|
+
const mention = `SELECT rid, name, type FROM org_item_entities WHERE ekey = $k
|
|
271
|
+
UNION ALL SELECT rid, from_name, NULL FROM org_item_relations WHERE from_key = $k
|
|
272
|
+
UNION ALL SELECT rid, to_name, NULL FROM org_item_relations WHERE to_key = $k`
|
|
273
|
+
const head = key
|
|
274
|
+
? (this.db
|
|
275
|
+
.query(`SELECT count(DISTINCT m.rid) AS items, min(m.name) AS name, min(m.type) AS type FROM (${mention}) m JOIN org_items ON org_items.rid = m.rid WHERE org_items.project = $p AND ${VISIBLE_SQL}`)
|
|
276
|
+
.get(kp as never) as { items: number; name: string | null; type: string | null })
|
|
277
|
+
: { items: 0, name: null, type: null }
|
|
278
|
+
if (!head.items || head.name === null) return { project, entity: null, neighbours: [], items: [] }
|
|
279
|
+
const nb = this.db
|
|
280
|
+
.query(
|
|
281
|
+
`SELECT n.other_name AS name, n.type AS relation, n.dir AS direction, count(DISTINCT n.rid) AS items FROM (
|
|
282
|
+
SELECT rid, to_name AS other_name, to_key AS other_key, type, 'out' AS dir FROM org_item_relations WHERE from_key = $k
|
|
283
|
+
UNION ALL SELECT rid, from_name, from_key, type, 'in' FROM org_item_relations WHERE to_key = $k) n
|
|
284
|
+
JOIN org_items ON org_items.rid = n.rid WHERE org_items.project = $p AND ${VISIBLE_SQL}
|
|
285
|
+
GROUP BY n.other_key, n.type, n.dir ORDER BY items DESC, n.other_key, n.type LIMIT 100`,
|
|
286
|
+
)
|
|
287
|
+
.all(kp as never) as Array<{ name: string; relation: string; direction: "out" | "in"; items: number }>
|
|
288
|
+
const items = this.db
|
|
289
|
+
.query(
|
|
290
|
+
`SELECT DISTINCT org_items.item_id, org_items.name, org_items.kind, org_items.scope, org_items.owner, org_items.team, org_items.source, org_items.stored_at, org_items.rid
|
|
291
|
+
FROM (${mention}) m JOIN org_items ON org_items.rid = m.rid WHERE org_items.project = $p AND ${VISIBLE_SQL}
|
|
292
|
+
ORDER BY org_items.stored_at DESC, org_items.rid LIMIT 50`,
|
|
293
|
+
)
|
|
294
|
+
.all(kp as never) as Array<{ item_id: string; name: string; kind: string; scope: Scope; owner: string; team: string | null; source: string }>
|
|
295
|
+
return {
|
|
296
|
+
project,
|
|
297
|
+
entity: { name: redact(head.name), ...(head.type ? { type: head.type } : {}), items: head.items },
|
|
298
|
+
neighbours: nb.map((r) => ({ name: redact(r.name), relation: r.relation, direction: r.direction, items: r.items })),
|
|
299
|
+
items: items.map((r) => ({ id: r.item_id, name: r.name, kind: r.kind, scope: r.scope, owner: r.owner, ...(r.team ? { team: r.team } : {}), source: r.source })),
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export interface GraphView {
|
|
305
|
+
project: string
|
|
306
|
+
entities: Array<{ name: string; type?: string; items: number }>
|
|
307
|
+
}
|
|
308
|
+
export interface GraphEntityView {
|
|
309
|
+
project: string
|
|
310
|
+
entity: { name: string; type?: string; items: number } | null
|
|
311
|
+
neighbours: Array<{ name: string; relation: string; direction: "out" | "in"; items: number }>
|
|
312
|
+
items: Array<{ id: string; name: string; kind: string; scope: Scope; owner: string; team?: string; source: string }>
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** The score of a graph-expanded item: below every direct hit (half the lowest), higher with more shared entities. */
|
|
316
|
+
export function expansionScore(lowestDirect: number, overlap: number): number {
|
|
317
|
+
return lowestDirect * 0.5 * (overlap / (overlap + 1))
|
|
318
|
+
}
|