Skip to content
238 linesCodeBlameRaw

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 API1/**
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
9import type { EntityKind, RelationKind } from "@g1t/contracts";
10
11import type { FileFacts, Hint } from "./extract";
Merge memory from docs: only docs on how to work here, whole sentences, no near-duplicates, stale doc suggestions cleared12import { harvestable } from "./harvest.ts";
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API13
14export 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
27export type FileRecord = { path: string; facts: FileFacts };
28
29export type Surroundings = {
30 /** Members who own it: named in its files, or who wrote most of it. */
31 owners: string[];
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
41export 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
50export type Ref = { kind: EntityKind; key: string };
51export type RelationDraft = { from: Ref; kind: RelationKind; to: Ref };
52
53export 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
62function unique<T>(values: T[]): T[] {
63 return [...new Set(values)];
64}
65
66/** The catalog entries for one project. */
67export 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 owners.length ? `Owned by ${owners.join(", ")}.` : null,
93 ]
94 .filter(Boolean)
95 .join(" ") || null,
96 data: {
97 repo: `${project.repo.namespace}/${project.repo.name}`,
98 rootDir: project.rootDir,
99 defaultBranch: project.defaultBranch,
100 languages,
101 owners,
102 tests,
103 testCommands: packages.flatMap((pkg) => (pkg.scripts?.test ? [`${pkg.ecosystem === "npm" ? "npm test" : pkg.scripts.test}`] : [])),
104 productionUrl: around.deploy?.production?.url ?? null,
105 },
106 ref: `/${project.repo.namespace}/${project.repo.name}${project.rootDir ? `/tree/${project.defaultBranch}/${project.rootDir}` : ""}`,
107 });
108
109 for (const owner of owners) {
110 entities.push({ kind: "owner", key: owner.toLowerCase(), name: owner, summary: null, data: {}, ref: `/u/${owner}` });
111 relate(me, "owned_by", { kind: "owner", key: owner.toLowerCase() });
112 }
113 for (const language of languages) {
114 entities.push({ kind: "language", key: language.toLowerCase(), name: language, summary: null, data: {}, ref: null });
115 relate(me, "uses", { kind: "language", key: language.toLowerCase() });
116 }
117 for (const pkg of packages) {
118 const key = `${pkg.ecosystem}:${pkg.name}`;
119 entities.push({
120 kind: "package",
121 key,
122 name: pkg.name,
123 summary: `${pkg.ecosystem} package${pkg.version ? ` ${pkg.version}` : ""} in ${project.name}; depends on ${pkg.dependencies.length} packages.`,
124 data: { ecosystem: pkg.ecosystem, version: pkg.version, dependencies: pkg.dependencies, scripts: pkg.scripts ?? {}, members: pkg.members ?? [], path: pkg.path },
125 ref: `/${project.repo.namespace}/${project.repo.name}/blob/${project.defaultBranch}/${[project.rootDir, pkg.path].filter(Boolean).join("/")}`,
126 });
127 relate(me, "exposes", { kind: "package", key });
128 // Within the workspace, a package using another is a dependency the
129 // catalog can draw; ones from outside have no entity and are dropped
130 // when read.
131 for (const dependency of pkg.dependencies) {
132 relate({ kind: "package", key }, "depends_on", { kind: "package", key: `${pkg.ecosystem}:${dependency}` });
133 }
134 }
135 for (const api of apis) {
136 const key = `${project.slug}:${api.kind}:${api.name}`;
137 entities.push({
138 kind: "api",
139 key,
140 name: api.name,
141 summary: api.summary,
142 data: { kind: api.kind, routes: api.routes, path: api.path },
143 ref: `/${project.repo.namespace}/${project.repo.name}/blob/${project.defaultBranch}/${[project.rootDir, api.path].filter(Boolean).join("/")}`,
144 });
145 relate(me, "exposes", { kind: "api", key });
146 if (hasApp && api.kind === "worker") relate(app, "exposes", { kind: "api", key });
147 }
148 for (const doc of docs) {
149 const key = `${project.slug}:${doc.path}`;
150 entities.push({
151 kind: "doc",
152 key,
153 name: doc.title,
154 summary: doc.summary,
155 data: { path: doc.path, role: doc.role, chunks: doc.chunks.length },
156 ref: `/${project.repo.namespace}/${project.repo.name}/blob/${project.defaultBranch}/${[project.rootDir, doc.path].filter(Boolean).join("/")}`,
157 });
158 relate(me, "documented_by", { kind: "doc", key });
159 }
160 if (hasApp && around.deploy) {
161 const { production, previews, latest } = around.deploy;
162 entities.push({
163 kind: "app",
164 key: project.slug,
165 name: project.name,
166 summary: production ? `Live at ${production.url}.` : "Deploys from its default branch; not live yet.",
167 data: { productionUrl: production?.url ?? null, previews, latest },
168 ref: `/${project.repo.namespace}/${project.repo.name}/deployments`,
169 });
170 relate(me, "exposes", app);
171 entities.push({
172 kind: "environment",
173 key: `${project.slug}/production`,
174 name: `${project.name} production`,
175 summary: production ? `${production.url}, from ${production.commit.slice(0, 7)}` : "Not deployed yet",
176 data: { url: production?.url ?? null, commit: production?.commit ?? null, deployedAt: production?.deployedAt ?? null, status: latest?.kind === "production" ? latest.status : production ? "ready" : null },
177 ref: production?.url ?? null,
178 });
179 relate(app, "deploys_to", { kind: "environment", key: `${project.slug}/production` });
180 if (previews > 0) {
181 entities.push({
182 kind: "environment",
183 key: `${project.slug}/preview`,
184 name: `${project.name} previews`,
185 summary: `${previews} preview${previews === 1 ? "" : "s"} up, one per open pull request`,
186 data: { previews },
187 ref: `/${project.repo.namespace}/${project.repo.name}/deployments`,
188 });
189 relate(app, "deploys_to", { kind: "environment", key: `${project.slug}/preview` });
190 }
191 }
192 const repoPath = `${project.repo.namespace}/${project.repo.name}`.toLowerCase();
193 for (const integration of around.integrations) {
194 if (integration.repo?.toLowerCase() === repoPath) relate(me, "uses", { kind: "integration", key: integration.id });
195 }
196
Merge memory from docs: only docs on how to work here, whole sentences, no near-duplicates, stale doc suggestions cleared197 // A doc is a source of memory only when it says how to work here (see
198 // ./harvest), or project.yml says it is.
199 const memory = files.find((file) => file.facts.memory)?.facts.memory ?? null;
200 const hints = files
201 .filter((file) => !file.facts.doc || harvestable(file.path, file.facts.doc.role, memory))
202 .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 API203 return { entities, relations, hints, tests };
204}
205
206/** A workspace's integrations, as catalog entries of their own. */
207export function integrationEntities(integrations: Surroundings["integrations"]): EntityDraft[] {
208 return integrations.map((integration) => ({
209 kind: "integration",
210 key: integration.id,
211 name: integration.name,
212 summary: `${integration.provider} (${integration.kind})${integration.repo ? `, opens issues on ${integration.repo}` : ""}`,
213 data: { provider: integration.provider, kind: integration.kind, repo: integration.repo },
214 ref: null,
215 }));
216}
217
218/**
219 * The members who wrote most of a project, from its recent commits: those
220 * with at least a fifth of them, at most three. A commit's author is a
221 * member when its name or its email's local part is their username.
222 */
223export function authorsOf(commits: { author: { name: string; email: string } }[], members: string[]): string[] {
224 const byName = new Map(members.map((member) => [member.toLowerCase(), member]));
225 const counts = new Map<string, number>();
226 for (const commit of commits) {
227 const name = commit.author.name.toLowerCase();
228 const local = commit.author.email.split("@")[0]?.toLowerCase() ?? "";
229 const member = byName.get(name) ?? byName.get(local);
230 if (member) counts.set(member, (counts.get(member) ?? 0) + 1);
231 }
232 const total = commits.length || 1;
233 return [...counts.entries()]
234 .filter(([, count]) => count / total >= 0.2)
235 .sort((a, b) => b[1] - a[1])
236 .slice(0, 3)
237 .map(([member]) => member);
238}

This file's history is long; its oldest lines are credited to the oldest commit read.