@event4u/agent-config 7.3.0 → 7.5.0
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/.claude-plugin/marketplace.json +4 -1
- package/CHANGELOG.md +81 -0
- package/README.md +2 -2
- package/dist/agent-src/commands/fix/pr-comments.md +42 -13
- package/dist/agent-src/commands/review-changes.md +14 -7
- package/dist/agent-src/rules/artifact-engagement-recording.md +9 -0
- package/dist/agent-src/rules/cli-output-handling.md +18 -0
- package/dist/agent-src/rules/domain-safety-pii.md +4 -3
- package/dist/agent-src/rules/icon-consistency.md +1 -0
- package/dist/agent-src/rules/legal-safety-floor.md +2 -2
- package/dist/agent-src/rules/untrusted-input-defense.md +8 -5
- package/dist/agent-src/skills/adr-create/SKILL.md +5 -1
- package/dist/agent-src/skills/adversarial-review/SKILL.md +1 -1
- package/dist/agent-src/skills/blade-ui/SKILL.md +4 -0
- package/dist/agent-src/skills/brand-to-tokens/SKILL.md +2 -1
- package/dist/agent-src/skills/code-review/SKILL.md +4 -0
- package/dist/agent-src/skills/context-authoring/SKILL.md +1 -1
- package/dist/agent-src/skills/design-intelligence/SKILL.md +40 -0
- package/dist/agent-src/skills/design-review/SKILL.md +23 -11
- package/dist/agent-src/skills/design-system-capture/SKILL.md +41 -0
- package/dist/agent-src/skills/design-system-capture/reference/design-system-json.md +62 -0
- package/dist/agent-src/skills/design-tokens/SKILL.md +1 -0
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +6 -0
- package/dist/agent-src/skills/flux/SKILL.md +4 -0
- package/dist/agent-src/skills/iconography/SKILL.md +1 -0
- package/dist/agent-src/skills/image-editing/evals/triggers.json +13 -13
- package/dist/agent-src/skills/judge-artifact-completeness/SKILL.md +136 -0
- package/dist/agent-src/skills/judge-artifact-completeness/calibration/fixtures.json +149 -0
- package/dist/agent-src/skills/judge-artifact-completeness/evals/triggers.json +17 -0
- package/dist/agent-src/skills/judge-artifact-completeness/rubrics/architecture-score.json +73 -0
- package/dist/agent-src/skills/judge-artifact-completeness/rubrics/pr-review-score.json +65 -0
- package/dist/agent-src/skills/judge-artifact-completeness/rubrics/roadmap-score.json +73 -0
- package/dist/agent-src/skills/judge-artifact-completeness/rubrics/ticket-quality-score.json +73 -0
- package/dist/agent-src/skills/judge-injection-defense/SKILL.md +99 -0
- package/dist/agent-src/skills/judge-injection-defense/calibration/fixtures.json +24 -0
- package/dist/agent-src/skills/judge-injection-defense/evals/triggers.json +17 -0
- package/dist/agent-src/skills/judge-injection-defense/fixtures/behavioral-fixtures.json +63 -0
- package/dist/agent-src/skills/judge-injection-defense/fixtures/coverage-report.md +53 -0
- package/dist/agent-src/skills/judge-injection-defense/fixtures/perturbation-taxonomy.json +110 -0
- package/dist/agent-src/skills/judge-synthesis/SKILL.md +134 -0
- package/dist/agent-src/skills/judge-synthesis/evals/triggers.json +17 -0
- package/dist/agent-src/skills/logo-generation/SKILL.md +1 -1
- package/dist/agent-src/skills/markitdown/SKILL.md +1 -1
- package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +18 -0
- package/dist/agent-src/skills/react-shadcn-ui/reference/registry.md +59 -0
- package/dist/agent-src/skills/refine-ticket/SKILL.md +5 -1
- package/dist/agent-src/skills/release-comms/SKILL.md +1 -1
- package/dist/agent-src/skills/roadmap-management/SKILL.md +4 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
- package/dist/agent-src/skills/script-writing/SKILL.md +1 -1
- package/dist/agent-src/skills/sql-writing/SKILL.md +1 -1
- package/dist/agent-src/skills/systematic-debugging/SKILL.md +1 -1
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +4 -0
- package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +1 -1
- package/dist/agent-src/skills/token-optimizer/SKILL.md +1 -1
- package/dist/agent-src/skills/typography-system/SKILL.md +2 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +109 -43
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +8 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +7 -4
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/router.json +1 -1
- package/dist/server/schemas/settings.js +6 -0
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/architecture.md +2 -2
- package/docs/catalog.md +28 -20
- package/docs/command-flows.md +1 -1
- package/docs/enforcement-by-host.md +9 -0
- package/docs/governance-advantage.md +72 -0
- package/docs/guidelines/design-antipatterns.md +26 -0
- package/docs/guidelines/design-canon.md +62 -0
- package/package.json +2 -1
- package/src/config/agent-settings.template.yml +21 -0
- package/src/scripts/_cli/cmd_analyze_session.ts +347 -0
- package/src/scripts/_dispatch.bash +8 -0
- package/src/scripts/_lib/token_count.ts +110 -35
- package/src/scripts/audit_initial_context.ts +37 -12
- package/src/scripts/bench_quality_run.ts +253 -0
- package/src/scripts/bench_rule_load_latency.ts +178 -0
- package/src/scripts/check_kernel_prefix_stability.ts +0 -0
- package/src/scripts/check_quality_regression.ts +244 -0
- package/src/scripts/check_token_quality_golden.ts +253 -0
- package/src/scripts/check_token_regression.ts +266 -0
- package/src/scripts/design_slop_rules.test.ts +193 -0
- package/src/scripts/design_slop_rules.ts +502 -0
- package/src/scripts/external_sources_denylist.json +7 -1
- package/src/scripts/hook_manifest.yaml +26 -3
- package/src/scripts/hooks/design_slop_hook.ts +181 -0
- package/src/scripts/hooks/rtk_wrap_hook.ts +228 -0
- package/src/scripts/lint_agent_security.ts +1 -0
- package/src/scripts/lint_confusables.ts +197 -0
- package/src/scripts/lint_design_slop.ts +245 -0
- package/src/scripts/probe_host_compliance.ts +145 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Token-regression gate (token-saving Phase 0).
|
|
4
|
+
*
|
|
5
|
+
* Fails CI when a tracked always-loaded / projected token surface grows more
|
|
6
|
+
* than the tolerance (default 5%) over a committed baseline. This is the
|
|
7
|
+
* RELATIVE guard — distinct from `audit-tokens-budget` (absolute budgets) and
|
|
8
|
+
* from the cache-prefix byte-stability guard (Phase 5).
|
|
9
|
+
*
|
|
10
|
+
* Source of truth is `internal/bench/reports/projection-cost.json`, which is
|
|
11
|
+
* now produced with EXACT counts via the real tokenizer
|
|
12
|
+
* (`src/scripts/_lib/token_count.ts` → js-tiktoken cl100k_base). Run
|
|
13
|
+
* `task audit-tokens` first so the report reflects the working tree.
|
|
14
|
+
*
|
|
15
|
+
* Gated metrics (all `tokens_gpt`):
|
|
16
|
+
* - eager_rule_load — thin_projection.eager_gpt (the eager always-on load)
|
|
17
|
+
* - thin_rule_load — thin_projection.thin_gpt
|
|
18
|
+
* - skill_descriptions — description_catalog.skills_core_source.tokens_gpt
|
|
19
|
+
* - command_descriptions — description_catalog.commands_core_source.tokens_gpt
|
|
20
|
+
* - mcp_schemas — Σ mcp_tool_schemas[*].tokens_gpt
|
|
21
|
+
*
|
|
22
|
+
* A baseline captured under a different `token_method` (e.g. proxy → exact)
|
|
23
|
+
* is a legitimate reset, not a regression — the gate WARNS and asks for a
|
|
24
|
+
* `--update-baseline` rather than failing on the method switch alone.
|
|
25
|
+
*
|
|
26
|
+
* CLI:
|
|
27
|
+
* ./scripts-run src/scripts/check_token_regression # gate (read-only)
|
|
28
|
+
* ./scripts-run src/scripts/check_token_regression --json # machine output
|
|
29
|
+
* ./scripts-run src/scripts/check_token_regression --tolerance 0.05
|
|
30
|
+
* ./scripts-run src/scripts/check_token_regression --update-baseline # record current as baseline
|
|
31
|
+
*
|
|
32
|
+
* Exit codes:
|
|
33
|
+
* 0 — within tolerance (or baseline written / warmup: no baseline yet)
|
|
34
|
+
* 1 — argument / file error
|
|
35
|
+
* 2 — regression: a metric exceeded baseline by more than the tolerance
|
|
36
|
+
*/
|
|
37
|
+
import * as fs from 'node:fs';
|
|
38
|
+
import * as path from 'node:path';
|
|
39
|
+
import process from 'node:process';
|
|
40
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
41
|
+
|
|
42
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
43
|
+
// REPO_ROOT = <repo>/src/scripts/<file> → up two.
|
|
44
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
45
|
+
|
|
46
|
+
const PROJECTION_REPORT = path.join(
|
|
47
|
+
REPO_ROOT,
|
|
48
|
+
'internal/bench/reports/projection-cost.json',
|
|
49
|
+
);
|
|
50
|
+
const BASELINE_FILE = path.join(
|
|
51
|
+
REPO_ROOT,
|
|
52
|
+
'internal/bench/reports/token-baseline.json',
|
|
53
|
+
);
|
|
54
|
+
const DEFAULT_TOLERANCE = 0.05;
|
|
55
|
+
|
|
56
|
+
type Json = Record<string, unknown>;
|
|
57
|
+
|
|
58
|
+
interface Metrics {
|
|
59
|
+
token_method: string;
|
|
60
|
+
metrics: Record<string, number>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function _num(obj: unknown, ...keys: string[]): number {
|
|
64
|
+
let cur: unknown = obj;
|
|
65
|
+
for (const k of keys) {
|
|
66
|
+
if (cur && typeof cur === 'object' && k in (cur as Json)) {
|
|
67
|
+
cur = (cur as Json)[k];
|
|
68
|
+
} else {
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return typeof cur === 'number' ? cur : 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Extract the flat gated-metric map from a projection-cost report. */
|
|
76
|
+
function extract_metrics(report: Json): Metrics {
|
|
77
|
+
const mcp = (report['mcp_tool_schemas'] as Json) ?? {};
|
|
78
|
+
let mcp_total = 0;
|
|
79
|
+
for (const server of Object.values(mcp)) {
|
|
80
|
+
mcp_total += _num(server, 'tokens_gpt');
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
token_method: String(report['token_method'] ?? 'unknown'),
|
|
84
|
+
metrics: {
|
|
85
|
+
eager_rule_load: _num(report, 'thin_projection', 'eager_gpt'),
|
|
86
|
+
thin_rule_load: _num(report, 'thin_projection', 'thin_gpt'),
|
|
87
|
+
skill_descriptions: _num(
|
|
88
|
+
report,
|
|
89
|
+
'description_catalog',
|
|
90
|
+
'skills_core_source',
|
|
91
|
+
'tokens_gpt',
|
|
92
|
+
),
|
|
93
|
+
command_descriptions: _num(
|
|
94
|
+
report,
|
|
95
|
+
'description_catalog',
|
|
96
|
+
'commands_core_source',
|
|
97
|
+
'tokens_gpt',
|
|
98
|
+
),
|
|
99
|
+
mcp_schemas: mcp_total,
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function _readJson(file: string): Json | null {
|
|
105
|
+
try {
|
|
106
|
+
return JSON.parse(fs.readFileSync(file, 'utf-8')) as Json;
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface Row {
|
|
113
|
+
id: string;
|
|
114
|
+
current: number;
|
|
115
|
+
baseline: number;
|
|
116
|
+
pct: number; // signed fraction, e.g. 0.07 = +7%
|
|
117
|
+
regressed: boolean;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface GateResult {
|
|
121
|
+
status: 'ok' | 'regression' | 'warmup' | 'method-changed';
|
|
122
|
+
tolerance: number;
|
|
123
|
+
current_method: string;
|
|
124
|
+
baseline_method: string | null;
|
|
125
|
+
rows: Row[];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function evaluate(
|
|
129
|
+
current: Metrics,
|
|
130
|
+
baseline: Metrics | null,
|
|
131
|
+
tolerance: number,
|
|
132
|
+
): GateResult {
|
|
133
|
+
if (baseline === null) {
|
|
134
|
+
return {
|
|
135
|
+
status: 'warmup',
|
|
136
|
+
tolerance,
|
|
137
|
+
current_method: current.token_method,
|
|
138
|
+
baseline_method: null,
|
|
139
|
+
rows: [],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const method_changed = baseline.token_method !== current.token_method;
|
|
143
|
+
const rows: Row[] = [];
|
|
144
|
+
for (const [id, cur] of Object.entries(current.metrics)) {
|
|
145
|
+
const base = baseline.metrics[id] ?? 0;
|
|
146
|
+
const pct = base > 0 ? (cur - base) / base : 0;
|
|
147
|
+
rows.push({
|
|
148
|
+
id,
|
|
149
|
+
current: cur,
|
|
150
|
+
baseline: base,
|
|
151
|
+
pct,
|
|
152
|
+
// A method switch is a legitimate baseline reset, not a regression.
|
|
153
|
+
regressed: !method_changed && base > 0 && pct > tolerance,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
rows.sort((a, b) => b.pct - a.pct);
|
|
157
|
+
const anyRegressed = rows.some((r) => r.regressed);
|
|
158
|
+
const status: GateResult['status'] = anyRegressed
|
|
159
|
+
? 'regression'
|
|
160
|
+
: method_changed
|
|
161
|
+
? 'method-changed'
|
|
162
|
+
: 'ok';
|
|
163
|
+
return {
|
|
164
|
+
status,
|
|
165
|
+
tolerance,
|
|
166
|
+
current_method: current.token_method,
|
|
167
|
+
baseline_method: baseline.token_method,
|
|
168
|
+
rows,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function _fmtPct(p: number): string {
|
|
173
|
+
const sign = p >= 0 ? '+' : '';
|
|
174
|
+
return `${sign}${(p * 100).toFixed(1)}%`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function main(argv: string[]): number {
|
|
178
|
+
let tolerance = DEFAULT_TOLERANCE;
|
|
179
|
+
let asJson = false;
|
|
180
|
+
let updateBaseline = false;
|
|
181
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
182
|
+
const a = argv[i];
|
|
183
|
+
if (a === '--json') asJson = true;
|
|
184
|
+
else if (a === '--update-baseline') updateBaseline = true;
|
|
185
|
+
else if (a === '--tolerance') {
|
|
186
|
+
const v = Number(argv[(i += 1)]);
|
|
187
|
+
if (!Number.isFinite(v) || v < 0) {
|
|
188
|
+
process.stderr.write(`error: invalid --tolerance\n`);
|
|
189
|
+
return 1;
|
|
190
|
+
}
|
|
191
|
+
tolerance = v;
|
|
192
|
+
} else if (a === '-h' || a === '--help') {
|
|
193
|
+
process.stdout.write(
|
|
194
|
+
'usage: check_token_regression [--json] [--tolerance F] [--update-baseline]\n',
|
|
195
|
+
);
|
|
196
|
+
return 0;
|
|
197
|
+
} else {
|
|
198
|
+
process.stderr.write(`error: unknown argument: ${a}\n`);
|
|
199
|
+
return 1;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const report = _readJson(PROJECTION_REPORT);
|
|
204
|
+
if (report === null) {
|
|
205
|
+
process.stderr.write(
|
|
206
|
+
`error: cannot read ${path.relative(REPO_ROOT, PROJECTION_REPORT)} — run \`task audit-tokens\` first\n`,
|
|
207
|
+
);
|
|
208
|
+
return 1;
|
|
209
|
+
}
|
|
210
|
+
const current = extract_metrics(report);
|
|
211
|
+
|
|
212
|
+
if (updateBaseline) {
|
|
213
|
+
const payload = JSON.stringify(current, null, 2) + '\n';
|
|
214
|
+
fs.writeFileSync(BASELINE_FILE, payload);
|
|
215
|
+
process.stdout.write(
|
|
216
|
+
`✅ token baseline written: ${path.relative(REPO_ROOT, BASELINE_FILE)} (method: ${current.token_method})\n`,
|
|
217
|
+
);
|
|
218
|
+
return 0;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const baselineRaw = _readJson(BASELINE_FILE);
|
|
222
|
+
const baseline: Metrics | null =
|
|
223
|
+
baselineRaw === null ? null : (baselineRaw as unknown as Metrics);
|
|
224
|
+
const result = evaluate(current, baseline, tolerance);
|
|
225
|
+
|
|
226
|
+
if (asJson) {
|
|
227
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
228
|
+
} else if (result.status === 'warmup') {
|
|
229
|
+
process.stdout.write(
|
|
230
|
+
`⚠️ no token baseline yet — run \`./scripts-run src/scripts/check_token_regression --update-baseline\` to record one.\n`,
|
|
231
|
+
);
|
|
232
|
+
} else {
|
|
233
|
+
for (const r of result.rows) {
|
|
234
|
+
const mark = r.regressed ? '❌' : '✅';
|
|
235
|
+
process.stdout.write(
|
|
236
|
+
`${mark} ${r.id}: ${r.current} vs baseline ${r.baseline} (${_fmtPct(r.pct)})\n`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
if (result.status === 'method-changed') {
|
|
240
|
+
process.stdout.write(
|
|
241
|
+
`⚠️ token_method changed (${result.baseline_method} → ${result.current_method}); ` +
|
|
242
|
+
`baseline reset is legitimate — run --update-baseline to re-anchor.\n`,
|
|
243
|
+
);
|
|
244
|
+
} else if (result.status === 'regression') {
|
|
245
|
+
process.stdout.write(
|
|
246
|
+
`❌ token regression: a projection grew >${(tolerance * 100).toFixed(0)}% over baseline.\n`,
|
|
247
|
+
);
|
|
248
|
+
} else {
|
|
249
|
+
process.stdout.write(
|
|
250
|
+
`✅ token surfaces within +${(tolerance * 100).toFixed(0)}% of baseline.\n`,
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return result.status === 'regression' ? 2 : 0;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const _IS_MAIN =
|
|
259
|
+
process.argv[1] !== undefined &&
|
|
260
|
+
import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href;
|
|
261
|
+
|
|
262
|
+
if (_IS_MAIN) {
|
|
263
|
+
process.exit(main(process.argv.slice(2)));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export { extract_metrics, main };
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { SLOP_RULES, type DesignContext } from "./design_slop_rules.js";
|
|
3
|
+
import { scanFile } from "./lint_design_slop.js";
|
|
4
|
+
|
|
5
|
+
// Context with no DESIGN.md → all gates OFF, every rule active.
|
|
6
|
+
const NO_CTX: DesignContext = { raw: "", has: () => false };
|
|
7
|
+
const ctxWith = (raw: string): DesignContext => ({
|
|
8
|
+
raw: raw.toLowerCase(),
|
|
9
|
+
has: (...ks: string[]) => ks.some((k) => raw.toLowerCase().includes(k.toLowerCase())),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Calibration corpus (Phase 4). One positive (must fire) + one negative (must be
|
|
14
|
+
* clean) per rule. `ext` drives the engine class. No untested tell — the guard
|
|
15
|
+
* below asserts every registry rule appears here.
|
|
16
|
+
*/
|
|
17
|
+
const FIXTURES: Record<string, { ext: string; positive: string; negative: string }> = {
|
|
18
|
+
"slop-v1-side-stripe": {
|
|
19
|
+
ext: "css",
|
|
20
|
+
positive: ".card { border-left: 4px solid #6c5ce7; padding: 16px; }",
|
|
21
|
+
negative: ".card { border-left: 1px solid #eee; padding: 16px; }",
|
|
22
|
+
},
|
|
23
|
+
"slop-v3-ghost-card": {
|
|
24
|
+
ext: "css",
|
|
25
|
+
positive: ".card { border: 1px solid #ddd; box-shadow: 0 8px 24px rgba(0,0,0,.1); }",
|
|
26
|
+
negative: ".card { box-shadow: 0 2px 4px rgba(0,0,0,.1); }",
|
|
27
|
+
},
|
|
28
|
+
"slop-v6-diagonal-stripes": {
|
|
29
|
+
ext: "css",
|
|
30
|
+
positive: ".bg { background: repeating-linear-gradient(45deg,#000,#000 10px,#111 10px,#111 20px); }",
|
|
31
|
+
negative: ".bg { background: linear-gradient(#fff,#eee); }",
|
|
32
|
+
},
|
|
33
|
+
"slop-c2-gradient-text": {
|
|
34
|
+
ext: "css",
|
|
35
|
+
positive: ".h { background: linear-gradient(90deg,#f00,#00f); -webkit-background-clip: text; color: transparent; }",
|
|
36
|
+
negative: ".h { color: #111; }",
|
|
37
|
+
},
|
|
38
|
+
"slop-c5-cream-palette": {
|
|
39
|
+
ext: "css",
|
|
40
|
+
positive: "body { background: #f5f1ea; } .a { color: #b08947; }",
|
|
41
|
+
negative: "body { background: #ffffff; } .a { color: #111111; }",
|
|
42
|
+
},
|
|
43
|
+
"slop-t4-eyebrow-overuse": {
|
|
44
|
+
ext: "html",
|
|
45
|
+
positive:
|
|
46
|
+
'<section><span class="uppercase">Features</span></section><section><span class="uppercase">Pricing</span></section><section><h2>Contact</h2></section>',
|
|
47
|
+
negative:
|
|
48
|
+
'<section><span class="uppercase">Features</span></section><section><h2>Pricing</h2></section><section><h2>Contact</h2></section>',
|
|
49
|
+
},
|
|
50
|
+
"slop-t6-crushed-tracking": {
|
|
51
|
+
ext: "css",
|
|
52
|
+
positive: ".display { letter-spacing: -0.05em; }",
|
|
53
|
+
negative: ".display { letter-spacing: -0.02em; }",
|
|
54
|
+
},
|
|
55
|
+
"slop-t7-default-fonts": {
|
|
56
|
+
ext: "css",
|
|
57
|
+
positive: "body { font-family: 'Inter', sans-serif; }",
|
|
58
|
+
negative: "body { font-family: 'Söhne', sans-serif; }",
|
|
59
|
+
},
|
|
60
|
+
"slop-l4-numbered-markers": {
|
|
61
|
+
ext: "html",
|
|
62
|
+
positive: "<div>01</div><div>02</div><div>03</div>",
|
|
63
|
+
negative: "<div>Discover</div><div>Build</div><div>Ship</div>",
|
|
64
|
+
},
|
|
65
|
+
"slop-l8-zindex-magic": {
|
|
66
|
+
ext: "css",
|
|
67
|
+
positive: ".modal { z-index: 9999; }",
|
|
68
|
+
negative: ".modal { z-index: 400; }",
|
|
69
|
+
},
|
|
70
|
+
"slop-m4-transition-all": {
|
|
71
|
+
ext: "css",
|
|
72
|
+
positive: ".x { transition: all 200ms ease-out; }",
|
|
73
|
+
negative: ".x { transition: transform 200ms ease-out; }",
|
|
74
|
+
},
|
|
75
|
+
"slop-m2-animate-layout": {
|
|
76
|
+
ext: "css",
|
|
77
|
+
positive: ".x { transition: width 200ms ease; }",
|
|
78
|
+
negative: ".x { transition: transform 200ms, opacity 200ms; }",
|
|
79
|
+
},
|
|
80
|
+
"slop-cp1-em-dash": {
|
|
81
|
+
ext: "md",
|
|
82
|
+
// ≥80 words is the rule floor; positive carries 4 em-dashes (well over the
|
|
83
|
+
// ~2-per-500 density), negative carries one across the same length.
|
|
84
|
+
positive:
|
|
85
|
+
"We build tools — fast, focused tools — that teams genuinely love using every single day of the working week. " +
|
|
86
|
+
"The product is calm and clear and useful, and people return to it daily because it respects their time — and their attention — and never wastes a single click of effort or a moment of their busy day, and that quiet reliability is the whole point of the thing we set out to make together, and the team keeps shipping the small improvements that matter most to the people who rely on it.",
|
|
87
|
+
negative:
|
|
88
|
+
"We build tools, fast and focused tools, that teams genuinely love using every single day of the working week. " +
|
|
89
|
+
"The product is calm and clear and useful, and people return to it daily because it respects their time and their attention and never wastes a single click of effort or a moment of their busy day — and that quiet reliability is the whole point of the thing we set out to make together, and the team keeps shipping the small improvements that matter most to the people who rely on it.",
|
|
90
|
+
},
|
|
91
|
+
"slop-cp2-buzzword": {
|
|
92
|
+
ext: "md",
|
|
93
|
+
positive: "We streamline and empower your robust, world-class workflow.",
|
|
94
|
+
negative: "We cut page load by 40% and remove three steps from checkout.",
|
|
95
|
+
},
|
|
96
|
+
"slop-lock-shape": {
|
|
97
|
+
ext: "css",
|
|
98
|
+
positive: ".a{border-radius:4px}.b{border-radius:8px}.c{border-radius:12px}.d{border-radius:20px}",
|
|
99
|
+
negative: ".a{border-radius:8px}.b{border-radius:12px}",
|
|
100
|
+
},
|
|
101
|
+
"slop-lock-colour": {
|
|
102
|
+
ext: "css",
|
|
103
|
+
positive: ".a{color:#e11d48}.b{background:#2563eb}.c{border-color:#16a34a}",
|
|
104
|
+
negative: ".a{color:#111827}.b{background:#6b7280}.c{border-color:#2563eb}",
|
|
105
|
+
},
|
|
106
|
+
"slop-l9-section-monotony": {
|
|
107
|
+
ext: "html",
|
|
108
|
+
// 8 identical media+text sections → 1 distinct layout family (< 4)
|
|
109
|
+
positive: '<section><div class="grid"><img src="x"><h2>A</h2><p>t</p></div></section>'.repeat(8),
|
|
110
|
+
// 8 sections, 4 distinct signatures → not monotonous
|
|
111
|
+
negative: (
|
|
112
|
+
'<section><div class="grid"><img src="x"><h2>A</h2></div></section>' +
|
|
113
|
+
"<section><h2>B</h2><p>t</p></section>" +
|
|
114
|
+
'<section><div class="flex"><p>c</p></div></section>' +
|
|
115
|
+
'<section><img src="y"></section>'
|
|
116
|
+
).repeat(2),
|
|
117
|
+
},
|
|
118
|
+
"slop-l10-zigzag": {
|
|
119
|
+
ext: "html",
|
|
120
|
+
// 3 consecutive media+text two-column sections → run > 2
|
|
121
|
+
positive: '<section><div class="grid"><img src="x"><h2>A</h2><p>t</p></div></section>'.repeat(3),
|
|
122
|
+
// only 2 consecutive → within cap
|
|
123
|
+
negative: '<section><div class="grid"><img src="x"><h2>A</h2><p>t</p></div></section>'.repeat(2),
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
function findingsFor(ruleId: string, content: string, ext: string, ctx = NO_CTX) {
|
|
128
|
+
return scanFile(content, `fixture.${ext}`, ctx).filter((f) => f.rule === ruleId);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
describe("design-slop rule registry", () => {
|
|
132
|
+
it("every rule has a unique id + a catalog id", () => {
|
|
133
|
+
const ids = SLOP_RULES.map((r) => r.id);
|
|
134
|
+
expect(new Set(ids).size).toBe(ids.length);
|
|
135
|
+
for (const r of SLOP_RULES) {
|
|
136
|
+
expect(r.id).toMatch(/^slop-/);
|
|
137
|
+
expect(r.catalogId).toMatch(/^[A-Z]+\d+$/);
|
|
138
|
+
expect(r.engines.length).toBeGreaterThan(0);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("no untested tell — every registry rule has a +/- fixture", () => {
|
|
143
|
+
for (const r of SLOP_RULES) {
|
|
144
|
+
expect(FIXTURES[r.id], `missing fixture for ${r.id}`).toBeDefined();
|
|
145
|
+
}
|
|
146
|
+
// and no orphan fixtures
|
|
147
|
+
const ids = new Set(SLOP_RULES.map((r) => r.id));
|
|
148
|
+
for (const k of Object.keys(FIXTURES)) {
|
|
149
|
+
expect(ids.has(k), `orphan fixture ${k}`).toBe(true);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("positive fixtures fire, negative fixtures are clean", () => {
|
|
154
|
+
for (const r of SLOP_RULES) {
|
|
155
|
+
const fx = FIXTURES[r.id];
|
|
156
|
+
if (!fx) continue;
|
|
157
|
+
it(`${r.id} (${r.catalogId})`, () => {
|
|
158
|
+
expect(findingsFor(r.id, fx.positive, fx.ext).length, "positive must fire").toBeGreaterThan(0);
|
|
159
|
+
expect(findingsFor(r.id, fx.negative, fx.ext).length, "negative must be clean").toBe(0);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("context gates suppress when DESIGN.md declares intent", () => {
|
|
166
|
+
it("t7 default-font is suppressed when DESIGN.md adopts Inter", () => {
|
|
167
|
+
const fx = FIXTURES["slop-t7-default-fonts"]!;
|
|
168
|
+
expect(findingsFor("slop-t7-default-fonts", fx.positive, "css", NO_CTX).length).toBeGreaterThan(0);
|
|
169
|
+
expect(
|
|
170
|
+
findingsFor("slop-t7-default-fonts", fx.positive, "css", ctxWith("Body font: Inter (chosen for variable metrics in a data-dense dashboard)")).length,
|
|
171
|
+
).toBe(0);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("c5 cream-palette is suppressed under a declared warm-neutral language", () => {
|
|
175
|
+
const fx = FIXTURES["slop-c5-cream-palette"]!;
|
|
176
|
+
expect(findingsFor("slop-c5-cream-palette", fx.positive, "css", NO_CTX).length).toBeGreaterThan(0);
|
|
177
|
+
expect(
|
|
178
|
+
findingsFor("slop-c5-cream-palette", fx.positive, "css", ctxWith("Palette: warm-neutral, first-party")).length,
|
|
179
|
+
).toBe(0);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe("inline-ignore", () => {
|
|
184
|
+
it("design-slop-disable-next-line suppresses the next line", () => {
|
|
185
|
+
const css = "/* design-slop-disable-next-line slop-m4-transition-all */\n.x { transition: all 200ms; }";
|
|
186
|
+
expect(findingsFor("slop-m4-transition-all", css, "css").length).toBe(0);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("file-scope design-slop-disable suppresses the whole file", () => {
|
|
190
|
+
const css = "/* design-slop-disable slop-l8-zindex-magic */\n.a{z-index:9999}\n.b{z-index:999}";
|
|
191
|
+
expect(findingsFor("slop-l8-zindex-magic", css, "css").length).toBe(0);
|
|
192
|
+
});
|
|
193
|
+
});
|