# SlopGuard (TypeScript): complete interface reference (agent-ingestible) slopguard-ts measures complex, undertested code in TypeScript and JavaScript sources. It computes a weighted CRAP (Change Risk Anti-Patterns) score combining cyclomatic and cognitive complexity with line coverage, and prints a structured report you can pipe into jq or fail CI on. It is the TypeScript sibling of slopguard-swift: same formula, same JSON schema, same UX. Status: alpha (v0.1.x). The analyzer is stable and self-tested; the CLI surface and JSON schema may move before v1.0. License: MIT. Repo: https://github.com/JeevanThandi/slopguard-typescript ## Why this matters for coding agents Autonomous/background agents iterate by changing code and running tests. Two codebase properties decide how well that loop works: 1. Coverage: an agent editing untested code cannot tell a fix from a regression. Coverage is the agent's only feedback signal. 2. Complexity: deeply nested, many-branched methods are where agents (like humans) make mistakes. wCRAP captures both in one number per method. Use it as a CI gate (agents cannot merge new slop) or as a work queue (feed the JSON to an agent: "get every method under 30"). ## The formula wCRAP(m) = (cyc × cog) × (1 − cov/100)³ + sqrt(cyc × cog) - cyc: cyclomatic complexity (McCabe), parsed via the TypeScript compiler API. - cog: cognitive complexity (SonarSource 2023 spec): penalises nesting, ignores early-exit shapes (??, plain return), charges a whole switch once. - cov: line coverage percentage in [0, 100], gathered by slopguard-ts itself by driving the project's own test runner (vitest or jest) and reading the resulting istanbul coverage map. Never user-supplied. It cannot be hand-fed or faked; the only way to lower a score without refactoring is to write tests that actually execute the code. - Semantics: cov = 100 → score collapses to sqrt(cyc × cog) (complexity floor). cov = 0 → full quadratic penalty (cyc × cog) + sqrt(cyc × cog). The cubed factor sharply rewards partial coverage: 50% coverage removes 87.5% of the risk term. - Inputs are clamped (complexity ≥ 0, coverage in [0,100]); score is always ≥ 0. - Default "crappy" threshold: 30 (matches the original CRAP paper). - Weighting rationale: a flat 50-case switch (cyc=50, cog=1) scores like a small method; a deeply nested 3-branch tangle (cyc=3, cog=12) scores like medium-complex code. ## Install git clone https://github.com/JeevanThandi/slopguard-typescript.git cd slopguard-typescript npm install && npm run build npm link # exposes slopguard-ts on your PATH Requirements: Node 18.17+. Two top-level runtime dependencies: typescript (Apache-2.0, the official parser) and commander (MIT). ## Subcommands | Command | Purpose | |---|---| | analyze | Walk a directory of TS/JS sources, drive the test runner (vitest/jest) for coverage, emit a wCRAP report (text or JSON). Default subcommand. | | version | Print version metadata as JSON. | `analyze` is the default and `--path` defaults to the current directory; a bare `slopguard-ts` in a project root just works. ## analyze: all flags | Flag | Default | Meaning | |---|---|---| | -p, --path | . | Directory of TypeScript/JavaScript sources, or a single source file. | | -t, --threshold | 30 | wCRAP score above which a method/type is flagged isCrappy. | | --runner | auto-detected | Test runner to drive for coverage. Auto-detected from the project when omitted. | | --project-dir | nearest package.json | Directory the test runner executes in. Defaults to the nearest package.json above --path. | | --no-coverage | off | Skip the test run; complexity only. Every method reads 0% coverage (worst-case scores). | | --coverage-file | — | Pre-built istanbul coverage-final.json to join instead of running tests. Escape hatch for runners slopguard can't drive (nyc, c8, mocha) or CI that already produced coverage. | | --include | — | Glob(s) of files to include. Repeat or pass space-separated. | | --exclude | — | Extra exclude glob(s), combined with built-in defaults (node_modules, dist, build, out, coverage, *.d.ts, *.min.js, *.test.*, *.spec.*, __tests__, …). | | --no-default-excludes | off | Start with an empty exclude list (e.g. to analyze test code itself). | | --json | off | Emit JSON to stdout (default is pretty text). | | --fail-over | — | Exit code 2 if any method's wCRAP exceeds this value. The CI gate. | | -v, --verbose | off | Stream test-runner/subprocess output to stderr. | | --quiet | off | Silence all stderr progress chatter. Wins over --verbose. Stdout unaffected. | ## Exit codes and streams | Code | Meaning | |---|---| | 0 | Analysis completed; nothing exceeded --fail-over (or flag not passed). | | 1 | Analysis error (bad path, test/coverage failure, parse error). With --json a structured error envelope goes to stderr. | | 2 | Quality gate tripped: at least one method's wCRAP exceeds --fail-over. | Stream discipline: the report (text or JSON) goes to stdout; progress markers ("slopguard: running vitest with coverage…") go to stderr. Piped stdout is always clean. ## How coverage works Coverage is an artifact of the analysis, not an input, mirroring how slopguard-swift drives `xcodebuild test` itself. slopguard-ts drives the project's own test runner: 1. Project discovery. Walk up from --path to the nearest package.json (override with --project-dir). 2. Runner detection. Look for vitest or jest signals: a config file (vitest.config.* / jest.config.*), the dependency itself, a `jest` key in package.json, or the runner named in the `test` script. Exactly one match wins; both → runner_ambiguous (pass --runner); neither → runner_not_detected (pass --runner, --coverage-file, or --no-coverage). 3. Test run. Spawn the project-local binary (node_modules/.bin/) with flags that force an istanbul coverage-final.json into a slopguard-owned temp directory, e.g. `vitest run --coverage.enabled=true --coverage.reporter=json --coverage.reportsDirectory=…` or `jest --coverage --coverageReporters=json --coverageDirectory=…`. The project's own coverage config is untouched. Failing tests don't abort; partial coverage is still useful (a note is attached). A broken run with no coverage output aborts with the stderr tail. 4. Join. Parse the istanbul map into a line index, join per-method line coverage onto the parsed declarations (basename + longest-suffix fallback for CI-vs-local path mismatches), then delete the temp dir. The istanbul coverage-final.json is the universal interchange format (jest, vitest, nyc, and c8 all emit it), so any runner slopguard can't drive directly is still supported via --coverage-file. ## What counts as a method Functions, class methods, constructors, get/set accessors, static blocks, and named arrow functions / function expressions (`const handler = () => {}`, object properties, class fields). Anonymous inline callbacks don't get their own entry; their branches count toward the enclosing method, with a cognitive nesting bump for the callback body, per the Sonar spec. Default excludes: node_modules, dist, build, out, coverage, *.d.ts, *.min.js, generated code, and test files (*.test.*, *.spec.*, __tests__/, test(s)/). Analyze test code itself with --no-default-excludes. ## JSON report schema (schemaVersion "2", shared with slopguard-swift) Top level: schemaVersion, generatedAt, coverageAvailable, summary, methods[], types[]. summary: - files, types, methods counts + crappy counts - avgCrap, maxCrap, average cyclomatic/cognitive/weighted complexity - weighted coverage across analyzed methods (when gathered) - coverageAvailable: false under --no-coverage or when no coverage data was found methods[], every analyzed function/method/constructor/accessor/named-arrow: - id: STABLE identifier "file#qualifiedName@line". Track methods across runs with this. - qualifiedName: "Type.method(signature)" - file, line, endLine: location relative to analyzed root - kind: function | method | constructor | getter | setter | … - typeName: enclosing type - complexity: cyclomatic (McCabe) - cognitiveComplexity: SonarSource 2023 - weightedComplexity: sqrt(cyc × cog) - coverage: line coverage percent 0–100 - crap: the wCRAP score - isCrappy: true when crap > threshold types[], per-type aggregation (class, interface, enum, namespace, method-bearing object literal): - aggregatedCrap: formula applied to the type's totals (total burden, comparable across types) - maxCrap: worst single-method offender ("biggest fire") - isCrappy, weighted coverage, method counts Example (real output from slopguard-ts analyzing its own sources): { "qualifiedName": "CrapAggregator.aggregate", "file": "core/aggregation/crapAggregator.ts", "line": 48, "kind": "method", "complexity": 20, "cognitiveComplexity": 16, "weightedComplexity": 17.89, "coverage": 0, "crap": 337.89, "isCrappy": true, "id": "core/aggregation/crapAggregator.ts#CrapAggregator.aggregate@48" } ## Quickstart commands # Zero-config: analyze the current directory (detects vitest/jest, runs it for coverage) slopguard-ts # Scan a specific directory and print the top crappy methods slopguard-ts analyze --path src --threshold 30 # Skip runner auto-detection: name the framework to drive slopguard-ts analyze --path src --runner vitest slopguard-ts analyze --path src --runner jest # Monorepo: run the tests of a specific package slopguard-ts analyze --path packages/core/src --project-dir packages/core # Full JSON for CI / downstream tooling slopguard-ts analyze --path src --json | jq '.methods | sort_by(-.crap)[:10]' # Fail CI when any method's CRAP exceeds 50 slopguard-ts analyze --path src --fail-over 50 # Complexity only (skip the test run) slopguard-ts analyze --path src --no-coverage # Join coverage CI already produced (nyc, c8, mocha, or a prior run) slopguard-ts analyze --path src --coverage-file coverage/coverage-final.json ## Agent integration ### CLAUDE.md / AGENTS.md drop-in ## Quality gate: SlopGuard Before declaring any TypeScript task complete, run: slopguard-ts analyze --path src --json --quiet - The report is JSON on stdout; `.methods[]` entries with `isCrappy: true` exceed the wCRAP threshold (30). - If your change ADDED a crappy method, refactor it or cover it with tests until its `crap` score is below 30. Coverage is gathered by the tool itself by driving your test runner (vitest/jest). Write real tests; the number cannot be hand-fed. - CI runs `--fail-over 50` and will reject the PR at exit code 2. - Full interface reference: https://www.slopguard.dev/llms-full.ts.txt ### Recipes # Agent work queue: worst methods first, with stable IDs slopguard-ts analyze --json --quiet \ | jq '[.methods[] | select(.isCrappy)] | sort_by(-.crap) | map({id, crap, coverage, file, line})' # Coverage gaps: complex code no test touches slopguard-ts analyze --json --quiet \ | jq '.methods[] | select(.complexity >= 5 and .coverage <= 50)' # Regression check around an agent change before=$(slopguard-ts analyze --json --quiet | jq '.summary.maxCrap') # … agent edits code … after=$(slopguard-ts analyze --json --quiet | jq '.summary.maxCrap') # Fast pre-commit sweep, no test run (use a high fail-over: coverage reads 0%) slopguard-ts analyze --no-coverage --quiet --fail-over 200 ### CI gate (GitHub Actions) name: slop-gate on: [pull_request] jobs: slopguard: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: 20 } - run: npm ci - name: Build slopguard-ts run: | git clone --depth 1 https://github.com/JeevanThandi/slopguard-typescript.git /tmp/sg cd /tmp/sg && npm install && npm run build && npm link - name: Gate on wCRAP run: slopguard-ts analyze --path src --fail-over 50 Threshold guidance: flag at 30 (classic CRAP paper threshold); hard-fail CI at 50. Under --no-coverage all methods read 0% coverage, so use a much higher fail-over (e.g. 200) for complexity-only sweeps. ## Security / supply-chain posture - Two top-level runtime dependencies: typescript (Apache-2.0, the official parser) and commander (MIT). - The only subprocess slopguard-ts spawns is the project's own test runner, from the project's own node_modules/.bin. - No network, no telemetry, no source mutation. - MIT licensed. - Dogfooded: CI keeps the suite green on Node 18.17/20/22, enforces 100% coverage, and asserts the analyzer stays under its own threshold against its own sources and the sample-apps/ fixtures. ## Roadmap - v0.1: CLI, full core + coverage (vitest/jest), istanbul interchange. (current) - v0.2: node --test + bun runners, SARIF output for GitHub code scanning. - v0.3: Per-PR diff mode (slopguard-ts diff origin/main…HEAD).