opencode-anti-slop 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,275 @@
1
- # Temporary Holding Version
1
+ <div align="center">
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ ```text
4
+ █████╗ ███╗ ██╗████████╗ ██╗ ███████╗ ██╗ ██████╗ ██████╗
5
+ ██╔══██╗ ████╗ ██║╚══██╔══╝ ██║ ██╔════╝ ██║ ██╔═══██╗ ██╔══██╗
6
+ ███████║ ██╔══██║ ██║ ██║ ═════ ███████╗ ██║ ██║ ██║ ██████╔╝
7
+ ██╔══██║ ██║ ╚███║ ██║ ██║ ╚════██║ ██║ ██║ ██║ ██╔═══╝
8
+ ██║ ██║ ██║ ╚████ ██║ ██║ ███████║ ███████╗ ╚██████╔╝ ██║
9
+ ╚═╝ ╚═╝ ╚═╝ ╚═══ ╚═╝ ╚═╝ ╚══════╝ ╚══════╝ ╚═════╝ ╚═╝
10
+ ```
11
+
12
+ **Stop your AI coding agent from producing slop.**
13
+
14
+ Agent skills + deterministic checks that force [OpenCode](https://opencode.ai)
15
+ (and other AI coding agents) to reuse what exists, compose instead of rewrite,
16
+ use React primitives correctly, and keep files modular — enforced at write
17
+ time, commit time, and push time.
18
+
19
+ [![ci](https://github.com/hyperbolictonic/opencode-anti-slop/actions/workflows/ci.yml/badge.svg)](https://github.com/hyperbolictonic/opencode-anti-slop/actions/workflows/ci.yml)
20
+ ![npm](https://img.shields.io/badge/install-npx--from--git-blue)
21
+ ![zero dependencies](https://img.shields.io/badge/dependencies-zero-success)
22
+
23
+ </div>
24
+
25
+ Built for **React and Node** projects. The gates run on **any JS/TS codebase**;
26
+ the skills use the standard Agent Skills format, so they also work with Claude
27
+ Code by copying them into `.claude/skills/`. The scripts and git hooks are
28
+ agent-agnostic.
29
+
30
+ ## Quick start
31
+
32
+ One command — installs skills, the `/audit-ui` command, gate scripts, the CI
33
+ workflow, and (with `--hooks`) the git gates into the current directory:
34
+
35
+ ```sh
36
+ npx opencode-anti-slop --hooks
37
+ ```
38
+
39
+ > **npm >= 11 and the kit is git-hosted?** Use
40
+ > `npx --allow-git=all github:hyperbolictonic/opencode-anti-slop --hooks`.
41
+ > The `--allow-git=all` flag is npm's gate for git-hosted packages; older npm
42
+ > runs without it. `--force` overwrites previously installed kit files; an
43
+ > explicit target dir can be passed as the first argument.
44
+
45
+ Manual equivalent (clone first, no npm involvement):
46
+
47
+ ```sh
48
+ git clone https://github.com/hyperbolictonic/opencode-anti-slop.git
49
+ node opencode-anti-slop/install.mjs path/to/your-project
50
+ node scripts/anti-slop/generate-component-index.mjs # in your project
51
+ ```
52
+
53
+ Requires Node >= 18 and git. No npm dependencies — the kit installs clean into
54
+ any project. The installer prints the manual steps that cannot be safely
55
+ automated: an `AGENTS.md` snippet, optional `opencode.json` instructions,
56
+ `package.json` scripts, and the ESLint hooks plugin.
57
+
58
+ Skills are auto-discovered by OpenCode from `.opencode/skills/` in the target
59
+ repo — no registration needed. They are advertised to the model by their
60
+ `description` and load on demand via the `skill` tool. The `/audit-ui` command
61
+ arrives with the kit and produces a prioritized refactor plan from the same
62
+ deterministic checks.
63
+
64
+ ## The problem
65
+
66
+ AI agents write code that works and rots: the same layout rebuilt per page, the
67
+ same helper rewritten in three files, `useEffect` doing derived-state work,
68
+ one 400-line file doing six jobs. Prompts like "keep it DRY" drift within a
69
+ session and vanish between sessions. What does not drift is a script that exits
70
+ non-zero — so this kit pairs model-level skills with deterministic gates:
71
+
72
+ - the **agent** is instructed (skills) to check before building;
73
+ - the **inventory** (`docs/COMPONENT_INDEX.md`, generated) tells it what exists;
74
+ - the **gates** (scripts) catch whatever slips through;
75
+ - the **hooks** (git) make violations fail loudly at commit and push.
76
+
77
+ ## What it catches
78
+
79
+ | Slop pattern | Caught by |
80
+ |---|---|
81
+ | Rewritten layouts instead of reused components | `reuse-before-create` + `layout-composition` skills |
82
+ | Duplicated components, hooks, utils, server actions | duplicate gate (token-based clone detection) |
83
+ | Misused `useEffect`, premature `useMemo`/`useCallback`, Context abuse | `react-state-and-effects` skill |
84
+ | Monolithic files, god classes, oversized functions | modularity gate (size budgets) |
85
+ | Undocumented shared exports, stale component inventory | docs gate + index generator |
86
+ | Load-bearing domain rules buried in UI components | `abstraction-review` skill (post-refactor) |
87
+
88
+ A blocked commit looks like this:
89
+
90
+ ```text
91
+ [anti-slop] modularity check on staged files...
92
+ src/lib/monolith.ts (321 lines):
93
+ [file] line 1: 321 lines (> 300)
94
+ [function] line 16: 93 lines (> 80)
95
+ [class] line 1: class ReportOrchestrator: 12 methods (> 10)
96
+
97
+ [x] Modularity check FAILED.
98
+ Fix: split by responsibility (see the modular-architecture skill) — transport/service/domain/data,
99
+ smaller functions, smaller public surfaces. Do NOT compress code, raise budgets, or disable the check.
100
+ ```
101
+
102
+ Every failure message names the fix and the skill that owns it. Gate thresholds
103
+ are per-repo tunable; weakening the check to pass it is the one forbidden fix.
104
+
105
+ ## What's inside
106
+
107
+ ```
108
+ .opencode/skills/
109
+ reuse-before-create/ Mandatory pre-flight: search inventory + package.json
110
+ before creating ANY new file. Rule of Two, extend
111
+ don't clone, adapter pattern for external services.
112
+ layout-composition/ Pages with the same structure share ONE skeleton
113
+ (2-column, dashboard, settings) filled via slots;
114
+ page-specific widgets are sub-components.
115
+ react-composition-patterns/ Data/UI separation, slots & children over prop
116
+ drilling, compound components, colocation.
117
+ react-state-and-effects/ useEffect/useMemo/useCallback/Context decision
118
+ rules with explicit bans and gotchas.
119
+ modular-architecture/ UI and non-UI: single-responsibility modules,
120
+ ports & adapters with injected dependencies, OO
121
+ composition, no god files/classes/functions.
122
+ abstraction-review/ Post-refactor review for duplication that has
123
+ not happened yet: load-bearing rules left in UI
124
+ components, structural twins, near-clone triage.
125
+ Reports with a cost-ranked contract; never
126
+ auto-extracts.
127
+ verify-clean-code/ End-of-task gate: typecheck -> lint -> tests ->
128
+ duplicate + modularity + docs checks -> index
129
+ freshness -> abstraction review for refactors.
130
+
131
+ .opencode/commands/
132
+ audit-ui.md /audit-ui — prioritized refactor plan built from
133
+ the deterministic checks (duplication, monoliths,
134
+ layout reuse, missing docs, misplaced logic).
135
+
136
+ scripts/anti-slop/
137
+ lib.mjs Shared scanning helpers (walk, stripNoise,
138
+ tokenize, lineOf, export extraction).
139
+ cli.mjs Shared CLI/config plumbing (flag parsing,
140
+ per-repo config, path resolution).
141
+ generate-component-index.mjs Scans src/ and (re)generates docs/COMPONENT_INDEX.md;
142
+ pulls each export's "When to use" from its JSDoc.
143
+ check-duplicates.mjs Copy/paste detector (token-based, zero deps);
144
+ exits non-zero when duplication exceeds the
145
+ per-repo threshold.
146
+ check-modularity.mjs Monolith gate: file >300 / function >80 lines,
147
+ class >10 methods. Per-file mode for git hooks.
148
+ check-imports.mjs Import-boundary gate: circular dependencies
149
+ (default) plus configurable layer rules
150
+ (imports.deny). Per-file cycles are tree-wide.
151
+ check-docs.mjs Docs gate: fails when exports lack a one-line
152
+ "When to use" JSDoc. Per-file mode for hooks.
153
+ install-hooks.mjs Installs pre-commit (staged files: modularity +
154
+ docs, blocking) and pre-push (duplicate +
155
+ import gates + lint/typecheck/test) git hooks.
156
+
157
+ templates/anti-slop-ci.yml GitHub Actions workflow the installer drops
158
+ into target repos (.github/workflows/).
159
+ install.mjs Installs skills + commands + scripts + CI
160
+ template into a target repo.
161
+ ```
162
+
163
+ ## How enforcement works
164
+
165
+ | Layer | Mechanism | Catches |
166
+ |---|---|---|
167
+ | Skills (model) | Loaded by trigger description before relevant work | Rewritten layouts, cloned components, wrong hook usage, monolith growth — at write time |
168
+ | Component index (data) | Regenerated by script; JSDoc feeds the "When to use" column | Duplicating things that already exist because "nothing existed" |
169
+ | Duplicates gate (script) | Built-in clone detector fails on duplication above threshold | Duplicates that slip past the model |
170
+ | Modularity gate (script) | Size budgets: file >300 lines, function >80, class >10 methods | Monolithic files, god classes/functions |
171
+ | Import gate (script) | Cycle detection + configurable `imports.deny` layer rules | Circular dependencies, domain logic importing transport |
172
+ | Git hooks (target repo) | pre-commit checks staged files only (modularity + docs); pre-push runs full gates | New violations and undocumented exports at commit time without blocking on legacy code |
173
+ | ESLint (config, target repo) | `eslint-plugin-react-hooks`, React Compiler rules | Hook dependency errors, redundant memoization |
174
+
175
+ Rules of thumb baked into the skills:
176
+
177
+ - **Rule of Two** — extract a shared abstraction only on the second use (or a
178
+ file over ~150 lines); never clone on the first variation.
179
+ - **One skeleton, many fillers** — pages with the same structure share one
180
+ layout template via slots; they differ in sub-components, not markup.
181
+ - **Monoliths are defects** — one responsibility per module; ports & adapters
182
+ with injected dependencies; size budgets enforced by the gate.
183
+ - **Composition before Context** — slots/`children` before creating a Context.
184
+ - **Effects only for external sync** — derived state during render, events in
185
+ handlers, data via the project's fetching library.
186
+ - **No threshold weakening** — fix the duplication/monolith, don't tune the check.
187
+
188
+ ## Tuning per repo: `anti-slop.config.json`
189
+
190
+ Each gate reads `anti-slop.config.json` from the repo root (defaults shown):
191
+
192
+ ```json
193
+ {
194
+ "duplicates": {
195
+ "src": "src",
196
+ "minTokens": 50,
197
+ "threshold": 3,
198
+ "ignore": ["src/collections/**"]
199
+ },
200
+ "modularity": {
201
+ "src": "src",
202
+ "maxFileLines": 300,
203
+ "maxFunctionLines": 80,
204
+ "maxClassMethods": 10
205
+ },
206
+ "imports": {
207
+ "deny": [{ "from": "src/access/**", "forbid": "src/app/**" }]
208
+ }
209
+ }
210
+ ```
211
+
212
+ - `ignore` (duplicates only): globs relative to `src` for **framework-idiomatic
213
+ declarative files** — collection/field configs, generated route scaffolding.
214
+ Their repetition is shape a framework requires, not logic that can drift;
215
+ excluding them keeps the gate focused on logic clones. Exclusions are printed
216
+ on every run, so nothing is silently ignored.
217
+ - `imports.deny`: rules encoding dependency direction — any file matching
218
+ `from` importing a file matching `forbid` is a violation. With no rules, the
219
+ import gate still detects circular dependencies.
220
+ - CLI flags override the config; the config overrides defaults. Hooks call the
221
+ scripts bare, so the config governs them automatically.
222
+
223
+ ## What counts as duplication — and what doesn't
224
+
225
+ - **Counts:** logic whose copies can drift — repeated guards, repeated
226
+ fetch/map flows, hand-rolled helpers for what a library or the framework
227
+ already provides.
228
+ - **Doesn't count:** declarative scaffolding a framework requires you to write.
229
+ The dividing line is *drift*: extract a repeated block only when its copies
230
+ could behave differently. If a declarative block embeds a real rule (e.g.
231
+ who may read a record), extract that rule to one place and reference it —
232
+ the config stays, the behavior is single-source.
233
+ - **Prefer the framework's own primitives** (auth, field-level access,
234
+ validation, versions) over hand-rolled equivalents — a second source of
235
+ truth is worse than repetition.
236
+
237
+ ## Tests and self-enforcement
238
+
239
+ The gates are regression-tested with `node --test` (zero dependencies — same
240
+ constraint as the scripts). The `jsdocBefore` cases exist because a one-line
241
+ regex version of that function shipped a real bug: every export in an index
242
+ reported the file's first docblock. If you touch the scanners, `npm test` is
243
+ not optional.
244
+
245
+ ```sh
246
+ npm test # unit tests for lib.mjs and cli.mjs
247
+ npm run gates # the kit runs its own gates on its own source
248
+ npm run index # regenerate the kit's own component index
249
+ ```
250
+
251
+ CI runs the tests and the self-gates on Node 20/22/24.
252
+
253
+ ## Scope, requirements and limitations
254
+
255
+ - **Stacks:** React/Node rules in the skills; gates scan `.ts` `.tsx` `.js`
256
+ `.jsx` `.mjs` `.mts`. Other languages need their own gates — the system
257
+ design (inventory + drift principle + gates + hooks) still applies.
258
+ - **Heuristics, not compilers:** the clone detector tokenizes rather than
259
+ parses; the modularity gate brace-matches rather than builds an AST. Treat
260
+ findings as strong signals, and `anti-slop.config.json` as the way to live
261
+ with rare false positives.
262
+ - **Agent support:** skills follow the Agent Skills format — native for
263
+ OpenCode, copyable to `.claude/skills/` for Claude Code. The scripts and
264
+ hooks work with any agent plus git. `/audit-ui` is OpenCode-specific.
265
+
266
+ ## Reusable vs. project-specific
267
+
268
+ The skills are project-agnostic: they direct the agent to discover each repo's
269
+ own structure (`package.json`, source tree, `docs/COMPONENT_INDEX.md`) at
270
+ runtime. Tuning per project happens in the target repo's `anti-slop.config.json`
271
+ and its own `AGENTS.md` — not by editing these skills.
272
+
273
+ ## License
274
+
275
+ [MIT](LICENSE)
package/install.mjs ADDED
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * install.mjs — install the anti-slop kit into a target repository.
4
+ *
5
+ * Also the npx entrypoint (`bin` in package.json):
6
+ * npx github:hyperbolictonic/opencode-anti-slop # install into cwd
7
+ * npx github:hyperbolictonic/opencode-anti-slop ./app # explicit target
8
+ * npx github:hyperbolictonic/opencode-anti-slop --hooks # + git hooks
9
+ *
10
+ * Copies:
11
+ * .opencode/skills/<skills> -> <target>/.opencode/skills/
12
+ * .opencode/commands/audit-ui.md -> <target>/.opencode/commands/
13
+ * scripts/anti-slop/*.mjs -> <target>/scripts/anti-slop/
14
+ * templates/anti-slop-ci.yml -> <target>/.github/workflows/
15
+ *
16
+ * Then prints the manual setup steps that cannot be safely automated
17
+ * (AGENTS.md snippet, opencode.json instructions, package.json scripts,
18
+ * ESLint setup). --hooks also installs the git pre-commit gate when the
19
+ * target is a git repo.
20
+ *
21
+ * Usage:
22
+ * npx opencode-anti-slop [target-dir] [--force] [--hooks]
23
+ * node install.mjs [target-repo-dir] [--force] [--hooks]
24
+ */
25
+
26
+ import { cpSync, existsSync, mkdirSync, readdirSync, statSync } from 'node:fs';
27
+ import path from 'node:path';
28
+ import { fileURLToPath } from 'node:url';
29
+
30
+ const kitRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)));
31
+ const argv = process.argv.slice(2);
32
+ const flags = new Set(argv.filter((a) => a.startsWith('--')));
33
+ const force = flags.has('--force');
34
+ const wantHooks = flags.has('--hooks');
35
+ const targetArg = argv.find((a) => !a.startsWith('--'));
36
+ const helpRequested = flags.has('-h') || flags.has('--help');
37
+
38
+ if (helpRequested) {
39
+ const help = [
40
+ 'Usage: npx github:hyperbolictonic/opencode-anti-slop [target-dir] [--force] [--hooks]',
41
+ '',
42
+ ' target-dir defaults to the current directory',
43
+ ' --force overwrite kit files that already exist in the target',
44
+ ' --hooks also install the git pre-commit gate (requires .git)',
45
+ ].join('\n');
46
+ console.log(help);
47
+ process.exit(0);
48
+ }
49
+
50
+ const target = path.resolve(targetArg ?? process.cwd());
51
+ if (!existsSync(target) || !statSync(target).isDirectory()) {
52
+ console.error(`Target directory not found: ${target}`);
53
+ process.exit(1);
54
+ }
55
+ if (path.resolve(kitRoot) === target) {
56
+ console.error('Refusing to install the kit into itself.');
57
+ process.exit(1);
58
+ }
59
+
60
+ const copyOverwrite = (src, dest) => {
61
+ if (existsSync(dest) && !force) {
62
+ console.error(`[skip] ${dest} already exists (use --force to overwrite)`);
63
+ return false;
64
+ }
65
+ cpSync(src, dest, { recursive: true });
66
+ console.log(`[ok] ${dest}`);
67
+ return true;
68
+ };
69
+
70
+ // --- skills -----------------------------------------------------------------
71
+ const skillsSrc = path.join(kitRoot, '.opencode', 'skills');
72
+ const skillsDest = path.join(target, '.opencode', 'skills');
73
+ mkdirSync(skillsDest, { recursive: true });
74
+
75
+ for (const entry of readdirSync(skillsSrc, { withFileTypes: true })) {
76
+ if (entry.isDirectory() && existsSync(path.join(skillsSrc, entry.name, 'SKILL.md'))) {
77
+ copyOverwrite(path.join(skillsSrc, entry.name), path.join(skillsDest, entry.name));
78
+ }
79
+ }
80
+
81
+ // --- scripts ----------------------------------------------------------------
82
+ const scriptsSrc = path.join(kitRoot, 'scripts', 'anti-slop');
83
+ const scriptsDest = path.join(target, 'scripts', 'anti-slop');
84
+ mkdirSync(path.dirname(scriptsDest), { recursive: true });
85
+ copyOverwrite(scriptsSrc, scriptsDest);
86
+
87
+ // --- CI workflow template -----------------------------------------------------
88
+ const templateSrc = path.join(kitRoot, 'templates', 'anti-slop-ci.yml');
89
+ const templateDest = path.join(target, '.github', 'workflows', 'anti-slop.yml');
90
+ if (existsSync(templateSrc)) {
91
+ mkdirSync(path.dirname(templateDest), { recursive: true });
92
+ copyOverwrite(templateSrc, templateDest);
93
+ }
94
+
95
+ // --- commands ----------------------------------------------------------------
96
+ // Copy command FILES individually so a pre-existing commands dir doesn't nest
97
+ // (dir-into-dir copy would produce commands/commands/x.md → /commands/x).
98
+ const commandsSrc = path.join(kitRoot, '.opencode', 'commands');
99
+ const commandsDest = path.join(target, '.opencode', 'commands');
100
+ mkdirSync(commandsDest, { recursive: true });
101
+ for (const entry of readdirSync(commandsSrc, { withFileTypes: true })) {
102
+ if (entry.isFile() && entry.name.endsWith('.md')) {
103
+ copyOverwrite(path.join(commandsSrc, entry.name), path.join(commandsDest, entry.name));
104
+ }
105
+ }
106
+
107
+ // --- manual steps -----------------------------------------------------------
108
+ console.log(`
109
+ Manual setup in ${target} (one-time):
110
+
111
+ 1. AGENTS.md — add:
112
+ ${'```markdown'}
113
+ ## Anti-slop skills
114
+ - Before creating any new component, hook, or utility: load the \`reuse-before-create\` skill.
115
+ - When building/refactoring React UI: load \`react-composition-patterns\`.
116
+ - When touching state/effects/memo/context: load \`react-state-and-effects\`.
117
+ - Before marking any coding task complete: run the \`verify-clean-code\` checklist.
118
+ ${'```'}
119
+
120
+ 2. opencode.json (optional, makes the inventory always loaded):
121
+ ${'```json'}
122
+ { "$schema": "https://opencode.ai/config.json", "instructions": ["docs/COMPONENT_INDEX.md"] }
123
+ ${'```'}
124
+
125
+ 3. package.json scripts:
126
+ ${'```json'}
127
+ "scripts:anti-slop": {
128
+ "index": "node scripts/anti-slop/generate-component-index.mjs",
129
+ "dupes": "node scripts/anti-slop/check-duplicates.mjs"
130
+ }
131
+ ${'```'}
132
+ (flatten to plain keys if you prefer: "index:components", "check:dupes")
133
+
134
+ 4. ESLint: enable eslint-plugin-react-hooks (and the React Compiler eslint
135
+ plugin if the project uses the compiler) so hook misuse fails in the linter.
136
+
137
+ 5. Generate the first inventory: node scripts/anti-slop/generate-component-index.mjs
138
+
139
+ 6. Git gates (recommended): git init && node scripts/anti-slop/install-hooks.mjs
140
+ - pre-commit: modularity + docs on staged files (blocking), duplicate/import reports
141
+ - pre-push: full duplicate + import gates + lint/typecheck/test via --if-present
142
+ - /audit-ui in OpenCode produces a prioritized refactor plan from the same checks
143
+ `);
144
+
145
+ // --- optional hooks ------------------------------------------------------------
146
+ if (wantHooks) {
147
+ if (!existsSync(path.join(target, '.git'))) {
148
+ console.error('[skip] --hooks requested but no .git directory in target; run git init first.');
149
+ } else {
150
+ const { spawnSync } = await import('node:child_process');
151
+ const hookScript = path.join(scriptsDest, 'install-hooks.mjs');
152
+ if (existsSync(hookScript)) {
153
+ const res = spawnSync(process.execPath, [hookScript], { stdio: 'inherit', cwd: target });
154
+ if (res.error) console.error(`[warn] hook installer failed: ${res.error.message}`);
155
+ }
156
+ }
157
+ }
package/package.json CHANGED
@@ -1,6 +1,43 @@
1
1
  {
2
2
  "name": "opencode-anti-slop",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "OpenCode enforcement kit: agent skills plus deterministic gates that keep AI-generated React and Node code modular, DRY, and documented",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": "install.mjs",
8
+ "files": [
9
+ "install.mjs",
10
+ "templates",
11
+ "scripts/anti-slop",
12
+ ".opencode"
13
+ ],
14
+ "keywords": [
15
+ "opencode",
16
+ "ai-agents",
17
+ "agent-skills",
18
+ "claude-code",
19
+ "react",
20
+ "nodejs",
21
+ "typescript",
22
+ "code-quality",
23
+ "dry",
24
+ "technical-debt",
25
+ "lint",
26
+ "git-hooks",
27
+ "ci"
28
+ ],
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/hyperbolictonic/opencode-anti-slop.git"
32
+ },
33
+ "homepage": "https://github.com/hyperbolictonic/opencode-anti-slop#readme",
34
+ "bugs": "https://github.com/hyperbolictonic/opencode-anti-slop/issues",
35
+ "engines": {
36
+ "node": ">=18"
37
+ },
38
+ "scripts": {
39
+ "test": "node --test scripts/anti-slop/lib.test.mjs scripts/anti-slop/cli.test.mjs scripts/anti-slop/check-imports.test.mjs",
40
+ "gates": "node scripts/anti-slop/check-modularity.mjs --src scripts/anti-slop && node scripts/anti-slop/check-imports.mjs --src scripts && node scripts/anti-slop/check-docs.mjs --src scripts/anti-slop && node scripts/anti-slop/check-duplicates.mjs --src scripts",
41
+ "index": "node scripts/anti-slop/generate-component-index.mjs --src scripts"
42
+ }
43
+ }
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * check-docs.mjs
4
+ *
5
+ * Documentation gate for shared exports. Fails (exit 1) when exported
6
+ * declarations in the checked files lack a one-line `/** When to use ... *\/`
7
+ * JSDoc above them. Keeps docs/COMPONENT_INDEX.md (generated from the same
8
+ * JSDoc) a maintained, trustworthy inventory instead of a stale artifact.
9
+ *
10
+ * Zero dependencies.
11
+ *
12
+ * Usage:
13
+ * node scripts/anti-slop/check-docs.mjs [paths...] [--src src] [--report-only]
14
+ *
15
+ * With no path arguments it scans --src (default `src`). With path arguments
16
+ * it checks exactly those files — used by git hooks to check only staged
17
+ * files, so legacy undocumented code never blocks commits.
18
+ */
19
+
20
+ import { readFileSync, existsSync } from 'node:fs';
21
+ import path from 'node:path';
22
+ import { walk, extractExports } from './lib.mjs';
23
+ import { loadConfig, bootstrapGate, collectPathFiles, resolveScanTarget } from './cli.mjs';
24
+
25
+ // Defaults < anti-slop.config.json < CLI flags.
26
+ const cfgDocs = loadConfig().docs ?? {};
27
+ const { args, positionals } = bootstrapGate(process.argv.slice(2), {
28
+ usage: 'Usage: node check-docs.mjs [paths...] [--src src] [--report-only]\n' +
29
+ 'Config: anti-slop.config.json "docs" section (src).\n' +
30
+ 'With path arguments, checks exactly those files (git-hook staged mode).',
31
+ spec: { '--src': 'string', '--report-only': 'flag' },
32
+ defaults: { src: cfgDocs.src ?? 'src' },
33
+ });
34
+
35
+ const files = positionals.length > 0 ? collectPathFiles(positionals) : resolveScanTarget(args.src);
36
+
37
+ let missing = 0;
38
+ const detail = [];
39
+ for (const file of files) {
40
+ let exports;
41
+ try {
42
+ exports = extractExports(readFileSync(file.full, 'utf8'));
43
+ } catch {
44
+ continue;
45
+ }
46
+ const undocumented = exports.filter((e) => !e.desc && e.name !== 'default');
47
+ if (undocumented.length === 0) continue;
48
+ missing += undocumented.length;
49
+ detail.push({ rel: file.rel, names: undocumented.map((e) => e.name) });
50
+ }
51
+
52
+ if (missing === 0) {
53
+ console.log(`[ok] Docs check passed (${files.length} file(s), every export documented).`);
54
+ process.exit(0);
55
+ }
56
+
57
+ console.log(`${missing} undocumented export(s) in ${detail.length} file(s):`);
58
+ for (const d of detail.slice(0, 20)) {
59
+ console.log(` ${d.rel}: ${d.names.join(', ')}`);
60
+ }
61
+ if (detail.length > 20) console.log(` ... and ${detail.length - 20} more file(s)`);
62
+
63
+ console.error(
64
+ '\n[x] Docs check FAILED.\n' +
65
+ ' Fix: add a one-line "/** When to use ... */" JSDoc above each export —\n' +
66
+ ' this is what feeds docs/COMPONENT_INDEX.md, the inventory the\n' +
67
+ ' reuse-before-create pre-flight depends on. Do NOT delete the check or\n' +
68
+ ' leave the doc as a bare restatement of the name.'
69
+ );
70
+ process.exit(args.reportOnly ? 0 : 1);