Skip to content
578 linesCodeBlameRaw
1/**
2 * What a project's files say about it, read without running anything: the
3 * languages and packages in its manifests, the APIs it exposes, its docs,
4 * whether its workflows run tests, who owns it, and short facts and
5 * conventions worth remembering. Pure: given a file's path and text, the
6 * same facts every time, so a file whose blob has not changed is never read
7 * again.
8 */
9
10import { parse as parseYaml } from "yaml";
11
12import { aspirational, bulletsOf, classify, bulletSection, MAX_DOC_HINT_CHARS, planLike, planSection, plain, SETUP_HEADING, wholeSentences, type MemoryDocsConfig } from "./harvest.ts";
13
14export type Ecosystem = "npm" | "cargo" | "go" | "pypi";
15
16export type PackageFact = {
17 ecosystem: Ecosystem;
18 name: string;
19 version: string | null;
20 /** Its dependencies' names, at most `MAX_DEPENDENCIES`. */
21 dependencies: string[];
22 /** For npm: the scripts people run. */
23 scripts?: Record<string, string>;
24 /** For a monorepo's root: where its packages are. */
25 members?: string[];
26};
27
28export type ApiFact = {
29 /** `openapi` for a described HTTP API, `worker` for a Worker's routes. */
30 kind: "openapi" | "worker";
31 name: string;
32 summary: string | null;
33 /** `GET /users`, or a route pattern such as `api.example.com/*`. */
34 routes: string[];
35};
36
37export type DocFact = {
38 path: string;
39 title: string;
40 summary: string | null;
41 /** The doc in pieces of at most `CHUNK_CHARS`, each starting with its heading. */
42 chunks: string[];
43 /** README, AGENTS.md (or CLAUDE.md), CONTRIBUTING, or another doc. */
44 role: "readme" | "agents" | "contributing" | "doc";
45};
46
47/** Something worth remembering, as a memory candidate. */
48export type Hint = {
49 kind: "fact" | "convention" | "decision" | "gotcha";
50 text: string;
51 /** 0 to 1. A doc's hint at or above 0.85 is kept without review. */
52 confidence: number;
53 evidence: string;
54};
55
56export type FileFacts = {
57 languages: string[];
58 packages: PackageFact[];
59 apis: ApiFact[];
60 doc: DocFact | null;
61 /** A workflow that runs tests. */
62 tests: boolean;
63 /** Usernames this file names as owners. */
64 owners: string[];
65 hints: Hint[];
66 /** For `.g1t/project.yml`: which docs memory is suggested from. */
67 memory?: MemoryDocsConfig | null;
68 /** The `EXTRACT_VERSION` that read it; facts from an older one are read again. */
69 version?: number;
70};
71
72/** What extraction knows besides the file itself. */
73export type ExtractContext = {
74 /** The project's name, for writing hints. */
75 project: string;
76 /** The names of the files beside the manifest: lockfiles, tsconfig.json. */
77 siblings: string[];
78};
79
80/** Bumped when what is read from a file changes, so stored facts are read again. */
81export const EXTRACT_VERSION = 2;
82export const MAX_DEPENDENCIES = 60;
83export const CHUNK_CHARS = 1500;
84export const MAX_CHUNKS = 12;
85const MAX_ROUTES = 40;
86const MAX_HINTS_PER_FILE = 12;
87const MAX_HINT_CHARS = MAX_DOC_HINT_CHARS;
88
89const EMPTY: FileFacts = { languages: [], packages: [], apis: [], doc: null, tests: false, owners: [], hints: [] };
90
91const DOC_NAME = /^(readme|agents|claude|contributing)(\.(md|markdown|txt))?$/i;
92const OPENAPI_NAME = /^(openapi|swagger)\.(json|ya?ml)$/i;
93const MANIFEST_NAMES = new Set([
94 "package.json",
95 "Cargo.toml",
96 "go.mod",
97 "pyproject.toml",
98 "requirements.txt",
99 "wrangler.jsonc",
100 "wrangler.json",
101 "wrangler.toml",
102 "CODEOWNERS",
103]);
104
105/** The base name of a path. */
106function base(path: string): string {
107 return path.slice(path.lastIndexOf("/") + 1);
108}
109
110/**
111 * Whether a file at `path` (relative to the project's root) is one this
112 * reads: a manifest, an API description, a doc, a workflow or ownership.
113 */
114export function interesting(path: string): boolean {
115 const name = base(path);
116 const dir = path.slice(0, Math.max(0, path.lastIndexOf("/")));
117 if (dir === "" && (MANIFEST_NAMES.has(name) || DOC_NAME.test(name) || OPENAPI_NAME.test(name))) return true;
118 if ((dir === "docs" || dir === "doc" || dir === "runbooks" || dir === "docs/runbooks") && /\.(md|markdown)$/i.test(name)) return true;
119 if ((dir === ".g1t/workflows" || dir === ".github/workflows") && /\.ya?ml$/i.test(name)) return true;
120 if (path === ".g1t/project.yml" || path === ".github/CODEOWNERS" || path === "docs/CODEOWNERS") return true;
121 if (dir === "" && OPENAPI_NAME.test(name)) return true;
122 return false;
123}
124
125/** One line, at most `max` characters. */
126export function clip(text: string, max = MAX_HINT_CHARS): string {
127 const line = text.replace(/\s+/g, " ").trim();
128 return line.length <= max ? line : `${line.slice(0, max - 1).replace(/\s+\S*$/, "")}…`;
129}
130
131/** JSON with comments and trailing commas, as wrangler.jsonc and tsconfig.json allow. */
132export function parseJsonc(text: string): unknown {
133 let out = "";
134 let inString = false;
135 for (let i = 0; i < text.length; i++) {
136 const c = text[i];
137 if (inString) {
138 out += c;
139 if (c === "\\") {
140 out += text[++i] ?? "";
141 } else if (c === '"') {
142 inString = false;
143 }
144 continue;
145 }
146 if (c === '"') {
147 inString = true;
148 out += c;
149 } else if (c === "/" && text[i + 1] === "/") {
150 while (i < text.length && text[i] !== "\n") i++;
151 out += "\n";
152 } else if (c === "/" && text[i + 1] === "*") {
153 i += 2;
154 while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
155 i++;
156 } else {
157 out += c;
158 }
159 }
160 return JSON.parse(out.replace(/,(\s*[}\]])/g, "$1"));
161}
162
163type Toml = Record<string, Record<string, unknown>>;
164
165/**
166 * The little of TOML that manifests use: `[sections]`, `key = "string"`,
167 * numbers, booleans, one-line and multi-line string arrays, and inline
168 * tables kept as text. Keys outside a section are under "".
169 */
170export function parseToml(text: string): Toml {
171 const out: Toml = { "": {} };
172 let section = "";
173 const lines = text.split(/\r?\n/);
174 for (let i = 0; i < lines.length; i++) {
175 let line = lines[i].replace(/(^|\s)#.*$/, "").trim();
176 if (!line) continue;
177 const header = /^\[\[?([^\]]+)\]\]?$/.exec(line);
178 if (header) {
179 section = header[1].trim().replace(/"/g, "");
180 out[section] ??= {};
181 continue;
182 }
183 const eq = line.indexOf("=");
184 if (eq < 0) continue;
185 const key = line.slice(0, eq).trim().replace(/"/g, "");
186 let value = line.slice(eq + 1).trim();
187 // A multi-line array: gather until its closing bracket.
188 if (value.startsWith("[") && !value.includes("]")) {
189 while (i + 1 < lines.length && !value.includes("]")) value += " " + lines[++i].replace(/#.*$/, "").trim();
190 }
191 out[section] ??= {};
192 out[section][key] = tomlValue(value);
193 }
194 return out;
195}
196
197function tomlValue(value: string): unknown {
198 if (value.startsWith('"') || value.startsWith("'")) return value.slice(1, value.lastIndexOf(value[0]));
199 if (value === "true" || value === "false") return value === "true";
200 if (value.startsWith("[")) {
201 return [...value.matchAll(/"([^"]*)"|'([^']*)'/g)].map((m) => m[1] ?? m[2]);
202 }
203 if (/^-?\d+(\.\d+)?$/.test(value)) return Number(value);
204 return value;
205}
206
207function str(value: unknown): string | null {
208 return typeof value === "string" && value.trim() ? value.trim() : null;
209}
210
211function keys(value: unknown): string[] {
212 return value && typeof value === "object" && !Array.isArray(value) ? Object.keys(value) : [];
213}
214
215/** The package manager a JavaScript project's lockfile says it uses. */
216export function packageManager(siblings: string[]): "pnpm" | "yarn" | "bun" | "npm" | null {
217 const has = (name: string) => siblings.includes(name);
218 if (has("pnpm-lock.yaml")) return "pnpm";
219 if (has("yarn.lock")) return "yarn";
220 if (has("bun.lockb") || has("bun.lock")) return "bun";
221 if (has("package-lock.json")) return "npm";
222 return null;
223}
224
225function npm(path: string, text: string, ctx: ExtractContext): FileFacts {
226 const json = JSON.parse(text) as Record<string, unknown>;
227 const scripts = (json.scripts && typeof json.scripts === "object" ? json.scripts : {}) as Record<string, string>;
228 const deps = [...keys(json.dependencies), ...keys(json.devDependencies)];
229 const typescript = deps.includes("typescript") || ctx.siblings.includes("tsconfig.json");
230 const workspaces = Array.isArray(json.workspaces)
231 ? (json.workspaces as unknown[]).filter((w): w is string => typeof w === "string")
232 : Array.isArray((json.workspaces as { packages?: unknown })?.packages)
233 ? ((json.workspaces as { packages: unknown[] }).packages.filter((w): w is string => typeof w === "string"))
234 : [];
235 const pm = packageManager(ctx.siblings);
236 const run = (script: string) => (script === "test" ? `${pm ?? "npm"} test` : `${pm ?? "npm"} run ${script}`);
237 const hints: Hint[] = [];
238 if (pm) {
239 const others = ["npm", "pnpm", "yarn", "bun"].filter((other) => other !== pm).join(" or ");
240 hints.push({
241 kind: "convention",
242 text: `${ctx.project} uses ${pm} (its lockfile is committed): install with \`${pm} install\`, not ${others}.`,
243 confidence: 0.9,
244 evidence: `${path} beside its ${pm} lockfile`,
245 });
246 }
247 for (const script of ["test", "typecheck", "lint", "build"]) {
248 const command = str(scripts[script]);
249 if (!command) continue;
250 hints.push({
251 kind: "fact",
252 text: clip(`In ${ctx.project}, \`${run(script)}\` runs ${script === "test" ? "the tests" : `the ${script}`}: \`${command}\`.`),
253 confidence: 0.9,
254 evidence: `${path} scripts.${script}`,
255 });
256 }
257 if (workspaces.length > 0) {
258 hints.push({
259 kind: "fact",
260 text: clip(`${ctx.project} is a monorepo; its packages are in ${workspaces.join(", ")}.`),
261 confidence: 0.9,
262 evidence: `${path} workspaces`,
263 });
264 }
265 const name = str(json.name);
266 return {
267 ...EMPTY,
268 languages: typescript ? ["TypeScript"] : ["JavaScript"],
269 packages: name
270 ? [
271 {
272 ecosystem: "npm",
273 name,
274 version: str(json.version),
275 dependencies: deps.slice(0, MAX_DEPENDENCIES),
276 scripts: Object.fromEntries(Object.entries(scripts).filter(([, v]) => typeof v === "string").slice(0, 20)),
277 ...(workspaces.length ? { members: workspaces } : {}),
278 },
279 ]
280 : [],
281 hints,
282 };
283}
284
285function cargo(path: string, text: string, ctx: ExtractContext): FileFacts {
286 const toml = parseToml(text);
287 const name = str(toml.package?.name);
288 const members = Array.isArray(toml.workspace?.members) ? (toml.workspace.members as string[]) : [];
289 const deps = [...keys(toml.dependencies), ...keys(toml["dev-dependencies"]), ...keys(toml["workspace.dependencies"])];
290 // Where its crates live (apps/*, crates/*), not each one: the list
291 // changes with every crate added, and the fact does not.
292 const places = [...new Set(members.map((member) => (member.includes("/") ? `${member.slice(0, member.indexOf("/"))}/*` : member)))];
293 const hints: Hint[] = [
294 {
295 kind: "fact",
296 text: clip(
297 members.length
298 ? `${ctx.project} is a Cargo workspace (${places.join(", ")}); \`cargo test\` runs its tests.`
299 : `${ctx.project} is written in Rust; \`cargo test\` runs its tests.`,
300 ),
301 confidence: 0.9,
302 evidence: path,
303 },
304 ];
305 return {
306 ...EMPTY,
307 languages: ["Rust"],
308 packages: name
309 ? [{ ecosystem: "cargo", name, version: str(toml.package?.version), dependencies: [...new Set(deps)].slice(0, MAX_DEPENDENCIES) }]
310 : members.length
311 ? [{ ecosystem: "cargo", name: `${ctx.project} (workspace)`, version: null, dependencies: [...new Set(deps)].slice(0, MAX_DEPENDENCIES), members }]
312 : [],
313 hints,
314 };
315}
316
317function goMod(path: string, text: string, ctx: ExtractContext): FileFacts {
318 const module = /^module\s+(\S+)/m.exec(text)?.[1] ?? null;
319 const version = /^go\s+(\S+)/m.exec(text)?.[1] ?? null;
320 const requires = [...text.matchAll(/^\s*(?:require\s+)?([\w.-]+\.[\w.-]+\/\S+)\s+v[\w.+-]+/gm)].map((m) => m[1]);
321 return {
322 ...EMPTY,
323 languages: ["Go"],
324 packages: module ? [{ ecosystem: "go", name: module, version, dependencies: requires.slice(0, MAX_DEPENDENCIES) }] : [],
325 hints: [
326 {
327 kind: "fact",
328 text: clip(`${ctx.project} is a Go module${module ? ` (${module}${version ? `, go ${version}` : ""})` : ""}; \`go test ./...\` runs its tests.`),
329 confidence: 0.9,
330 evidence: path,
331 },
332 ],
333 };
334}
335
336function python(path: string, text: string, ctx: ExtractContext): FileFacts {
337 let name: string | null = null;
338 let version: string | null = null;
339 let deps: string[] = [];
340 if (base(path) === "pyproject.toml") {
341 const toml = parseToml(text);
342 name = str(toml.project?.name) ?? str(toml["tool.poetry"]?.name);
343 version = str(toml.project?.version) ?? str(toml["tool.poetry"]?.version);
344 deps = [
345 ...(Array.isArray(toml.project?.dependencies) ? (toml.project.dependencies as string[]) : []),
346 ...keys(toml["tool.poetry.dependencies"]),
347 ];
348 } else {
349 deps = text.split(/\r?\n/).map((line) => line.replace(/#.*$/, "").trim()).filter((line) => line && !line.startsWith("-"));
350 }
351 deps = deps.map((dep) => dep.split(/[<>=!~;\[\s]/)[0]).filter(Boolean);
352 const hints: Hint[] = deps.includes("pytest")
353 ? [{ kind: "fact", text: `In ${ctx.project}, \`pytest\` runs the tests.`, confidence: 0.85, evidence: path }]
354 : [];
355 return {
356 ...EMPTY,
357 languages: ["Python"],
358 packages: name ? [{ ecosystem: "pypi", name, version, dependencies: deps.slice(0, MAX_DEPENDENCIES) }] : [],
359 hints,
360 };
361}
362
363function wrangler(path: string, text: string): FileFacts {
364 let config: Record<string, unknown> = {};
365 if (path.endsWith(".toml")) {
366 const toml = parseToml(text);
367 config = { ...toml[""], routes: Array.isArray(toml[""].routes) ? toml[""].routes : [] };
368 if (toml.routes) config.routes = [toml.routes.pattern];
369 } else {
370 config = parseJsonc(text) as Record<string, unknown>;
371 }
372 const routes: string[] = [];
373 const add = (value: unknown) => {
374 const pattern = typeof value === "string" ? value : str((value as { pattern?: unknown })?.pattern);
375 if (pattern) routes.push(pattern);
376 };
377 if (Array.isArray(config.routes)) config.routes.forEach(add);
378 add(config.route);
379 const name = str(config.name);
380 if (!name) return EMPTY;
381 return {
382 ...EMPTY,
383 apis: [
384 {
385 kind: "worker",
386 name,
387 summary: routes.length ? `Served at ${routes.slice(0, 5).join(", ")}` : "A Worker reached through service bindings",
388 routes: routes.slice(0, MAX_ROUTES),
389 },
390 ],
391 };
392}
393
394const METHODS = ["get", "post", "put", "patch", "delete", "head", "options"];
395
396function openapi(path: string, text: string): FileFacts {
397 const spec = (/\.json$/i.test(path) ? JSON.parse(text) : parseYaml(text)) as {
398 info?: { title?: unknown; description?: unknown; version?: unknown };
399 paths?: Record<string, Record<string, unknown>>;
400 };
401 const routes: string[] = [];
402 for (const [route, operations] of Object.entries(spec?.paths ?? {})) {
403 for (const method of Object.keys(operations ?? {})) {
404 if (METHODS.includes(method)) routes.push(`${method.toUpperCase()} ${route}`);
405 }
406 }
407 const title = str(spec?.info?.title) ?? base(path);
408 return {
409 ...EMPTY,
410 apis: [
411 {
412 kind: "openapi",
413 name: title,
414 summary: clip(str(spec?.info?.description) ?? `${routes.length} operations${str(spec?.info?.version) ? `, version ${spec.info!.version}` : ""}`),
415 routes: routes.slice(0, MAX_ROUTES),
416 },
417 ],
418 };
419}
420
421/** A doc split at its headings into pieces of at most `CHUNK_CHARS`. */
422export function chunk(text: string, title: string): string[] {
423 const sections: string[] = [];
424 let current = "";
425 for (const line of text.split(/\r?\n/)) {
426 if (/^#{1,3}\s/.test(line) && current.trim()) {
427 sections.push(current.trim());
428 current = "";
429 }
430 current += line + "\n";
431 }
432 if (current.trim()) sections.push(current.trim());
433 const pieces: string[] = [];
434 for (const section of sections) {
435 const heading = /^#{1,3}\s+(.*)$/m.exec(section)?.[1]?.trim() ?? title;
436 for (let at = 0; at < section.length && pieces.length < MAX_CHUNKS; at += CHUNK_CHARS) {
437 const body = section.slice(at, at + CHUNK_CHARS);
438 pieces.push(at === 0 ? body : `${heading} (continued)\n${body}`);
439 }
440 }
441 return pieces.slice(0, MAX_CHUNKS);
442}
443
444const COMMAND = /^\s*(?:\$\s*)?((?:npm|pnpm|yarn|bun|npx|cargo|go|make|pytest|python3?|poetry|uv|docker|wrangler|just|mix|bundle|rake|gradle|\.\/gradlew|mvn|dotnet)\b[^\n]{0,160})$/;
445
446/**
447 * What a doc says worth remembering: the commands in its setup sections,
448 * and the bullets of its conventions and setup sections, each whole (see
449 * `./harvest`). A doc that reads as a plan, a report or feedback says
450 * nothing; nor does a plan section in any doc, or a line that says what
451 * someone wants rather than how things are.
452 */
453function docHints(path: string, text: string, role: DocFact["role"], ctx: ExtractContext): Hint[] {
454 const agents = role === "agents";
455 if (!agents && planLike(text)) return [];
456 const hints: Hint[] = [];
457 const where = (heading: string) => `${path}${heading ? ` (${heading})` : ""}`;
458
459 // Commands in code blocks under a setup heading (any, in AGENTS.md).
460 let heading = "";
461 let skipping = 0;
462 let fenced = false;
463 for (const raw of text.split(/\r?\n/)) {
464 const line = raw.trimEnd();
465 if (/^\s*(```|~~~)/.test(line)) {
466 fenced = !fenced;
467 continue;
468 }
469 const h = /^(#{1,6})\s+(.*)$/.exec(line);
470 if (h && !fenced) {
471 const level = h[1].length;
472 if (skipping && level <= skipping) skipping = 0;
473 heading = h[2].trim();
474 if (!skipping && planSection(heading)) skipping = level;
475 continue;
476 }
477 if (!fenced || skipping) continue;
478 const command = COMMAND.exec(line)?.[1];
479 if (command && (agents || SETUP_HEADING.test(heading))) {
480 const what = heading ? plain(heading).replace(/[:.]$/, "").toLowerCase() : "work on it";
481 const said = wholeSentences(`In ${ctx.project}, for ${what}: \`${command.trim()}\`.`, MAX_HINT_CHARS);
482 if (said) hints.push({ kind: "fact", text: said, confidence: agents ? 0.9 : 0.7, evidence: where(heading) });
483 }
484 }
485
486 // Bullets, whole, from the sections that say how things are done.
487 for (const bullet of bulletsOf(text)) {
488 if (!bulletSection(bullet.heading, role)) continue;
489 // A bullet that is only a link is a table of contents.
490 if (/^\[[^\]]*\]\([^)]*\)\.?$/.test(bullet.text)) continue;
491 if (aspirational(bullet.text, role)) continue;
492 const words = plain(bullet.text);
493 // Too short to mean anything, or a lead-in to a list of its own.
494 if (words.length < 20 || /:$/.test(words)) continue;
495 const said = wholeSentences(words, MAX_HINT_CHARS);
496 if (!said) continue;
497 hints.push({ ...classify(said, bullet.heading, role), text: said, evidence: where(bullet.heading) });
498 }
499 // The same line under two headings is one hint.
500 const seen = new Set<string>();
501 return hints
502 .filter((hint) => {
503 const key = hint.text.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, " ").trim();
504 return !seen.has(key) && seen.add(key);
505 })
506 .slice(0, MAX_HINTS_PER_FILE);
507}
508
509function doc(path: string, text: string, ctx: ExtractContext): FileFacts {
510 const name = base(path);
511 const role: DocFact["role"] = /^readme/i.test(name)
512 ? "readme"
513 : /^(agents|claude)/i.test(name)
514 ? "agents"
515 : /^contributing/i.test(name)
516 ? "contributing"
517 : "doc";
518 const title = /^#\s+(.*)$/m.exec(text)?.[1]?.trim() ?? name.replace(/\.(md|markdown|txt)$/i, "");
519 const paragraph = text
520 .split(/\r?\n\s*\r?\n/)
521 .map((block) => block.trim())
522 .find((block) => block && !block.startsWith("#") && !block.startsWith("```") && !block.startsWith("<") && !block.startsWith("!["));
523 return {
524 ...EMPTY,
525 doc: { path, title: clip(title, 120), summary: paragraph ? clip(paragraph) : null, chunks: chunk(text, title), role },
526 hints: docHints(path, text, role, ctx),
527 };
528}
529
530/** Strings, from a YAML value that is a string or a list of them. */
531function strings(value: unknown): string[] {
532 const list = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
533 return list.filter((item): item is string => typeof item === "string" && item.trim() !== "").map((item) => item.trim()).slice(0, 50);
534}
535
536function owners(path: string, text: string): FileFacts {
537 const names = new Set<string>();
538 let memory: MemoryDocsConfig | null = null;
539 if (path.endsWith("project.yml")) {
540 const parsed = parseYaml(text) as { owners?: unknown; memory?: { docs?: unknown; skip?: unknown } } | null;
541 if (parsed?.memory && typeof parsed.memory === "object") memory = { docs: strings(parsed.memory.docs), skip: strings(parsed.memory.skip) };
542 const list = Array.isArray(parsed?.owners) ? parsed.owners : typeof parsed?.owners === "string" ? [parsed.owners] : [];
543 for (const owner of list) if (typeof owner === "string") names.add(owner.replace(/^@/, "").trim());
544 } else {
545 for (const line of text.split(/\r?\n/)) {
546 if (line.trim().startsWith("#")) continue;
547 for (const match of line.matchAll(/@([A-Za-z0-9][A-Za-z0-9_.-]*)(?=\s|$)/g)) names.add(match[1]);
548 }
549 }
550 return { ...EMPTY, owners: [...names].filter(Boolean).slice(0, 20), ...(memory ? { memory } : {}) };
551}
552
553const TESTS = /\b(test|tests|pytest|vitest|jest|mocha|cargo test|go test|npm test|playwright|cypress)\b/i;
554
555/** The facts in one file. A file that cannot be parsed says nothing. */
556export function extract(path: string, text: string, ctx: ExtractContext): FileFacts {
557 return { ...read(path, text, ctx), version: EXTRACT_VERSION };
558}
559
560function read(path: string, text: string, ctx: ExtractContext): FileFacts {
561 const name = base(path);
562 try {
563 if (path.startsWith(".g1t/workflows/") || path.startsWith(".github/workflows/")) {
564 return { ...EMPTY, tests: TESTS.test(text) };
565 }
566 if (path === ".g1t/project.yml" || name === "CODEOWNERS") return owners(path, text);
567 if (name === "package.json") return npm(path, text, ctx);
568 if (name === "Cargo.toml") return cargo(path, text, ctx);
569 if (name === "go.mod") return goMod(path, text, ctx);
570 if (name === "pyproject.toml" || name === "requirements.txt") return python(path, text, ctx);
571 if (/^wrangler\.(jsonc|json|toml)$/.test(name)) return wrangler(path, text);
572 if (OPENAPI_NAME.test(name)) return openapi(path, text);
573 if (/\.(md|markdown|txt)$/i.test(name) || DOC_NAME.test(name)) return doc(path, text, ctx);
574 } catch {
575 return EMPTY;
576 }
577 return EMPTY;
578}