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