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/.opencode/commands/audit-ui.md +34 -0
- package/.opencode/skills/abstraction-review/SKILL.md +76 -0
- package/.opencode/skills/layout-composition/SKILL.md +78 -0
- package/.opencode/skills/modular-architecture/SKILL.md +65 -0
- package/.opencode/skills/react-composition-patterns/SKILL.md +43 -0
- package/.opencode/skills/react-state-and-effects/SKILL.md +61 -0
- package/.opencode/skills/reuse-before-create/SKILL.md +108 -0
- package/.opencode/skills/verify-clean-code/SKILL.md +78 -0
- package/LICENSE +21 -0
- package/README.md +274 -2
- package/install.mjs +157 -0
- package/package.json +41 -4
- package/scripts/anti-slop/check-docs.mjs +70 -0
- package/scripts/anti-slop/check-duplicates.mjs +182 -0
- package/scripts/anti-slop/check-imports.mjs +209 -0
- package/scripts/anti-slop/check-imports.test.mjs +128 -0
- package/scripts/anti-slop/check-modularity.mjs +150 -0
- package/scripts/anti-slop/cli.mjs +175 -0
- package/scripts/anti-slop/cli.test.mjs +106 -0
- package/scripts/anti-slop/generate-component-index.mjs +133 -0
- package/scripts/anti-slop/install-hooks.mjs +99 -0
- package/scripts/anti-slop/lib.mjs +204 -0
- package/scripts/anti-slop/lib.test.mjs +178 -0
- package/templates/anti-slop-ci.yml +22 -0
package/README.md
CHANGED
|
@@ -1,3 +1,275 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
[](https://github.com/hyperbolictonic/opencode-anti-slop/actions/workflows/ci.yml)
|
|
20
|
+

|
|
21
|
+

|
|
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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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);
|