* feat: llms.txt + llms-full.txt + AGENTS.md (v0.15.0) Ship three new public artifacts at the repo root so agents that aren't Claude Code can discover GBrain documentation cleanly: - AGENTS.md — ~45-line install + operating protocol for non-Claude agents (Codex, Cursor, OpenClaw, Aider). Covers install, read order, trust boundary, config/debug/migration pointers, fork regeneration. Uses relative links so it survives fork/rename. - llms.txt — llmstxt.org-spec index (H1 + blockquote + Core entry points / Configuration / Debugging / Migrations / Philosophy / Optional H2s). - llms-full.txt — same index with core docs inlined for single-fetch ingestion. ~225KB, well under the 600KB FULL_SIZE_BUDGET. Generator-driven via scripts/build-llms.ts + scripts/llms-config.ts. LLMS_REPO_BASE env var makes it fork-friendly. bun run build:llms regenerates both outputs deterministically. test/build-llms.test.ts has 7 cases: paths resolve on disk, generator idempotent, llms.txt spec shape, checked-in files match generator output (drift guard), content contract (RESOLVER / AGENTS / INSTALL referenced), AGENTS mirrors README + INSTALL_FOR_AGENTS install path, llms-full.txt under size budget. Leverage point per Codex review: README.md + INSTALL_FOR_AGENTS.md install prompts now tell agents to fetch AGENTS.md first. Without this, the new files were invisible. Drive-by fix: INSTALL_FOR_AGENTS.md:136 had `git pull origin main` while the repo's default branch is master (origin/HEAD -> master). Corrected. Plan + reviews: /plan-eng-review CLEARED, /codex adversarial review found 15 issues — 7 folded in directly, 3 user tension decisions, 5 stayed as NOT-in-scope with reasoning. Version bumps to 0.15.0 (new public-artifact feature surface per Step 12 of /ship feature-signal heuristic). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: normalize VERSION to 3-digit to match master master uses 3-digit semver (0.14.2); my earlier /ship bumped VERSION to the 4-digit gstack format (0.15.0.0). Revert to 0.15.0 to match package.json (already 3-digit) and master's convention. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
99 lines
4.1 KiB
TypeScript
99 lines
4.1 KiB
TypeScript
import { describe, test, expect } from "bun:test";
|
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
|
|
import { buildLlmsFiles } from "../scripts/build-llms";
|
|
import { SECTIONS, FULL_SIZE_BUDGET } from "../scripts/llms-config";
|
|
|
|
const repoRoot = join(import.meta.dir, "..");
|
|
|
|
describe("build-llms generator", () => {
|
|
// Case 1 — every config path resolves on disk. Catches rename-induced 404s.
|
|
test("every configured path exists on disk", () => {
|
|
for (const section of SECTIONS) {
|
|
for (const entry of section.entries) {
|
|
const abs = join(repoRoot, entry.path);
|
|
expect(existsSync(abs), `missing: ${entry.path}`).toBe(true);
|
|
|
|
const st = statSync(abs);
|
|
if (entry.path.endsWith("/")) {
|
|
expect(st.isDirectory(), `${entry.path} should be a directory`).toBe(true);
|
|
} else {
|
|
expect(st.isFile(), `${entry.path} should be a file`).toBe(true);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Case 2 — generator is idempotent. Run twice in-memory, compare byte-for-byte.
|
|
test("generator output is deterministic across runs", () => {
|
|
const first = buildLlmsFiles();
|
|
const second = buildLlmsFiles();
|
|
expect(second.llmsTxt).toBe(first.llmsTxt);
|
|
expect(second.llmsFullTxt).toBe(first.llmsFullTxt);
|
|
});
|
|
|
|
// Case 3 — llms.txt spec shape per llmstxt.org: H1 + blockquote + required H2s.
|
|
test("llms.txt follows llmstxt.org spec shape", () => {
|
|
const { llmsTxt } = buildLlmsFiles();
|
|
const lines = llmsTxt.split("\n");
|
|
|
|
expect(lines[0], "first line must be H1").toBe("# GBrain");
|
|
|
|
// Blockquote summary on line 2 or 3 (spec allows blank line after H1).
|
|
const hasEarlyBlockquote =
|
|
lines.slice(1, 4).some((line) => line.startsWith("> "));
|
|
expect(hasEarlyBlockquote, "needs > blockquote summary near top").toBe(true);
|
|
|
|
// Required H2 sections for GBrain's user need (config/debug/migration).
|
|
expect(llmsTxt).toContain("## Core entry points");
|
|
expect(llmsTxt).toContain("## Configuration");
|
|
expect(llmsTxt).toContain("## Debugging");
|
|
expect(llmsTxt).toContain("## Migrations");
|
|
});
|
|
|
|
// Case 4 — checked-in files match generator output. Catches "forgot to rerun
|
|
// generator" before ship. If this fails in CI, run `bun run build:llms` and
|
|
// commit the result.
|
|
test("committed llms.txt + llms-full.txt match current generator output", () => {
|
|
const { llmsTxt, llmsFullTxt } = buildLlmsFiles();
|
|
|
|
const committedLlms = readFileSync(join(repoRoot, "llms.txt"), "utf8");
|
|
const committedFull = readFileSync(join(repoRoot, "llms-full.txt"), "utf8");
|
|
|
|
const helpMsg =
|
|
"Run `bun run build:llms` and commit the updated output before shipping.";
|
|
expect(committedLlms, helpMsg).toBe(llmsTxt);
|
|
expect(committedFull, helpMsg).toBe(llmsFullTxt);
|
|
});
|
|
|
|
// Case 5 — content contract. Prevents silent removal of critical sections or
|
|
// entries from llms-config.ts. Catches "someone deleted the Debugging section."
|
|
test("content contract: llms.txt references required entry points", () => {
|
|
const { llmsTxt } = buildLlmsFiles();
|
|
expect(llmsTxt).toContain("skills/RESOLVER.md");
|
|
expect(llmsTxt).toContain("INSTALL_FOR_AGENTS.md");
|
|
expect(llmsTxt).toContain("AGENTS.md");
|
|
expect(llmsTxt).toContain("CLAUDE.md");
|
|
});
|
|
|
|
test("content contract: AGENTS.md mirrors README + INSTALL_FOR_AGENTS install path", () => {
|
|
const agents = readFileSync(join(repoRoot, "AGENTS.md"), "utf8");
|
|
expect(agents).toContain("CLAUDE.md");
|
|
expect(agents).toContain("skills/RESOLVER.md");
|
|
expect(agents).toContain("INSTALL_FOR_AGENTS.md");
|
|
expect(agents).toContain("llms.txt");
|
|
// Trust boundary is the non-obvious security concept agents need up-front.
|
|
expect(agents.toLowerCase()).toContain("trust boundary");
|
|
});
|
|
|
|
test("llms-full.txt stays within size budget", () => {
|
|
const { llmsFullTxt } = buildLlmsFiles();
|
|
const bytes = Buffer.byteLength(llmsFullTxt, "utf8");
|
|
expect(
|
|
bytes,
|
|
`llms-full.txt is ${bytes} bytes (budget ${FULL_SIZE_BUDGET}). Add includeInFull: false to large entries.`,
|
|
).toBeLessThan(FULL_SIZE_BUDGET);
|
|
});
|
|
});
|