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
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/8740-a-dependency-the-readme-does-not-justify → main 1 commit fork
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. | ||
| 167 | 167 | |
| 168 | 168 | Git hosting, code browsing, and a JSON API in one fast Bun process, with |
| 169 | 169 | **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 | |
| 174 | 172 | router, the SSR templates, the design system, the markdown renderer. The only |
| 175 | 173 | thing forge asks of the host is a `git` binary. |
| 176 | 174 | |
| 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 | ||
| 177 | 187 | Forgejo is the reference for what a forge should _feel_ like. This is that |
| 178 | 188 | feeling on a foundation sized for one person. |
| 179 | 189 |
docs/PRINCIPLES.md +1 −1
| @@ -12,4 +12,4 @@ remove one or absorb it into another, or turn it into code. | ||
| 12 | 12 | - **Git is truth; the database is a derived index.** Enforced by: `src/core/reconcile.ts` rebuilds it on boot. |
| 13 | 13 | - **Speed is counted in work, never in a clock.** Enforced by: `test/flake.test.ts`. |
| 14 | 14 | - **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 | }); |
new above
offline