Public: anyone can read this; the crew can change it.

A dependency the README does not justify fails the gate: “less is better” gets its enforcer #8756

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

mason/forge:mason/8740-a-dependency-the-readme-does-not-justify → main 1 commit fork

  • Merged. mason/8740-a-dependency-the-readme-does-not-justify was merged into main as 7b690fc.

Review

Sign in to review this change.
3 files changed +61 −5
README.md +14 −4
@@ -167,13 +167,23 @@ a failed or killed install only logs and lets the run start.
167167
168168 Git hosting, code browsing, and a JSON API in one fast Bun process, with
169169 **zero runtime dependencies** — the `package.json` dependency list is empty and
170 stays that way. The one exception is dev-only: `@anthropic-ai/sandbox-runtime`
171 (a devDependency, with its own few transitive packages) lets
172 `test/crew-sandbox-real.test.ts` run a shell under the very sandbox Claude Code
173 builds from the crew's settings; it never ships and the app never imports it. Everything worth having is written here: the Result type, the
170 stays that way. The dev-only exceptions are listed under Dependencies below;
171 none ships and the app never imports them. Everything worth having is written here: the Result type, the
174172 router, the SSR templates, the design system, the markdown renderer. The only
175173 thing forge asks of the host is a `git` binary.
176174
175 ## Dependencies
176
177 Every package in `package.json`, with why it is there. A package with no line
178 here fails `test/dependencies-named.test.ts`; so does a line for one that left.
179
180 - `@anthropic-ai/sandbox-runtime` — lets `test/crew-sandbox-real.test.ts` run a shell under the very sandbox Claude Code builds from the crew's settings.
181 - `@types/bun` — the type check's types for Bun's own APIs.
182 - `axe-core` — the release check's accessibility rules (`src/release/axe.ts`); they need a rendered page, and there is no zero-dependency equivalent.
183 - `happy-dom` — a DOM for tests that run a page's script (`test/changelog.test.ts`) without a browser.
184 - `typescript` — the native 7.x checker behind `bun x tsc --noEmit`.
185 - `typescript-api` — TypeScript 5.x, kept only for the compiler API `scripts/hollow.ts` walks; 7.x has none yet.
186
177187 Forgejo is the reference for what a forge should _feel_ like. This is that
178188 feeling on a foundation sized for one person.
179189
docs/PRINCIPLES.md +1 −1
@@ -12,4 +12,4 @@ remove one or absorb it into another, or turn it into code.
1212 - **Git is truth; the database is a derived index.** Enforced by: `src/core/reconcile.ts` rebuilds it on boot.
1313 - **Speed is counted in work, never in a clock.** Enforced by: `test/flake.test.ts`.
1414 - **Work that serves no pillar waits behind the Later gate.** Enforced by: `src/actions/plan.ts` refuses a plan item with neither.
15 - **Less is better: one person can hold the whole forge in their head, so no new dependency, no new rule without a reason.** Enforced by: nothing yet.
15 - **Less is better: one person can hold the whole forge in their head, so no new dependency, no new rule without a reason.** Enforced by: `test/dependencies-named.test.ts` (each package has its line in the README); nothing yet for rules.
test/dependencies-named.test.ts added +46
@@ -0,0 +1,46 @@
1 /**
2 * "Less is better" gets its enforcer (#8740): every package in package.json
3 * has one line of why in README.md's Dependencies section, and the section
4 * names no package that has left. Pure file reads.
5 */
6 import { expect, test } from "bun:test";
7 import { readFileSync } from "node:fs";
8 import { join } from "node:path";
9
10 const ROOT = join(import.meta.dir, "..");
11
12 /** The package names a README's `## Dependencies` section justifies: each `- \`name\` — why` bullet. */
13 export function justified(readme: string): Map<string, string> {
14 const start = readme.indexOf("\n## Dependencies\n");
15 if (start < 0) return new Map();
16 const rest = readme.slice(start + 1);
17 const end = rest.indexOf("\n## ", 1);
18 const section = end < 0 ? rest : rest.slice(0, end);
19 const out = new Map<string, string>();
20 for (const m of section.matchAll(/^- `([^`]+)` — (.+)$/gm)) out.set(m[1]!, m[2]!.trim());
21 return out;
22 }
23
24 const pkg = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8")) as {
25 dependencies?: Record<string, string>;
26 devDependencies?: Record<string, string>;
27 };
28 const packages = [...Object.keys(pkg.dependencies ?? {}), ...Object.keys(pkg.devDependencies ?? {})];
29 const lines = justified(readFileSync(join(ROOT, "README.md"), "utf8"));
30
31 test("every package in package.json has its line of why in the README", () => {
32 const missing = packages.filter((name) => !lines.get(name));
33 expect(missing, `README.md's Dependencies section does not justify: ${missing.join(", ")}`).toEqual([]);
34 });
35
36 test("the README justifies no package package.json no longer has", () => {
37 const stale = [...lines.keys()].filter((name) => !packages.includes(name));
38 expect(stale, `README.md's Dependencies section names packages not in package.json: ${stale.join(", ")}`).toEqual([]);
39 });
40
41 test("a package with no line is caught, and the section ends at the next heading", () => {
42 const readme = "# R\n## Dependencies\n\n- `a` — why a.\n- `b` —\n## Next\n- `c` — not here.\n";
43 const found = justified(readme);
44 expect([...found.keys()]).toEqual(["a"]);
45 expect(justified("# R\nno section\n").size).toBe(0);
46 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs