Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .claude/rules/code-style.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ Export style follows the kind of module. Components, layouts, App Router route f
- Components are **Server Components by default**; add `'use client'` only when the component needs hooks, event handlers, or browser APIs.
- Every component has a colocated `.test.tsx` (see [`Banner.test.tsx`](../../src/components/banner/Banner.test.tsx)).

## Structure and reuse

The counts, thresholds, and carve-outs behind these live in the skill. What follows is how they land here.

- **A component gets a directory, not a loose file.** Every one lives in its own kebab-case directory under `src/components/` with a PascalCase file and a colocated test, as [`navbar/Navbar.tsx`](../../src/components/navbar/Navbar.tsx) does. Six of the seven directories spell the name that way, and [`Stars/`](../../src/components/Stars/StarsBackground.tsx) is the single PascalCase exception rather than a second convention: match the six. Related files are grouped into a subdirectory rather than left flat beside unrelated ones, and entries sharing a name prefix are the group to propose.
- **Count the files sitting directly in a directory**, whatever subdirectories sit beside them: one subdirectory does not make the loose files next to it grouped. [`src/components/ServiceWorkerRegister.tsx`](../../src/components/ServiceWorkerRegister.tsx) and the two `ThemeRegistry` files sit directly in `src/components/` beside seven component directories, so the count there is three rather than ten.
- **A setting the tooling reads from configuration is set once, never per file.** [`jest.config.js`](../../jest.config.js) already sets `testEnvironment: 'jsdom'` for every test, so no test file carries a `@jest-environment` docblock. Path aliases are declared in [`tsconfig.json`](../../tsconfig.json) and mirrored in [`jest.config.js`](../../jest.config.js) rather than re-declared per import. Where the same directive would go into three or more files, **search for the key rather than for the directive's own spelling**, since the two are rarely the same word, and hoist the majority while leaving the minority declared. Moving a directive into the key the tool reads is not deleting it.
- **Reuse before writing.** Check this repository's own [`helpers`](../../src/helpers/ascii.ts) and [`util`](../../src/util/cookieConsent.ts) modules, then [`package.json`](../../package.json), then the platform, before hand-writing behaviour that has a name outside this repository. Where nothing present provides it, say so rather than adding a dependency. Never hand-roll anything that signs, verifies, hashes a credential, or settles an authorization outcome.

## TypeScript

- Strict mode is on; types must be explicit (no implicit `any`).
Expand Down
4 changes: 3 additions & 1 deletion .claude/rules/prompt-skill-sync.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ Whichever half someone takes is the only thing they get. Four rules follow.

An illustrative link, such as `[config.py](../src/config.py)` inside an example teaching the citation format, is not a real link and is allowed. The test is whether the target exists here: if it does, the author linked to something real and it will break.

`make -f .claude/Makefile check-skills` enforces every rule in this section, plus the specification itself: `name` matching the directory, `description` within its character limit, a body under 500 lines, a licence on every skill, and every bundled path resolving. It is deliberately **not** part of `npm run validate`, because the repository must build, test, and lint with no agent tooling present.
`make -f .claude/Makefile check-skills` enforces every rule in this section, plus the specification itself: `name` matching the directory, `description` within its character limit, a body under 500 lines, a licence on every skill, and every bundled path resolving. It also gives each prompt a character budget: 36,000 for [`audit-docs.prompt.md`](../../.github/prompts/audit-docs.prompt.md), whose subject is narrower, and 52,000 for the other two. A skill can move detail into `references/`; a prompt is one file a reader scrolls, so its budget is the whole of what it can say. **Growth past a budget is a signal to condense, never to raise it.** What a prompt loses first is a rule restated across sections and a worked example following the rule it illustrates, and never a rule, a checklist item, or a category.

**Characters, because a line here is a paragraph.** `MD013` is off repository-wide and Prettier leaves prose unwrapped, so a single line runs to seventeen hundred characters. A line budget charges a prompt for its blank lines and its headings and lets it pay by deleting them, which makes the document harder to read while the number improves and nothing is condensed at all. Characters do not move when a file is reformatted, so the only way down is to cut what the prompt says. It is deliberately **not** part of `npm run validate`, because the repository must build, test, and lint with no agent tooling present.

## The plugin manifest, and what it does not change

Expand Down
31 changes: 30 additions & 1 deletion .claude/scripts/check-skill-publishability.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,26 @@ const MAX_BODY_LINES = 500;
/** The spec caps `description` at 1024 characters, because it loads at startup. */
const MAX_DESCRIPTION = 1024;

/**
* A prompt is one file a reader scrolls in a chat pane, with no `references/` to move detail into,
* so its budget is the whole of what it can say. `audit-docs` is held tighter than the other two
* because its subject is narrower. Growth past a budget is a signal to condense, not to raise it:
* restatements of one rule across sections, and worked examples following the rule they
* illustrate, are what a prompt loses first, and never a rule, a checklist item, or a category.
*
* The budget counts characters because a line here is a paragraph. Markdown lint rule `MD013` is
* off repository-wide and Prettier leaves prose unwrapped, so one line in these files runs to
* seventeen hundred characters. Counting lines charges a document for its blank lines and its
* headings, and lets it pay by deleting them: the same eighteen sections cost 36 lines as `###`
* headings and nothing at all inline, while the text is identical either way. Characters do not
* move when a document is reformatted, so only cutting what a prompt says brings the number down.
*
* `wc -c` is the hand-check. It counts bytes where this counts UTF-16 code units, so it reads a
* dozen or so high on a file carrying emoji, which is far inside the headroom each budget leaves.
*/
const MAX_PROMPT_CHARS = 52_000;
const MAX_PROMPT_CHARS_BY_FILE = { 'audit-docs.prompt.md': 36_000 };

/**
* Directories a skill may bundle. The specification defines `references/`, `assets/`, and
* `scripts/`; `agents/` is a host extension, read only where a plugin manifest turns the
Expand Down Expand Up @@ -314,7 +334,16 @@ function skillFiles(name) {
/** Checks that a prompt still works as the only file someone holds. */
function checkPrompt(file) {
const label = `.github/prompts/${file}`;
const parts = split(readFileSync(join(PROMPT_DIR, file), 'utf8'));
const text = readFileSync(join(PROMPT_DIR, file), 'utf8');
const parts = split(text);

// Counted over the whole file rather than the body, because frontmatter is what a reader
// scrolls past too.
const budget = MAX_PROMPT_CHARS_BY_FILE[file] ?? MAX_PROMPT_CHARS;

if (text.length > budget) {
fail(label, `is ${text.length} characters against a budget of ${budget}; condense rather than raising it`);
}

if (!parts) {
fail(label, 'no frontmatter block');
Expand Down
Loading