g1t/services/context/src/assemble.ts
Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 1 | /** |
| 2 | * A project's place in the catalog, put together from what its files say | |
| 3 | * (see `./extract`) and what g1t knows about it already: its dependencies, | |
| 4 | * owners, deployments and integrations. Pure, so a rebuild from the same | |
| 5 | * inputs gives the same entities and relations, and writing them is | |
| 6 | * idempotent. | |
| 7 | */ | |
| 8 | ||
| 9 | import type { EntityKind, RelationKind } from "@g1t/contracts"; | |
| 10 | ||
| 11 | import type { FileFacts, Hint } from "./extract"; | |
| 12 | ||
| 13 | export type ProjectInput = { | |
| 14 | id: string; | |
| 15 | workspace: string; | |
| 16 | slug: string; | |
| 17 | name: string; | |
| 18 | description: string | null; | |
| 19 | private: boolean; | |
| 20 | repoId: string; | |
| 21 | repo: { namespace: string; name: string }; | |
| 22 | rootDir: string; | |
| 23 | defaultBranch: string; | |
| 24 | }; | |
| 25 | ||
| 26 | export type FileRecord = { path: string; facts: FileFacts }; | |
| 27 | ||
| 28 | export type Surroundings = { | |
| 29 | /** Members who own it: named in its files, or who wrote most of it. */ | |
| 30 | owners: string[]; | |
| 31 | dependsOn: { slug: string; as: string | null }[]; | |
| 32 | deploy: { | |
| 33 | enabled: boolean; | |
| 34 | production: { url: string; commit: string; deployedAt: string } | null; | |
| 35 | previews: number; | |
| 36 | latest: { status: string; kind: string; error: string | null; createdAt: string } | null; | |
| 37 | } | null; | |
| 38 | integrations: { id: string; provider: string; name: string; kind: string; repo: string | null }[]; | |
| 39 | }; | |
| 40 | ||
| 41 | export type EntityDraft = { | |
| 42 | kind: EntityKind; | |
| 43 | key: string; | |
| 44 | name: string; | |
| 45 | summary: string | null; | |
| 46 | data: Record<string, unknown>; | |
| 47 | ref: string | null; | |
| 48 | }; | |
| 49 | ||
| 50 | export type Ref = { kind: EntityKind; key: string }; | |
| 51 | export type RelationDraft = { from: Ref; kind: RelationKind; to: Ref }; | |
| 52 | ||
| 53 | export type Assembled = { | |
| 54 | entities: EntityDraft[]; | |
| 55 | relations: RelationDraft[]; | |
| 56 | /** Stack facts and doc hints, with the file each came from. */ | |
| 57 | hints: (Hint & { path: string })[]; | |
| 58 | /** Whether a workflow runs tests. */ | |
| 59 | tests: boolean; | |
| 60 | }; | |
| 61 | ||
| 62 | function unique<T>(values: T[]): T[] { | |
| 63 | return [...new Set(values)]; | |
| 64 | } | |
| 65 | ||
| 66 | /** The catalog entries for one project. */ | |
| 67 | export function assemble(project: ProjectInput, files: FileRecord[], around: Surroundings): Assembled { | |
| 68 | const me: Ref = { kind: "project", key: project.slug }; | |
| 69 | const entities: EntityDraft[] = []; | |
| 70 | const relations: RelationDraft[] = []; | |
| 71 | const relate = (from: Ref, kind: RelationKind, to: Ref) => relations.push({ from, kind, to }); | |
| 72 | ||
| 73 | const languages = unique(files.flatMap((file) => file.facts.languages)); | |
| 74 | const packages = files.flatMap((file) => file.facts.packages.map((pkg) => ({ ...pkg, path: file.path }))); | |
| 75 | const apis = files.flatMap((file) => file.facts.apis.map((api) => ({ ...api, path: file.path }))); | |
| 76 | const docs = files.flatMap((file) => (file.facts.doc ? [file.facts.doc] : [])); | |
| 77 | const tests = files.some((file) => file.facts.tests); | |
| 78 | const declaredOwners = unique(files.flatMap((file) => file.facts.owners)); | |
| 79 | const owners = unique([...declaredOwners, ...around.owners]); | |
| 80 | ||
| 81 | const app: Ref = { kind: "app", key: project.slug }; | |
| 82 | const hasApp = !!around.deploy?.enabled; | |
| 83 | entities.push({ | |
| 84 | kind: "project", | |
| 85 | key: project.slug, | |
| 86 | name: project.name, | |
| 87 | summary: | |
| 88 | [ | |
| 89 | project.description, | |
| 90 | languages.length ? `Written in ${languages.join(", ")}.` : null, | |
| 91 | packages.length ? `Packages: ${packages.map((pkg) => pkg.name).join(", ")}.` : null, | |
| 92 | around.dependsOn.length ? `Uses ${around.dependsOn.map((dep) => dep.slug).join(", ")}.` : null, | |
| 93 | owners.length ? `Owned by ${owners.join(", ")}.` : null, | |
| 94 | ] | |
| 95 | .filter(Boolean) | |
| 96 | .join(" ") || null, | |
| 97 | data: { | |
| 98 | repo: `${project.repo.namespace}/${project.repo.name}`, | |
| 99 | rootDir: project.rootDir, | |
| 100 | defaultBranch: project.defaultBranch, | |
| 101 | languages, | |
| 102 | owners, | |
| 103 | tests, | |
| 104 | testCommands: packages.flatMap((pkg) => (pkg.scripts?.test ? [`${pkg.ecosystem === "npm" ? "npm test" : pkg.scripts.test}`] : [])), | |
| 105 | productionUrl: around.deploy?.production?.url ?? null, | |
| 106 | }, | |
| 107 | ref: `/${project.repo.namespace}/${project.repo.name}${project.rootDir ? `/tree/${project.defaultBranch}/${project.rootDir}` : ""}`, | |
| 108 | }); | |
| 109 | ||
| 110 | for (const dep of around.dependsOn) { | |
| 111 | relate(me, "depends_on", { kind: "project", key: dep.slug }); | |
| 112 | } | |
| 113 | for (const owner of owners) { | |
| 114 | entities.push({ kind: "owner", key: owner.toLowerCase(), name: owner, summary: null, data: {}, ref: `/u/${owner}` }); | |
| 115 | relate(me, "owned_by", { kind: "owner", key: owner.toLowerCase() }); | |
| 116 | } | |
| 117 | for (const language of languages) { | |
| 118 | entities.push({ kind: "language", key: language.toLowerCase(), name: language, summary: null, data: {}, ref: null }); | |
| 119 | relate(me, "uses", { kind: "language", key: language.toLowerCase() }); | |
| 120 | } | |
| 121 | for (const pkg of packages) { | |
| 122 | const key = `${pkg.ecosystem}:${pkg.name}`; | |
| 123 | entities.push({ | |
| 124 | kind: "package", | |
| 125 | key, | |
| 126 | name: pkg.name, | |
| 127 | summary: `${pkg.ecosystem} package${pkg.version ? ` ${pkg.version}` : ""} in ${project.name}; depends on ${pkg.dependencies.length} packages.`, | |
| 128 | data: { ecosystem: pkg.ecosystem, version: pkg.version, dependencies: pkg.dependencies, scripts: pkg.scripts ?? {}, members: pkg.members ?? [], path: pkg.path }, | |
| 129 | ref: `/${project.repo.namespace}/${project.repo.name}/blob/${project.defaultBranch}/${[project.rootDir, pkg.path].filter(Boolean).join("/")}`, | |
| 130 | }); | |
| 131 | relate(me, "exposes", { kind: "package", key }); | |
| 132 | // Within the workspace, a package using another is a dependency the | |
| 133 | // catalog can draw; ones from outside have no entity and are dropped | |
| 134 | // when read. | |
| 135 | for (const dependency of pkg.dependencies) { | |
| 136 | relate({ kind: "package", key }, "depends_on", { kind: "package", key: `${pkg.ecosystem}:${dependency}` }); | |
| 137 | } | |
| 138 | } | |
| 139 | for (const api of apis) { | |
| 140 | const key = `${project.slug}:${api.kind}:${api.name}`; | |
| 141 | entities.push({ | |
| 142 | kind: "api", | |
| 143 | key, | |
| 144 | name: api.name, | |
| 145 | summary: api.summary, | |
| 146 | data: { kind: api.kind, routes: api.routes, path: api.path }, | |
| 147 | ref: `/${project.repo.namespace}/${project.repo.name}/blob/${project.defaultBranch}/${[project.rootDir, api.path].filter(Boolean).join("/")}`, | |
| 148 | }); | |
| 149 | relate(me, "exposes", { kind: "api", key }); | |
| 150 | if (hasApp && api.kind === "worker") relate(app, "exposes", { kind: "api", key }); | |
| 151 | } | |
| 152 | for (const doc of docs) { | |
| 153 | const key = `${project.slug}:${doc.path}`; | |
| 154 | entities.push({ | |
| 155 | kind: "doc", | |
| 156 | key, | |
| 157 | name: doc.title, | |
| 158 | summary: doc.summary, | |
| 159 | data: { path: doc.path, role: doc.role, chunks: doc.chunks.length }, | |
| 160 | ref: `/${project.repo.namespace}/${project.repo.name}/blob/${project.defaultBranch}/${[project.rootDir, doc.path].filter(Boolean).join("/")}`, | |
| 161 | }); | |
| 162 | relate(me, "documented_by", { kind: "doc", key }); | |
| 163 | } | |
| 164 | if (hasApp && around.deploy) { | |
| 165 | const { production, previews, latest } = around.deploy; | |
| 166 | entities.push({ | |
| 167 | kind: "app", | |
| 168 | key: project.slug, | |
| 169 | name: project.name, | |
| 170 | summary: production ? `Live at ${production.url}.` : "Deploys from its default branch; not live yet.", | |
| 171 | data: { productionUrl: production?.url ?? null, previews, latest }, | |
| 172 | ref: `/${project.repo.namespace}/${project.repo.name}/deployments`, | |
| 173 | }); | |
| 174 | relate(me, "exposes", app); | |
| 175 | entities.push({ | |
| 176 | kind: "environment", | |
| 177 | key: `${project.slug}/production`, | |
| 178 | name: `${project.name} production`, | |
| 179 | summary: production ? `${production.url}, from ${production.commit.slice(0, 7)}` : "Not deployed yet", | |
| 180 | data: { url: production?.url ?? null, commit: production?.commit ?? null, deployedAt: production?.deployedAt ?? null, status: latest?.kind === "production" ? latest.status : production ? "ready" : null }, | |
| 181 | ref: production?.url ?? null, | |
| 182 | }); | |
| 183 | relate(app, "deploys_to", { kind: "environment", key: `${project.slug}/production` }); | |
| 184 | if (previews > 0) { | |
| 185 | entities.push({ | |
| 186 | kind: "environment", | |
| 187 | key: `${project.slug}/preview`, | |
| 188 | name: `${project.name} previews`, | |
| 189 | summary: `${previews} preview${previews === 1 ? "" : "s"} up, one per open pull request`, | |
| 190 | data: { previews }, | |
| 191 | ref: `/${project.repo.namespace}/${project.repo.name}/deployments`, | |
| 192 | }); | |
| 193 | relate(app, "deploys_to", { kind: "environment", key: `${project.slug}/preview` }); | |
| 194 | } | |
| 195 | } | |
| 196 | const repoPath = `${project.repo.namespace}/${project.repo.name}`.toLowerCase(); | |
| 197 | for (const integration of around.integrations) { | |
| 198 | if (integration.repo?.toLowerCase() === repoPath) relate(me, "uses", { kind: "integration", key: integration.id }); | |
| 199 | } | |
| 200 | ||
| 201 | const hints = files.flatMap((file) => file.facts.hints.map((hint) => ({ ...hint, path: file.path }))); | |
| 202 | return { entities, relations, hints, tests }; | |
| 203 | } | |
| 204 | ||
| 205 | /** A workspace's integrations, as catalog entries of their own. */ | |
| 206 | export function integrationEntities(integrations: Surroundings["integrations"]): EntityDraft[] { | |
| 207 | return integrations.map((integration) => ({ | |
| 208 | kind: "integration", | |
| 209 | key: integration.id, | |
| 210 | name: integration.name, | |
| 211 | summary: `${integration.provider} (${integration.kind})${integration.repo ? `, opens issues on ${integration.repo}` : ""}`, | |
| 212 | data: { provider: integration.provider, kind: integration.kind, repo: integration.repo }, | |
| 213 | ref: null, | |
| 214 | })); | |
| 215 | } | |
| 216 | ||
| 217 | /** | |
| 218 | * The members who wrote most of a project, from its recent commits: those | |
| 219 | * with at least a fifth of them, at most three. A commit's author is a | |
| 220 | * member when its name or its email's local part is their username. | |
| 221 | */ | |
| 222 | export function authorsOf(commits: { author: { name: string; email: string } }[], members: string[]): string[] { | |
| 223 | const byName = new Map(members.map((member) => [member.toLowerCase(), member])); | |
| 224 | const counts = new Map<string, number>(); | |
| 225 | for (const commit of commits) { | |
| 226 | const name = commit.author.name.toLowerCase(); | |
| 227 | const local = commit.author.email.split("@")[0]?.toLowerCase() ?? ""; | |
| 228 | const member = byName.get(name) ?? byName.get(local); | |
| 229 | if (member) counts.set(member, (counts.get(member) ?? 0) + 1); | |
| 230 | } | |
| 231 | const total = commits.length || 1; | |
| 232 | return [...counts.entries()] | |
| 233 | .filter(([, count]) => count / total >= 0.2) | |
| 234 | .sort((a, b) => b[1] - a[1]) | |
| 235 | .slice(0, 3) | |
| 236 | .map(([member]) => member); | |
| 237 | } |