diff --git a/.claude/.markdownlint.jsonc b/.claude/.markdownlint.jsonc deleted file mode 100644 index cb89efc..0000000 --- a/.claude/.markdownlint.jsonc +++ /dev/null @@ -1,10 +0,0 @@ -// Overrides for Claude Code's own project files. `extends` is load-bearing: -// without it this file REPLACES the root rule set rather than adding to it. -{ - "extends": "../.markdownlint.jsonc", - - // A slash command and a rule file are PROMPT BODIES, not documents. Their - // frontmatter carries the title, so a top-level heading inside would - // duplicate it. - "MD041": false -} diff --git a/.claude/commands/newscript.md b/.claude/commands/newscript.md deleted file mode 100644 index 2c80814..0000000 --- a/.claude/commands/newscript.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -description: Start a new userscript the right way — measure the live page first, then scaffold. -argument-hint: -allowed-tools: Bash, Read, Edit, Write, Glob, Grep ---- - -Start a new install unit named `$1` targeting `$2`. - -**Measure before you scaffold.** A selector that was not verified against the -live page is a guess, and guesses rot silently. - -1. **Survey the live page with every userscript disabled** — otherwise you are - measuring your own output. Follow **CLAUDE.md § Measuring the live page**: it - carries the exact `page-lab` invocations, the `--target-id` trap, and why - `selector-verify.mjs`'s `GENERATED` verdict is what enforces the - no-generated-class-names rule. Opening an adult site in the operator's - browser is **their** action — ask for the tab. - Record for each anchor: the selector, the **exact node count** it matched, - and the date. - -2. **Reject any anchor that is a generated class name, a localised - `aria-label`, or a tool-generated id.** Anchor on ARIA roles, `href` values - and data attributes instead. - -3. **Scaffold.** `nix run .#new-script -- $1` - This writes `$1.user.js` with the required metadata keys and the teardown - contract already wired, plus a `listings/$1.md` stub. It passes all three - gates as generated. - -4. **Fill in the header comment with the measurements from step 1**, then the - `@name`, `@description` and `@match` for `$2`. Decide Greasy Fork vs Sleazy - Fork now — an adult-site script uploaded to Greasy Fork gets rejected or - relocated. - -5. **Confirm the failure mode.** Break your own selector on purpose and check - the page renders **stock**, not mangled. That is the contract; a script that - mangles a page on a miss is a defect. - -6. `nix run .#lint`, add the README catalogue row, add a `CHANGELOG.md` entry - under `## [Unreleased]`, and exercise the site's primary actions in a - browser before opening anything. diff --git a/.claude/commands/publish.md b/.claude/commands/publish.md deleted file mode 100644 index e61a1bc..0000000 --- a/.claude/commands/publish.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -description: Take a script from "changed" to "posted on Sleazy Fork", with every gate in order. -argument-hint: [patch|minor|major|x.y.z] -allowed-tools: Bash, Read, Edit, Glob, Grep ---- - -Publish `$1` at bump level `${2:-patch}`. - -Do these in order and **stop at the first failure** — report what failed and -what you need, rather than working around it. - -1. **Confirm the change is real and measured.** `git diff` the script. If a - selector changed, the header comment must carry the new measurement and its - date. A comment asserting behaviour the code does not perform is a defect — - fix the comment in this same change, not later. - -2. **Bump the version.** - `nix run .#bump -- $1 ${2:-patch}` - Never hand-edit the `@version` line; the command preserves the metadata - block's alignment and refuses anything that does not clear `origin/main`. - -3. **Write the CHANGELOG entry** under `## $1`, newest first, in Keep a - Changelog categories (`Added`/`Changed`/`Fixed`/`Removed`/`Security`). - Explain **why**, and include the measurement. "What" is in the diff. - -4. **Update `listings/$1.md`** if the behaviour changed. It is the Sleazy Fork - body, and an undisclosed behaviour is the most common reason a script is - taken down. Render any diagram with `nix run .#diagram`. - -5. **Run the gates.** `nix run .#lint`, then - `nix run .#publish-check -- $1`. Every line must read `ok`. - - Then check it on a real page: - `nix run .#verify -- --expect ` - That drives a real wheel event, so lazy content is measured rather than - assumed absent. - -6. **Exercise the site's primary actions** in a browser before you call it - done. Geometry proves a control is *present*, never that it *works* — and - it must work under a **trusted** event (CDP `Input.dispatchMouseEvent`, not - `el.click()`). CLAUDE.md § Measuring the live page has the tooling and the - injection traps; there are no acceptance specs in this repo yet, so this - step is manual and you must actually do it. - -7. **Branch, commit, push.** Subject `$1: what changed`; the body explains why - and carries the measurement. Never add AI attribution. Push the branch — - the **izzykatt-ci** App opens the pull request from that commit message and - squash-merges it once the checks are green. - -8. **Report what is left for a human**: posting the code and the listing body - on Sleazy Fork (select **Markdown** before pasting the body — a missed click - publishes raw `##` and fences), and adding the listing link to the README - catalogue row if this is a first publish. diff --git a/.claude/hooks/post-edit.mjs b/.claude/hooks/post-edit.mjs deleted file mode 100755 index 335ff1e..0000000 --- a/.claude/hooks/post-edit.mjs +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env node -// PostToolUse(Write|Edit). Two jobs, both cheap enough to run on every write: -// -// *.user.js -> parse it and run the Greasy Fork publish lint on THAT FILE. -// A userscript is copied verbatim into the browser, so a syntax -// error is a script that silently never runs. Catching it at the -// edit is worth far more than catching it in CI. -// *.nix -> git add it. A flake evaluates the git tree, so an unstaged new -// file is invisible to `nix flake check`. -// -// Exit 2 feeds stderr back to Claude to fix. Anything else never blocks. - -import { execFileSync } from 'node:child_process'; -import { existsSync } from 'node:fs'; -import { relative, isAbsolute } from 'node:path'; - -const root = process.env.CLAUDE_PROJECT_DIR || process.cwd(); - -let input = ''; -process.stdin.setEncoding('utf8'); -for await (const chunk of process.stdin) input += chunk; - -let file; -try { - file = JSON.parse(input)?.tool_input?.file_path; -} catch { - process.exit(0); -} -if (!file) process.exit(0); - -const rel = isAbsolute(file) ? relative(root, file) : file; -if (rel.startsWith('..')) process.exit(0); // outside the project - -const run = (cmd, args) => - execFileSync(cmd, args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }); - -if (rel.endsWith('.nix')) { - try { - run('git', ['add', '--', rel]); - } catch { - /* not a git checkout, or the file is ignored — neither is worth blocking */ - } - process.exit(0); -} - -if (!rel.endsWith('.user.js')) process.exit(0); - -const problems = []; -try { - run('node', ['--check', rel]); -} catch (e) { - problems.push(`${rel} does not parse — it would silently never run:\n${e.stderr || e.message}`); -} - -if (existsSync(`${root}/scripts/meta-lint.mjs`)) { - try { - run('node', ['scripts/meta-lint.mjs', rel]); - } catch (e) { - problems.push(`${rel} fails the Greasy Fork publish lint:\n${e.stdout || ''}${e.stderr || ''}`); - } -} - -if (problems.length) { - console.error(problems.join('\n\n')); - process.exit(2); -} - -// There is no live-injection loop to revive here. A change reaches the browser -// only through a real Violentmonkey install, which is the operator's action — -// see CONTRIBUTING.md "Testing a change". diff --git a/.claude/hooks/session-start.mjs b/.claude/hooks/session-start.mjs deleted file mode 100755 index 82ca567..0000000 --- a/.claude/hooks/session-start.mjs +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env node -// SessionStart digest. Three things that are invisible until they bite: -// -// 1. whether ./node_modules is the flake-pinned tree or an npm install, -// 2. untracked .nix files (a flake evaluates the GIT TREE, so they are -// invisible to `nix flake check` and the result is silently stale), -// 3. each script's @version against origin/main — the difference between -// "ready to publish" and "a re-install would be a silent no-op". -// -// Never fails the session: every probe is wrapped, and the worst case is a -// shorter digest. - -import { execFileSync } from 'node:child_process'; -import { lstatSync, readlinkSync, readdirSync, readFileSync } from 'node:fs'; - -const root = process.env.CLAUDE_PROJECT_DIR || process.cwd(); -const out = []; - -const git = (args) => - execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); - -const versionOf = (text) => /^\/\/\s*@version\s+(\S+)/m.exec(text)?.[1] ?? null; - -try { - const st = lstatSync(`${root}/node_modules`); - if (st.isSymbolicLink()) { - out.push( - readlinkSync(`${root}/node_modules`).includes('/nix/store/') - ? 'deps: node_modules is the flake-pinned tree.' - : 'deps: node_modules is a symlink OUTSIDE the Nix store.', - ); - } else { - out.push('deps: node_modules is an npm install — `nix run .#deps` pins it instead.'); - } -} catch { - out.push('deps: node_modules is ABSENT — run `nix run .#deps` (never `npm install`).'); -} - -try { - const untracked = git(['ls-files', '--others', '--exclude-standard', '--', '*.nix']); - if (untracked) { - out.push(`WARNING: untracked .nix files are INVISIBLE to nix flake check — git add them:\n ${untracked.split('\n').join('\n ')}`); - } -} catch { - /* not a git checkout; nothing to say */ -} - -try { - const scripts = readdirSync(root).filter((f) => f.endsWith('.user.js')).sort(); - for (const f of scripts) { - const head = versionOf(readFileSync(`${root}/${f}`, 'utf8')); - let base = null; - try { - base = versionOf(git(['show', `origin/main:${f}`])); - } catch { - /* new script, or no origin/main locally */ - } - if (head && base && head === base) out.push(`${f}: @version ${head} (same as origin/main — bump before publishing).`); - else if (head && base) out.push(`${f}: @version ${head}, origin/main has ${base}.`); - else if (head) out.push(`${f}: @version ${head} (not on origin/main yet).`); - } -} catch { - /* unreadable checkout; nothing to say */ -} - -if (out.length) console.log(out.join('\n')); diff --git a/.claude/hooks/stop-gate.mjs b/.claude/hooks/stop-gate.mjs deleted file mode 100755 index 97b4765..0000000 --- a/.claude/hooks/stop-gate.mjs +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env node -// Stop gate. Refuses to end a turn that leaves a changed userscript failing the -// two gates CI requires. The point is that "done" and "lints" mean the same -// thing here — a red gate discovered by CI costs a round trip, and a red gate -// discovered after publishing costs an install channel. -// -// Runs ONLY when a .user.js actually changed, so an unrelated turn pays -// nothing. `stop_hook_active` short-circuits the re-entry loop. - -import { execFileSync } from 'node:child_process'; -import { existsSync } from 'node:fs'; - -const root = process.env.CLAUDE_PROJECT_DIR || process.cwd(); - -let input = ''; -process.stdin.setEncoding('utf8'); -for await (const chunk of process.stdin) input += chunk; - -try { - if (JSON.parse(input)?.stop_hook_active) process.exit(0); -} catch { - /* no payload; carry on */ -} - -const run = (cmd, args) => - execFileSync(cmd, args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }); - -let changed = []; -try { - changed = run('git', ['status', '--porcelain', '--', '*.user.js']) - .split('\n') - .map((l) => l.slice(3).trim()) - .filter(Boolean); -} catch { - process.exit(0); -} -if (changed.length === 0) process.exit(0); - -const problems = []; -for (const f of changed) { - if (!existsSync(`${root}/${f}`)) continue; // deleted - try { - run('node', ['--check', f]); - } catch (e) { - problems.push(`${f} does not parse:\n${e.stderr || e.message}`); - } -} - -try { - run('node', ['scripts/meta-lint.mjs']); -} catch (e) { - problems.push(`meta-lint failed:\n${e.stdout || ''}${e.stderr || ''}`); -} - -const eslint = `${root}/node_modules/.bin/eslint`; -if (existsSync(eslint)) { - try { - run(eslint, changed); - } catch (e) { - problems.push(`eslint failed:\n${e.stdout || ''}${e.stderr || ''}`); - } -} else { - problems.push('node_modules is absent, so ESLint did not run. `nix run .#deps` installs it offline.'); -} - -if (problems.length) { - console.error( - `A changed userscript does not pass the gates CI requires. Fix these, then stop:\n\n${problems.join('\n\n')}`, - ); - process.exit(2); -} diff --git a/.claude/rules/flake-and-nix.md b/.claude/rules/flake-and-nix.md deleted file mode 100644 index c2b618a..0000000 --- a/.claude/rules/flake-and-nix.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -description: Nix flake evaluation reads the git tree — stage before you evaluate. -paths: - - "flake.nix" - - "flake.lock" - - "nix/**/*.nix" ---- - -# Working on the flake - -## Stage before you evaluate - -A flake evaluates the **git tree**, not the working directory. A new file that -has not been `git add`ed is **invisible** to the evaluator, which surfaces as a -confusing `path does not exist` or, worse, a silently stale evaluation. - -```bash -git add -A && nix flake check -``` - -The `PostToolUse` hook stages `.nix` writes as a safety net. Do not rely on it -alone when authoring several files at once. - -## What each output is for - -- `checks.*` — every gate that runs **offline**, in the sandbox. Adding one here - means CI gets it for free via `.github/workflows/nix.yml`. -- `packages.*` / `apps.*` — the maintenance commands. Each is a - `writeShellApplication`, so **shellcheck runs at build time**: a broken - command fails `nix flake check`, not someone's afternoon. -- `devShells.default` — the CLIs. Add one only after `nix search nixpkgs ` - confirms it is packaged. - -Prefer adding a command to the flake over documenting a manual procedure in a -README. The command is checked; the README is not. - -## Two traps already paid for - -- **`SC2329` is excluded on purpose.** Every command shares one prelude and uses - only part of it; shellcheck cannot see the other callers. -- **`actionlint` carries two `-ignore` patterns** for - `actions/create-github-app-token`. Its bundled action metadata predates - `client-id` auth. Drop them when a newer actionlint stops reporting them. - -## Formatting - -`nix fmt .` uses `nixfmt-tree`. `checks.formatting` fails on anything -unformatted, so run it before pushing. diff --git a/.claude/rules/listings.md b/.claude/rules/listings.md deleted file mode 100644 index 43dafec..0000000 --- a/.claude/rules/listings.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -description: Listing bodies are pasted into Sleazy Fork's form — write and render them accordingly. -paths: - - "listings/**" ---- - -# Listing copy - -`listings/