095 — The rules file that had to quote its own price

2026-09-08 · distribution lane · DISTRIBUTION D3, the last ungated half

D3's remaining ungated items were "a Claude Code plugin manifest and a per-library Cursor/AGENTS.md rules file, both generated surfaces rendered from corrections/". Both exist now. Neither is listed anywhere, which is Sam's gate. The work took one real decision, and the decision is a number this studio had already measured against itself.

A rules file is not a correction pack in a different wrapper

The obvious build was a re-render: take corrections/<lib>.md, put Cursor's frontmatter on it, call it a rules file. The correction packs are 31–74 KB. alwaysApply: true on 74 KB of prisma would be the single most expensive line in a user's project.

And the cost is not a guess here. BM1 measured the zod pack at 51,247 characters of context and put its break-even at 28.5 fact-dependent tasks in one session — that number was pre-registered against the product (prediction B2) and it did not hold in our favour. A pack is a document you read once. A rules file is context added to requests. Shipping the pack as an always-on rule would be recommending a cost we have measured against a benefit we have not.

So rules/<lib>.md is the statements only: the API, the release that changed it, the statement, the stale belief, and the primary source. No reproduced failures, no code, no evidence, no coverage table, no model-attribution table. That comes out at 2.4 to 3.9 times smaller than the pack — 8,052 characters for valibot, 28,343 for prisma — and each file prints its own size in characters, because the size is the thing the reader is actually deciding about and it should not require arithmetic. The .mdc ships alwaysApply: false with a description ("apply intelligently"), never always-on, and rules/README.md carries the whole size table so a reader who wants it glob-scoped can add globs with the number in front of them.

The claims policy lands inside every file rather than under it, the same lesson as the question pages (JOURNAL/089): a rules file gets pasted somewhere and read out of context, so each one states which pack the benchmark measured and that no measurement covers the shorter form. zod's says it with BM1's four figures. The other six say plainly that BM1 measured zod's pack, not theirs, and that there is no measurement of what either form does for their library.

Two things the build corrected about itself

"Three to five times the size" was wrong for every library in the table underneath it. Written into rules/README.md from an estimate made before the files existed; the real range is 2.4 to 3.9. It is now computed from the rendered files at build time, so it cannot be wrong again. This is the fourth instance of the JOURNAL/005 + /010 failure — prose with numbers in it — and the reason the whole thing is a generated surface: tools/build-rules.mjs --check is now the fifth check in the pre-commit chain.

The generator does not trust its own rendering loop. It counts the ### headings in the output against the api fields in facts.json and fails on any difference in either direction, and it asserts that the .mdc is byte-for-byte the .md with frontmatter prepended, so the two forms cannot drift. Six refusals were exercised against real mutations: a drifted rules file, an orphan file in rules/, a missing .mdc, a hand-edited plugin.json, a change_kind the renderer has no word for, and a fact renamed in facts.json.

The plugin, and why its root is the whole repository

The MCP server reads the dataset by path — the fact that produced JOURNAL/093's whole test. So the plugin manifest sits at the repository root, and ${CLAUDE_PLUGIN_ROOT}/tools/mcp-server.mjs resolves inside a tree that contains data/ and corrections/. The tidier-looking alternative — a plugin/ subdirectory whose MCP command is npx stale-priors-mcp — reaches the same corrections through two of Sam's gates instead of one, because it needs the npm package published as well as the repo flipped. One gate is better than two for the same product.

The server is declared inline in plugin.json rather than in a root .mcp.json. A .mcp.json at this repository's root would also be read as this project's own MCP config, in a context where ${CLAUDE_PLUGIN_ROOT} does not expand and the command is therefore a literal broken path. Inline, the placeholder only ever appears where something substitutes it. plugin-test.mjs asserts the absence of that file, so a later session cannot add one without meeting the reason.

Both manifests were written against documentation fetched today, not remembered, and one open question was settled by reading real data rather than reasoning about the docs: the docs describe a "marketplace-root" source form in words that contradict themselves ("resolves to plugin subdirectory"), so the choice of "source": "./" was made after pulling anthropics/claude-plugins-community's catalog — 1,871 of its 2,282 entries carry no path at all, which makes plugin-root-is-repo-root the ordinary case rather than a clever one.

plugin-test.mjs, and the part of it that is an admission

Same shape as pack-test.mjs, for the second distribution channel. It derives what the plugin root must carry from data/index.json — never from a list somebody maintains — copies exactly that into a scratch directory, expands ${CLAUDE_PLUGIN_ROOT} to it, spawns the command the manifest declares from an unrelated working directory (a plugin is enabled inside somebody else's project, and a server that only works when the cwd is its own checkout works for nobody), and requires byte-identical answers to this clone's. 63 checks. Four refusals exercised against the real manifest: the $CLAUDE_PLUGIN_ROOT misspelling, a root .mcp.json, a .. escaping the plugin root, and a manifest naming a real file that is not the server — which failed by starting the wrong program, and the derived-set discipline is what surfaced it.

What it cannot check, and this is written down rather than glossed: Claude Code's own parsing of the two manifests. The official validator is claude plugin validate .; the claude CLI is not on this machine. And the $schema URLs the documentation itself names — code.claude.com/schemas/plugin.json and .../marketplace.json — return the documentation site's HTML, not a schema document, with and without an Accept: application/json header. Checked twice today. So there was no schema to validate against, the structural checks are written from the documentation's field tables, and running the real validator once is now item 4 of D3's gate: one command, any machine with Claude Code on it, paste the output.

Where this lane is now

The rules files are mirrored to the site at /rules/, listed in llms.txt with each file's character count against its pack's and the same disclaimer, and linked from every correction pack page. That is the only part of today's work that is visible without a gate, and it is deliberate: in the repository these files reach nobody until the flip.

After this, this lane has no ungated build work left in D2 through D8. D5 and D7 wait on the repo flip; D6 needs an API token and ~$10 of credit, and its two free inputs (GitHub stars, npm downloads) are structurally zero until the repo is public and the package is published; D8 needs a price and a new surface. What is left that no one has to approve is D9's demand signal and a second benchmark — a different library, or a different subject, on the instrument BM1 built.

Not published, not listed, not sent. Sam's asks are D2's four questions and D3's four.