slopslint
v0.3.2
Published
Deterministic repository gate for duplication, orphan surfaces, contract claims, tombstones, and monotonic ceilings.
Maintainers
Readme
slopslint
A deterministic slop gate for repositories where a lot of the code is written by agents. It asks two independent questions: is this repeated? and who uses this? Duplication and orphan findings are measured in configured scopes, specific debt can be accepted as a permanent reviewable tombstone, and each total is held under a committed ceiling that can only ever go down. An optional claims map also proves that every declared public surface is tied to a live contract item.
One self-contained binary. No runtime, no package manager, no separately installed detector.
slopslint check --classify --enforceWhy
Agent-written code duplicates. Not dramatically, and rarely in a way any single review catches: a helper re-derived in a second module, a setup block pasted into a fourth test, a rejection path written four times with one word changed. Each instance is defensible. The aggregate is not. Agent-written repositories also accumulate plausible-looking surfaces that nobody owns: for example, a generated copy whose only inbound references are its generator and its own guard test. A duplication-only gate cannot see that directory because nothing inside it needs to be repeated.
A percentage threshold does not hold that line, because it drifts up quietly as the repository grows. slopslint holds absolute counts per scope, and each committed count is monotonic: a change may lower it, never raise it. Debt you decide to keep is not silently subtracted — it is written down as a tombstone that names the incident, the rule it established, and the evidence.
Install
Binary (recommended — nothing else needed):
curl -fsSL https://github.com/thellmwhisperer/slopslint/releases/latest/download/slopslint-linux-x64 \
-o /usr/local/bin/slopslint && chmod +x /usr/local/bin/slopslintPublished for darwin-arm64, darwin-x64, linux-arm64, linux-x64, and
windows-x64.
GitHub Action:
- uses: thellmwhisperer/[email protected]
with:
args: check --classify --enforcenpm (needs Node 20+):
npx slopslint check --classify --enforceConfigure
The base duplication setup uses two files under .slop/, both yours to own and
commit. Orphan and claims checks are off unless their blocks are present.
.slop/config.yml declares what to measure. Scope names, paths, and globs are
your data — slopslint ships no default layout.
schema: 1
detector:
name: jscpd
version: "4.2.5" # must match the detector linked into the binary
defaults:
format: python
mode: mild
min_lines: 5
min_tokens: 50
global_ignore:
- "**/__pycache__/**"
- "**/node_modules/**"
scopes:
python_production:
scan_path: py
pattern: "**/*.py"
ignore:
- "**/test_*.py"
- "**/conftest.py"
python_tests_fixtures:
scan_path: py
pattern: "**/{test_*,conftest}.py"
# Optional. Every list is repository-relative and uses fast-glob syntax.
orphan_scopes:
tools:
files: ["tools/**/*.{ts,js}"]
directories: ["skills/*"]
exported_symbols: ["src/**/*.{ts,js}"]
test_files: ["test/**/*.{ts,js}"]
generation_files: ["scripts/generate*.{ts,js}"]
ignore: ["**/fixtures/**"]
# Optional. Surface groups are labels, not language-specific concepts.
claims:
file: .slop/claims.yml
surfaces:
commands:
files: ["bin/*"]
tools:
directories: ["tools/*"]
assets:
files: ["assets/**/*"]
api:
exported_symbols: ["src/index.ts"]Scopes are measured independently and never leak into each other, so
duplication in production code cannot hide inside the test scope. Globs match
basenames: a directory named test_data/ keeps its production files in the
production scope.
An orphan scope enumerates exactly the configured files, directories, and
JavaScript/TypeScript exports. The census then searches repository text for
deterministic inbound evidence: static or dynamic imports, require calls,
repository-relative path strings (including build files, CI, and docs). In
Markdown, dot-relative paths such as ../src/used.ts are resolved from the
document's directory. Embed/generate directives that name the path also count.
A surface is an orphan when it has no inbound consumer outside itself,
configured test files, or configured generation files. Those ignored
references are included in the report with their reason. slopslint does not
execute code, inspect runtime telemetry, or ask a model to infer ownership.
.slop/ceilings.yml declares what you will tolerate:
schema: 1
scopes:
python_production:
active_clones_ceiling: 21
python_tests_fixtures:
active_clones_ceiling: 110
orphan_scopes:
tools:
active_orphans_ceiling: 3The ceiling is not a budget to spend. check --enforce fails when the measured
count is above it (a regression) and when it is below it (an
improvement you did not record). The same rule applies to
active_orphans_ceiling. Paying debt down means lowering the number in the same
change, so the base branch always states the truth.
Claims
When claims is configured, its committed YAML file maps each censused surface to a
contract file and an opaque item string:
schema: 1
claims:
bin/slopslint:
contract:
file: docs/contracts.md
item: CLI-SLOPSLINT
src/index.ts#runCheck:
contract:
file: docs/contracts.md
item: API-RUN-CHECKFile and directory identities are their repository-relative paths. Exported
symbol identities are path#symbol. The check fails when a censused surface is
missing, a claim names a surface outside the census, the referenced contract
file is absent, or that file no longer contains the item string. slopslint does
not interpret the item or parse a contract language; choosing the relationship
is an ordinary human- or agent-authored commit.
Commands
| Command | What it does | Exit |
| --- | --- | --- |
| slopslint check | run every configured census and claims sync; print canonical JSON | 0 |
| slopslint check --classify | additionally split accepted from active | 0 |
| slopslint check --classify --enforce | additionally fail on ceiling violations and stale records | 0 / 1 |
| slopslint ratchet <base-ref> | verify the committed ceilings only went down | 0 / 1 |
| slopslint tombstone list \| check \| add | manage .slop/tombstones | 0 / 2 |
Exit 1 is a gate condition. Exit 2 is invalid usage or an invalid record.
Tombstones
A tombstone is a permanent, reviewable record of debt you decided to keep. It CONSUMES a finding; it never produces one.
schema: 1
id: T-SHARED-FIXTURE-HEADER
status: accepted # accepted | legacy
category: duplication # duplication | orphan | alien_code | debt_normalization
title: "Fixture header shared by the ingest suites"
created_at: 2026-08-04
incident:
pattern: >
What the smell is, in general terms.
what_went_wrong: >
What actually happened here.
root_cause: >
Why it got through.
rule_established: >
What the team does differently now.
evidence:
- example: "the concrete instance"
family: clone_fingerprint
match:
family: clone_fingerprint
scope: python_tests_fixtures
fingerprint: <64 hex chars from `slopslint check`>Accepted clones and orphans stop counting as active, so ceilings measure live
debt only. Orphans use the same record shape with category: orphan,
family: orphan_fingerprint, scope, and the finding's SHA-256 fingerprint.
Two rules keep that honest:
- a record whose fingerprint matches nothing is stale and fails the gate — debt you paid off cannot leave its exemption behind;
- categories with no detector (
alien_code,debt_normalization) are standing records: validated, reported, never matched, never stale. They carry the incident now and reserve the schema path for a detector later. Because nothing matches them, several standing records may share family, artifact and category (one per incident); their ids must still be unique.
Standing records use one of the reserved families, which have no detector yet:
agent_artifact_in_repodocumented_as_conventionenvironment_layout_couplingformat_churn: formatting changes to lines the change did not otherwise touch, with no rule in the repository requiring theminline_foreign_languagemock_heavy_test: a test that replaces the code under test's own internals with mocks or monkeypatches instead of driving it through its inputsruntime_dependencyself_validating_test: a test whose fixture encodes the author's assumption about an external tool or format, so it passes against behaviour that does not existspeculative_feature: an option, flag, parameter or code path with no caller or user that asked for itspeculative_hardening: code and tests added to guard against a failure the design already rules out (random suffixes on names unique by construction, retries around calls that cannot fail transiently, validation of values the caller produces itself)subprocess_foreign_interpretertest_weakening: a test an issue or contract asks for is deleted, skipped or loosened
Check output
With only duplication configured, output remains the original array (or the
original classified object), byte for byte. Configuring orphans or claims emits
an object with the existing scopes plus opt-in keys:
{
"scopes": [],
"orphan_scopes": [
{
"scope": "tools",
"count": 1,
"orphans": [
{
"scope": "tools",
"kind": "file",
"path": "tools/generated.ts",
"fingerprint": "<64 lowercase hex characters>",
"evidence": [
{"source": "test/generated.test.ts", "reference": "import", "reason": "test"},
{"source": "scripts/generate.ts", "reference": "path", "reason": "generation"}
]
}
]
}
],
"claims": {
"count": 1,
"surfaces": [
{
"scope": "commands",
"kind": "file",
"surface": "bin/slopslint",
"contract": {"file": "docs/contracts.md", "item": "CLI-SLOPSLINT"}
}
]
}
}The normative JSON Schema is
docs/check-output.schema.json.
What fails closed
The gate refuses to pass rather than guess. Missing or invalid config, a detector version that disagrees with the linked library, zero scanned files, a clone path that escapes the repository, a range that runs backwards, a duplicated or ambiguous YAML key, a tombstone citing a file that does not exist, two records claiming one finding, a missing claim, a stale contract item, an orphan ceiling scope mismatch, or an unreachable git base ref — each is an error, never a silent pass.
Output carries integer totals, repository-relative paths, and a synthetic fingerprint per clone or orphan. It never carries source text, timestamps, floating percentages, or absolute paths, so a report is safe to paste into an issue. Two runs over one unchanged tree produce byte-identical output.
Development
bun install
bun test
bun run typecheck
bun run build:binariesLicense
MIT
