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
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib.mjs — shared source-scanning helpers for the anti-slop gate scripts.
|
|
3
|
+
*
|
|
4
|
+
* Zero dependencies (Node >= 18, ESM). CLI/config plumbing lives in cli.mjs;
|
|
5
|
+
* this module is pure scanning: file discovery, noise stripping, tokenizing,
|
|
6
|
+
* export extraction. Keep it free of I/O decisions and argument parsing.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readdirSync } from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
|
|
12
|
+
/** Directories never scanned by the gates (deps, build output, tests, tooling). */
|
|
13
|
+
export const EXCLUDE_DIRS = new Set([
|
|
14
|
+
'node_modules', 'dist', 'build', 'out', '.next', '.turbo',
|
|
15
|
+
'coverage', 'storybook-static', '__tests__', '__mocks__',
|
|
16
|
+
]);
|
|
17
|
+
/** Filename patterns never gated: test/spec/story files (any JS/TS extension incl. mjs/mts) and type declaration shims. */
|
|
18
|
+
export const IGNORE_FILE = /(\.test|\.spec|\.stories|\.d)\.[mc]?[tj]sx?$/;
|
|
19
|
+
|
|
20
|
+
/** Generated files that must never be gated (sizes/counts are machine-owned). */
|
|
21
|
+
export const GENERATED_FILE = /(^|\/)(payload-types|next-env)(\.d)?\.ts$/;
|
|
22
|
+
|
|
23
|
+
/** True when relPath is a generated artifact (see GENERATED_FILE). */
|
|
24
|
+
export function isGenerated(relPath) {
|
|
25
|
+
return GENERATED_FILE.test(relPath);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Source file extensions the gates scan. */
|
|
29
|
+
export const SOURCE_EXT = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.mts']);
|
|
30
|
+
|
|
31
|
+
/** Recursively list source files under `dir`, as { full, rel } with '/'-separated rel paths. */
|
|
32
|
+
export function walk(dir, base) {
|
|
33
|
+
const files = [];
|
|
34
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
35
|
+
if (entry.name.startsWith('.') || EXCLUDE_DIRS.has(entry.name)) continue;
|
|
36
|
+
const full = path.join(dir, entry.name);
|
|
37
|
+
const rel = path.relative(base, full).split(path.sep).join('/');
|
|
38
|
+
if (entry.isDirectory()) files.push(...walk(full, base));
|
|
39
|
+
else if (SOURCE_EXT.has(path.extname(entry.name)) && !IGNORE_FILE.test(entry.name) && !isGenerated(rel)) {
|
|
40
|
+
files.push({ full, rel });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return files;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Remove comments and string contents so brace/regex analysis is reliable.
|
|
48
|
+
* Newlines (including those inside strings) are preserved, so line numbers
|
|
49
|
+
* computed on the output match the original source.
|
|
50
|
+
*/
|
|
51
|
+
export function stripNoise(src) {
|
|
52
|
+
let out = '';
|
|
53
|
+
let i = 0;
|
|
54
|
+
const n = src.length;
|
|
55
|
+
while (i < n) {
|
|
56
|
+
const c = src[i];
|
|
57
|
+
const next = src[i + 1];
|
|
58
|
+
if (c === '/' && next === '/') {
|
|
59
|
+
while (i < n && src[i] !== '\n') i++;
|
|
60
|
+
} else if (c === '/' && next === '*') {
|
|
61
|
+
i += 2;
|
|
62
|
+
while (i < n && !(src[i] === '*' && src[i + 1] === '/')) {
|
|
63
|
+
if (src[i] === '\n') out += '\n';
|
|
64
|
+
i++;
|
|
65
|
+
}
|
|
66
|
+
i += 2;
|
|
67
|
+
} else if (c === '"' || c === "'" || c === '`') {
|
|
68
|
+
const quote = c;
|
|
69
|
+
i++;
|
|
70
|
+
while (i < n && src[i] !== quote) {
|
|
71
|
+
if (src[i] === '\\') i++;
|
|
72
|
+
if (src[i] === '\n') out += '\n';
|
|
73
|
+
i++;
|
|
74
|
+
}
|
|
75
|
+
i++; // closing quote
|
|
76
|
+
out += '""';
|
|
77
|
+
} else {
|
|
78
|
+
out += c;
|
|
79
|
+
i++;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Remove comments while KEEPING string contents, preserving newlines. Used
|
|
87
|
+
* where the text inside strings matters (import specifier parsing) but
|
|
88
|
+
* comment noise must not create false edges.
|
|
89
|
+
*/
|
|
90
|
+
export function stripComments(src) {
|
|
91
|
+
let out = '';
|
|
92
|
+
let i = 0;
|
|
93
|
+
const n = src.length;
|
|
94
|
+
while (i < n) {
|
|
95
|
+
const c = src[i];
|
|
96
|
+
const next = src[i + 1];
|
|
97
|
+
if (c === '/' && next === '/') {
|
|
98
|
+
while (i < n && src[i] !== '\n') i++;
|
|
99
|
+
} else if (c === '/' && next === '*') {
|
|
100
|
+
i += 2;
|
|
101
|
+
while (i < n && !(src[i] === '*' && src[i + 1] === '/')) {
|
|
102
|
+
if (src[i] === '\n') out += '\n';
|
|
103
|
+
i++;
|
|
104
|
+
}
|
|
105
|
+
i += 2;
|
|
106
|
+
} else {
|
|
107
|
+
out += c;
|
|
108
|
+
i++;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const TOKEN_RE =
|
|
115
|
+
/"(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*'|`(?:[^`\\]|\\.)*`|[A-Za-z_$][\w$]*|\d+(?:\.\d+)?|\S/g;
|
|
116
|
+
|
|
117
|
+
/** Tokenize stripped source into normalized tokens [{ t, line }] (1-based lines): strings -> `<str>`, numbers -> `<num>`, identifiers/symbols stay literal. */
|
|
118
|
+
export function tokenize(stripped) {
|
|
119
|
+
const tokens = [];
|
|
120
|
+
for (const m of stripped.matchAll(TOKEN_RE)) {
|
|
121
|
+
let nl = 0;
|
|
122
|
+
for (let k = 0; k < m.index; k++) if (m.input[k] === '\n') nl++;
|
|
123
|
+
const raw = m[0];
|
|
124
|
+
const t = raw[0] === '"' || raw[0] === "'" || raw[0] === '`' ? '<str>'
|
|
125
|
+
: /^\d/.test(raw) ? '<num>'
|
|
126
|
+
: raw;
|
|
127
|
+
tokens.push({ t, line: 1 + nl });
|
|
128
|
+
}
|
|
129
|
+
return tokens;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** 1-based line number of char index `idx` within comment/string-stripped source. */
|
|
133
|
+
export function lineOf(stripped, idx) {
|
|
134
|
+
let line = 1;
|
|
135
|
+
for (let i = 0; i < idx && i < stripped.length; i++) {
|
|
136
|
+
if (stripped[i] === '\n') line++;
|
|
137
|
+
}
|
|
138
|
+
return line;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Returns the first line of the JSDoc/TSDoc block immediately preceding
|
|
143
|
+
* source index `idx`, or null if there is none.
|
|
144
|
+
*
|
|
145
|
+
* Why this is not a one-line regex: the previous implementation scanned with
|
|
146
|
+
* `/\/\*\*([\s\S]*?)\*\/\s*$/`. The `$` anchor plus a lazy capture makes that
|
|
147
|
+
* match start at the FIRST docblock in the file and end at the LAST closing
|
|
148
|
+
* terminator before the declaration, so every export reported the module
|
|
149
|
+
* header — in a real repo, four server actions all described as "Carer-facing
|
|
150
|
+
* mutations." and a type's doc attached to the function above it. A one-line
|
|
151
|
+
* block also leaked its closing terminator into docs/COMPONENT_INDEX.md.
|
|
152
|
+
* check-docs only requires a non-null first line, so the gate passed while the
|
|
153
|
+
* inventory that `reuse-before-create` is grounded in was wrong.
|
|
154
|
+
*
|
|
155
|
+
* Keep both properties: the NEAREST block, and only when nothing but whitespace
|
|
156
|
+
* sits between its close and the declaration. Anything else must return null —
|
|
157
|
+
* that is the "undocumented" verdict the gate exists to report, and it is safer
|
|
158
|
+
* than borrowing a neighbouring comment.
|
|
159
|
+
*/
|
|
160
|
+
export function jsdocBefore(source, idx) {
|
|
161
|
+
const slice = source.slice(0, idx);
|
|
162
|
+
const start = slice.lastIndexOf('/**');
|
|
163
|
+
if (start === -1) return null;
|
|
164
|
+
const end = slice.indexOf('*/', start);
|
|
165
|
+
if (end === -1) return null;
|
|
166
|
+
if (slice.slice(end + 2).trim() !== '') return null;
|
|
167
|
+
|
|
168
|
+
const lines = slice
|
|
169
|
+
.slice(start + 3, end)
|
|
170
|
+
.split('\n')
|
|
171
|
+
.map((l) => l.replace(/^\s*\*+\s?/, '').trim())
|
|
172
|
+
.filter(Boolean);
|
|
173
|
+
if (lines.length === 0) return null;
|
|
174
|
+
const desc = lines[0];
|
|
175
|
+
return desc.startsWith('@') ? null : desc; // tag-only blocks are not usage docs
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Extract exported declarations from source: [{ name, kind, index, desc }].
|
|
180
|
+
* `desc` is the JSDoc first line (null when undocumented).
|
|
181
|
+
*/
|
|
182
|
+
export function extractExports(source) {
|
|
183
|
+
const found = [];
|
|
184
|
+
const add = (name, kind, index) => {
|
|
185
|
+
if (name && !found.some((e) => e.name === name)) found.push({ name, kind, index });
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
for (const m of source.matchAll(/export\s+(?:default\s+)?(?:async\s+)?function\s*\*?\s*([A-Za-z0-9_$]+)/g)) {
|
|
189
|
+
add(m[1], 'function', m.index);
|
|
190
|
+
}
|
|
191
|
+
for (const m of source.matchAll(/export\s+(?:default\s+)?const\s+([A-Za-z0-9_$]+)/g)) {
|
|
192
|
+
add(m[1], /=>/.test(source.slice(m.index, m.index + 200)) ? 'function' : 'value', m.index);
|
|
193
|
+
}
|
|
194
|
+
for (const m of source.matchAll(/export\s+(?:abstract\s+)?class\s+([A-Za-z0-9_$]+)/g)) {
|
|
195
|
+
add(m[1], 'class', m.index);
|
|
196
|
+
}
|
|
197
|
+
for (const m of source.matchAll(/export\s+(?:type|interface)\s+([A-Za-z0-9_$]+)/g)) {
|
|
198
|
+
add(m[1], 'type', m.index);
|
|
199
|
+
}
|
|
200
|
+
if (/export\s+default\s+(?!function|class|const)/.test(source)) {
|
|
201
|
+
add('default', 'default', source.length);
|
|
202
|
+
}
|
|
203
|
+
return found.map((e) => ({ ...e, desc: jsdocBefore(source, e.index) }));
|
|
204
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the shared scanning helpers (lib.mjs). Zero dependencies —
|
|
3
|
+
* runs with `node --test`. The jsdocBefore cases are regression tests for a
|
|
4
|
+
* real failure: a `$`-anchored lazy regex attached the file's FIRST docblock
|
|
5
|
+
* to every export, and the gate could not notice (it only required a non-null
|
|
6
|
+
* first line). If you "simplify" jsdocBackfore back into a one-line regex,
|
|
7
|
+
* these tests are the ones that will catch it.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import test from 'node:test';
|
|
11
|
+
import assert from 'node:assert/strict';
|
|
12
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
13
|
+
import { tmpdir } from 'node:os';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
extractExports,
|
|
18
|
+
isGenerated,
|
|
19
|
+
jsdocBefore,
|
|
20
|
+
lineOf,
|
|
21
|
+
stripNoise,
|
|
22
|
+
tokenize,
|
|
23
|
+
walk,
|
|
24
|
+
} from './lib.mjs';
|
|
25
|
+
|
|
26
|
+
// --- jsdocBefore -------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
test('jsdocBefore: attached block supplies its first line', () => {
|
|
29
|
+
const src = '/** Formats dates for tables. */\nexport function f() {}';
|
|
30
|
+
const idx = src.indexOf('export');
|
|
31
|
+
assert.equal(jsdocBefore(src, idx), 'Formats dates for tables.');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('jsdocBefore: multi-line block, first meaningful line wins', () => {
|
|
35
|
+
const src = [
|
|
36
|
+
'/**',
|
|
37
|
+
' * Renders the standard page shell.',
|
|
38
|
+
' * @param props - ignored',
|
|
39
|
+
' */',
|
|
40
|
+
'export function f() {}',
|
|
41
|
+
].join('\n');
|
|
42
|
+
const idx = src.indexOf('export');
|
|
43
|
+
assert.equal(jsdocBefore(src, idx), 'Renders the standard page shell.');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('jsdocBefore: one-line block must not leak its terminator', () => {
|
|
47
|
+
const src = '/** doc */ export const x = 1';
|
|
48
|
+
const idx = src.indexOf('export');
|
|
49
|
+
const desc = jsdocBefore(src, idx);
|
|
50
|
+
assert.ok(!desc.includes('*/'), `terminator leaked: ${desc}`);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('jsdocBefore: nearest of several blocks wins (regression: first-block bug)', () => {
|
|
54
|
+
const src = [
|
|
55
|
+
'/** Module header. */',
|
|
56
|
+
'export const a = 1;',
|
|
57
|
+
'/** Second helper. */',
|
|
58
|
+
'export const b = 2;',
|
|
59
|
+
].join('\n');
|
|
60
|
+
const idxA = src.indexOf('export const a');
|
|
61
|
+
const idxB = src.indexOf('export const b');
|
|
62
|
+
assert.equal(jsdocBefore(src, idxA), 'Module header.');
|
|
63
|
+
assert.equal(jsdocBefore(src, idxB), 'Second helper.');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('jsdocBefore: code between block and export means undocumented', () => {
|
|
67
|
+
const src = '/** Module header. */\nconst internal = 1;\nexport function f() {}';
|
|
68
|
+
const idx = src.indexOf('export');
|
|
69
|
+
assert.equal(jsdocBefore(src, idx), null);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('jsdocBefore: blank lines between block and export are allowed', () => {
|
|
73
|
+
const src = '/** Doc. */\n\n\nexport const x = 1';
|
|
74
|
+
const idx = src.indexOf('export');
|
|
75
|
+
assert.equal(jsdocBefore(src, idx), 'Doc.');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('jsdocBefore: tag-only block is not a usage doc', () => {
|
|
79
|
+
const src = '/** @internal */\nexport const x = 1';
|
|
80
|
+
const idx = src.indexOf('export');
|
|
81
|
+
assert.equal(jsdocBefore(src, idx), null);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('jsdocBefore: no block at all -> null', () => {
|
|
85
|
+
assert.equal(jsdocBefore('export const x = 1', 0), null);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// --- extractExports -----------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
test('extractExports: classifies functions, consts, classes, types, default', () => {
|
|
91
|
+
const src = [
|
|
92
|
+
'export async function doThing() {}',
|
|
93
|
+
'export const handler = () => 1;',
|
|
94
|
+
'export const LIMIT = 3;',
|
|
95
|
+
'export class Repo {}',
|
|
96
|
+
'export type Shape = { a: number };',
|
|
97
|
+
'export interface Person { id: number }',
|
|
98
|
+
'export default config;',
|
|
99
|
+
].join('\n');
|
|
100
|
+
const byName = new Map(extractExports(src).map((e) => [e.name, e]));
|
|
101
|
+
assert.equal(byName.get('doThing').kind, 'function');
|
|
102
|
+
assert.equal(byName.get('handler').kind, 'function');
|
|
103
|
+
assert.equal(byName.get('LIMIT').kind, 'value');
|
|
104
|
+
assert.equal(byName.get('Repo').kind, 'class');
|
|
105
|
+
assert.equal(byName.get('Shape').kind, 'type');
|
|
106
|
+
assert.equal(byName.get('Person').kind, 'type');
|
|
107
|
+
assert.equal(byName.get('default').kind, 'default');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('extractExports: attached docs flow through', () => {
|
|
111
|
+
const src = '/** Doubles a value. */\nexport function double(x: number) { return x * 2; }';
|
|
112
|
+
const [e] = extractExports(src);
|
|
113
|
+
assert.equal(e.desc, 'Doubles a value.');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// --- stripNoise / tokenize / lineOf -------------------------------------------
|
|
117
|
+
|
|
118
|
+
test('stripNoise: removes comments, blanks strings, preserves newlines', () => {
|
|
119
|
+
const src = [
|
|
120
|
+
'// line comment',
|
|
121
|
+
'const a = "two', // inside string newline
|
|
122
|
+
'words";',
|
|
123
|
+
'/* block',
|
|
124
|
+
'comment */',
|
|
125
|
+
'const b = 1;',
|
|
126
|
+
].join('\n');
|
|
127
|
+
const out = stripNoise(src);
|
|
128
|
+
assert.ok(!out.includes('line comment'));
|
|
129
|
+
assert.ok(!out.includes('block'));
|
|
130
|
+
assert.equal(out.split('\n').length, src.split('\n').length);
|
|
131
|
+
assert.ok(out.includes('const b = 1;'));
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('tokenize: strings and numbers normalized, lines tracked', () => {
|
|
135
|
+
const stripped = 'const msg = "hello";\nconst n = 42;';
|
|
136
|
+
const tokens = tokenize(stripped);
|
|
137
|
+
const values = tokens.map((t) => t.t);
|
|
138
|
+
assert.deepEqual(values, ['const', 'msg', '=', '<str>', ';', 'const', 'n', '=', '<num>', ';']);
|
|
139
|
+
const numLine = tokens.find((t) => t.t === '<num>').line;
|
|
140
|
+
assert.equal(numLine, 2);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('lineOf: char index to 1-based line', () => {
|
|
144
|
+
const s = 'one\ntwo\nthree';
|
|
145
|
+
assert.equal(lineOf(s, 0), 1);
|
|
146
|
+
assert.equal(lineOf(s, 5), 2);
|
|
147
|
+
assert.equal(lineOf(s, 9), 3);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// --- walk / isGenerated ---------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
test('walk: skips excluded dirs, tests, generated files', () => {
|
|
153
|
+
const root = mkdtempSync(path.join(tmpdir(), 'antislop-walk-'));
|
|
154
|
+
try {
|
|
155
|
+
mkdirSync(path.join(root, 'components'), { recursive: true });
|
|
156
|
+
mkdirSync(path.join(root, 'node_modules', 'pkg'), { recursive: true });
|
|
157
|
+
mkdirSync(path.join(root, 'dist'), { recursive: true });
|
|
158
|
+
writeFileSync(path.join(root, 'components', 'Card.tsx'), 'export const Card = 1;');
|
|
159
|
+
writeFileSync(path.join(root, 'components', 'Card.test.tsx'), 'export const t = 1;');
|
|
160
|
+
writeFileSync(path.join(root, 'components', 'util.test.mjs'), 'export const u = 1;');
|
|
161
|
+
writeFileSync(path.join(root, 'payload-types.ts'), 'export type X = 1;');
|
|
162
|
+
writeFileSync(path.join(root, 'node_modules', 'pkg', 'index.js'), 'module.exports = 1;');
|
|
163
|
+
writeFileSync(path.join(root, 'dist', 'out.js'), 'export const o = 1;');
|
|
164
|
+
|
|
165
|
+
const rels = walk(root, root).map((f) => f.rel);
|
|
166
|
+
assert.deepEqual(rels, ['components/Card.tsx']);
|
|
167
|
+
} finally {
|
|
168
|
+
rmSync(root, { recursive: true, force: true });
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('isGenerated: payload-types and next-env, nothing else', () => {
|
|
173
|
+
assert.ok(isGenerated('payload-types.ts'));
|
|
174
|
+
assert.ok(isGenerated('src/app/payload-types.ts'));
|
|
175
|
+
assert.ok(isGenerated('next-env.d.ts'));
|
|
176
|
+
assert.ok(!isGenerated('src/lib/dbs.ts'));
|
|
177
|
+
assert.ok(!isGenerated('types.ts'));
|
|
178
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Anti-slop gates for this repository (installed by opencode-anti-slop).
|
|
2
|
+
# Runs the deterministic checks on every push and pull request. Tune the
|
|
3
|
+
# checks via anti-slop.config.json at the repo root.
|
|
4
|
+
name: anti-slop
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
pull_request:
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
gates:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: actions/setup-node@v4
|
|
16
|
+
with:
|
|
17
|
+
node-version: 22
|
|
18
|
+
- run: node scripts/anti-slop/check-modularity.mjs
|
|
19
|
+
- run: node scripts/anti-slop/check-imports.mjs
|
|
20
|
+
- run: node scripts/anti-slop/check-docs.mjs
|
|
21
|
+
- run: node scripts/anti-slop/check-duplicates.mjs
|
|
22
|
+
- run: node scripts/anti-slop/generate-component-index.mjs
|