{"_id":"@alperlabs/crap4ts","_rev":"2-2b67e5f36084b636e18c1bd1423541ee","name":"@alperlabs/crap4ts","dist-tags":{"latest":"0.2.1"},"versions":{"0.2.0":{"name":"@alperlabs/crap4ts","version":"0.2.0","keywords":["crap","crap4j","cyclomatic-complexity","coverage","typescript","code-quality","ai-slop"],"author":{"name":"alperlabs"},"license":"MIT","_id":"@alperlabs/crap4ts@0.2.0","maintainers":[{"name":"alperlabsdev","email":"dev@alperlabs.com"}],"homepage":"https://github.com/alperlabs/crap4ts#readme","bugs":{"url":"https://github.com/alperlabs/crap4ts/issues"},"bin":{"crap4ts":"dist/main.js"},"dist":{"shasum":"d773d9b0d17d30199a6a1dd63d043ece67bda886","tarball":"https://registry.npmjs.org/@alperlabs/crap4ts/-/crap4ts-0.2.0.tgz","fileCount":201,"integrity":"sha512-AYMm5RUun7wns4OQlaB9awHsJEALAGWYadMPAbh2Q8pRPGguRIxUjRwhEPk5Cxn0h+nFmODyZuGkvXgSwcCZXQ==","signatures":[{"sig":"MEUCIQCw+hKh5wkP8lRTf4C5Fe28Vv9V7qFOehf3CpbLLgET6QIgWwthj1gNan4Mf7gf6OrXZbcdQLMX45POpilbplAQCF0=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@alperlabs%2fcrap4ts@0.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":233793},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./package.json":"./package.json"},"gitHead":"68667eba0e156764f51a183f7f5fc4dbeddaef9a","scripts":{"crap":"tsx src/main.ts","lint":"eslint .","test":"vitest run --coverage","build":"tsc -p tsconfig.json","start":"node dist/main.js","format":"prettier --write .","typecheck":"tsc -p tsconfig.json --noEmit","test:watch":"vitest","format:check":"prettier --check .","prepublishOnly":"npm run typecheck && npm run lint && npm test && npm run build"},"_npmUser":{"name":"alperlabsdev","email":"dev@alperlabs.com"},"repository":{"url":"git+https://github.com/alperlabs/crap4ts.git","type":"git"},"_npmVersion":"10.9.8","description":"A CRAP metric analyzer for TypeScript projects, with extra AI-slop heuristics. Modeled after crap4java.","directories":{},"_nodeVersion":"22.23.1","dependencies":{"typescript":"^5.6.3"},"publishConfig":{"access":"public","provenance":true},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.19.1","eslint":"^9.13.0","vitest":"^2.1.4","prettier":"^3.3.3","@eslint/js":"^9.13.0","@types/node":"^22.7.5","typescript-eslint":"^8.11.0","eslint-config-prettier":"^9.1.0","@vitest/coverage-istanbul":"^2.1.4"},"_npmOperationalInternal":{"tmp":"tmp/crap4ts_0.2.0_1785451809301_0.3751037001256645","host":"s3://npm-registry-packages-npm-production"}},"0.2.1":{"name":"@alperlabs/crap4ts","version":"0.2.1","description":"A CRAP metric analyzer for TypeScript projects, with extra AI-slop heuristics. Modeled after crap4java.","type":"module","bin":{"crap4ts":"dist/main.js"},"main":"dist/index.js","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./package.json":"./package.json"},"publishConfig":{"access":"public","provenance":true},"engines":{"node":">=18"},"scripts":{"build":"tsc -p tsconfig.json","prepublishOnly":"npm run typecheck && npm run lint && npm test && npm run build","start":"node dist/main.js","crap":"tsx src/main.ts","test":"vitest run --coverage","test:watch":"vitest","typecheck":"tsc -p tsconfig.json --noEmit","lint":"eslint .","format":"prettier --write .","format:check":"prettier --check ."},"repository":{"type":"git","url":"git+https://github.com/alperlabs/crap4ts.git"},"bugs":{"url":"https://github.com/alperlabs/crap4ts/issues"},"homepage":"https://github.com/alperlabs/crap4ts#readme","author":{"name":"alperlabs"},"keywords":["crap","crap4j","cyclomatic-complexity","coverage","typescript","code-quality","ai-slop"],"license":"MIT","dependencies":{"typescript":"^5.6.3"},"devDependencies":{"@eslint/js":"^9.13.0","@types/node":"^22.7.5","@vitest/coverage-istanbul":"^2.1.4","eslint":"^9.13.0","eslint-config-prettier":"^9.1.0","prettier":"^3.3.3","tsx":"^4.19.1","typescript-eslint":"^8.11.0","vitest":"^2.1.4"},"_id":"@alperlabs/crap4ts@0.2.1","gitHead":"b2026f9f4c3ec29851a381cc852bf1e5d438b0a6","_nodeVersion":"22.23.1","_npmVersion":"10.9.8","dist":{"integrity":"sha512-vfiSaGSWW5pQDcQHMrxV0KgH+CPGApaubrowIOyYK4zQRrcrZakoN9ANPavKkuR+N6EQzOJygs7ZxlHBxFCMWQ==","shasum":"849c87a9e9eb4ac56e4b9610eff19d26a9a5d690","tarball":"https://registry.npmjs.org/@alperlabs/crap4ts/-/crap4ts-0.2.1.tgz","fileCount":201,"unpackedSize":233793,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@alperlabs%2fcrap4ts@0.2.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDexuio3sJLauDY2HX3HT1K0bJugKo1rzX+xDyJGsQg/gIhAO4FDCAl0yrcay/K+e8qDGQYn3dyWpoQuPGIhzg8YV/i"}]},"_npmUser":{"name":"alperlabsdev","email":"dev@alperlabs.com"},"directories":{},"maintainers":[{"name":"alperlabsdev","email":"dev@alperlabs.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/crap4ts_0.2.1_1785453223406_0.11383234220553096"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-30T22:50:09.165Z","modified":"2026-07-30T23:13:44.045Z","0.2.0":"2026-07-30T22:50:09.456Z","0.2.1":"2026-07-30T23:13:43.631Z"},"bugs":{"url":"https://github.com/alperlabs/crap4ts/issues"},"author":{"name":"alperlabs"},"license":"MIT","homepage":"https://github.com/alperlabs/crap4ts#readme","keywords":["crap","crap4j","cyclomatic-complexity","coverage","typescript","code-quality","ai-slop"],"repository":{"type":"git","url":"git+https://github.com/alperlabs/crap4ts.git"},"description":"A CRAP metric analyzer for TypeScript projects, with extra AI-slop heuristics. Modeled after crap4java.","maintainers":[{"name":"alperlabsdev","email":"dev@alperlabs.com"}],"readme":"# crap4ts\n\n[![CI](https://github.com/alperlabs/crap4ts/actions/workflows/ci.yml/badge.svg)](https://github.com/alperlabs/crap4ts/actions/workflows/ci.yml)\n\n`crap4ts` is a standalone CRAP metric tool for TypeScript projects, modeled\nafter [`crap4java`](https://github.com/unclebob/crap4java).\n\nIt combines method cyclomatic complexity with per-method statement coverage and\nreports CRAP scores. On top of the classic CRAP metric it also measures a set of\n**AI-slop heuristics** — the syntactic tics (guard clauses, `instanceof`\nladders, `any` escape hatches, `!` assertions, `?.`/`??` soup, stray\n`console.log`s) that make generated TypeScript look like AI crap — and rolls\nthem into a per-method **slop score**.\n\nOn each run it deletes stale coverage artifacts, runs coverage, then analyzes\nthe selected files.\n\n## Formula\n\n`CRAP = CC^2 * (1 - coverage)^3 + CC`\n\n- `CC` is cyclomatic complexity.\n- `coverage` is the method's statement coverage fraction (covered / total\n  statements whose starting line falls inside the method), derived from an\n  Istanbul `coverage-final.json`.\n\n> The lowest possible CRAP score is **1.0** — a method with complexity 1 at 100%\n> coverage scores `1² · 0³ + 1 = 1`. There is no such thing as CRAP 0.\n\n## Reading CRAP scores\n\nCRAP means **Change Risk Anti-Patterns**: how risky it is to change a method,\nnot how ugly it looks. High CRAP usually means “complex **and** under-tested.”\nA clean method with no tests can outrank a messy one that is fully covered.\n\n### Example: clean use case, CRAP 20\n\nThis NestJS-style use case is short, linear, and follows clean architecture —\nyet it scored **CRAP 20** on a real project run:\n\n```ts\nasync execute(command: AdminUpdateOrganizationCommand): Promise<Organization> {\n  const organization = await this.organizationRepository.findById(\n    command.organizationId,\n  );\n  if (!organization) {\n    throw new OrganizationNotFoundException(command.organizationId);\n  }\n\n  if (command.categorySlug) {\n    const exists = await this.categoryRepository.existsBySlug(\n      command.categorySlug,\n    );\n    if (!exists) {\n      throw new OrganizationCategoryNotFoundException(command.categorySlug);\n    }\n  }\n\n  const updated = organization.updateProfile({ /* fields from command */ });\n  await this.organizationRepository.save(updated);\n  return updated;\n}\n```\n\n| Input    |  Value | Why                                              |\n| -------- | -----: | ------------------------------------------------ |\n| CC       |      4 | base + three `if`s (not-found, optional slug, …) |\n| Coverage |     0% | no use-case spec; callers mock this class        |\n| **CRAP** | **20** | `4² · (1 − 0)³ + 4 = 20`                         |\n\nAt **100%** coverage the same method would score **CRAP 4.0** — well under the\ndefault gate of 8 (tune it with `--threshold`). The fix is a focused unit test\nfor `execute`, not a rewrite.\n\n### Example: dense domain update, CRAP 90\n\nSame project, same 0% coverage — but here the method itself is hard to change.\nOptional fields are merged through nested ternaries and `||` fallbacks:\n\n```ts\nupdateDetails(params: {\n  displayName?: string;\n  phone?: string | null;\n  locale?: string;\n  timezone?: string;\n  avatarUrl?: string | null;\n}): UserProfile {\n  const displayName =\n    params.displayName !== undefined\n      ? params.displayName.trim() || this.displayNameValue\n      : this.displayNameValue;\n  return new UserProfile(\n    this.id,\n    this.accountIdValue,\n    displayName,\n    params.avatarUrl === undefined\n      ? this.avatarUrlValue\n      : params.avatarUrl?.trim() || undefined,\n    params.phone === undefined\n      ? this.phoneValue\n      : params.phone?.trim() || undefined,\n    params.locale?.trim() || this.localeValue,\n    params.timezone?.trim() || this.timezoneValue,\n  );\n}\n```\n\n| Input    |  Value | Why                                   |\n| -------- | -----: | ------------------------------------- |\n| CC       |      9 | ternaries + `                         |     | ` short-circuits on every optional field |\n| Coverage |     0% | no direct unit coverage for this path |\n| **CRAP** | **90** | `9² · (1 − 0)³ + 9 = 90`              |\n\nEven at **100%** coverage this would still score **CRAP 9.0** (above the gate)\nuntil the branching is simplified — e.g. small per-field helpers or an explicit\npatch/merge step. Tests alone are not enough; the structure has to get simpler.\n\n### How to read the two rows\n\n| Signal           | Clean use case (CRAP 20) | Dense update (CRAP 90)   |\n| ---------------- | ------------------------ | ------------------------ |\n| Structure        | fine                     | tangled optional merging |\n| Coverage         | 0%                       | 0%                       |\n| Fix              | add a use-case spec      | simplify **and** test    |\n| At 100% coverage | CRAP 4 (passes)          | CRAP 9 (still fails)     |\n\nUse **CRAP** for change risk, and the **slop** breakdown when you care about\nAI-shaped syntax. A high-CRAP / low-complexity row is usually testing debt;\nhigh-CRAP / high-complexity is where design review belongs.\n\n## AI-slop metrics\n\nEach method is also scanned for the following smells. They are counted per\nmethod and rolled up into a weighted **slop score** (weight in parentheses).\nSmells come in two categories, and the report groups them accordingly.\n\n**Escape hatches** — strong signals; they defeat the type system or error\nhandling outright:\n\n| Smell        | What it counts                                                          | Weight |\n| ------------ | ----------------------------------------------------------------------- | ------ |\n| `suppress`   | `@ts-ignore`/`@ts-expect-error`/`@ts-nocheck`/`eslint-disable` comments | 4      |\n| `any`        | `any` type annotations (`: any`, `as any`, `Array<any>`, ...)           | 3      |\n| `mute-catch` | empty `catch` blocks that swallow errors                                | 3      |\n| `nonNull`    | non-null assertions (`x!`)                                              | 2      |\n| `loose-eq`   | loose equality (`==`, `!=`), excluding the `x == null` idiom            | 2      |\n| `var`        | function-scoped `var` declarations                                      | 2      |\n| `as`         | type assertions (`x as T`, `<T>x`), excluding `as const`                | 1      |\n\n**Style heuristics** — soft signals; each is idiomatic on its own and only\nsuspicious in aggregate:\n\n| Smell       | What it counts                                                               | Weight |\n| ----------- | ---------------------------------------------------------------------------- | ------ |\n| `guard`     | consecutive `if (...) return/throw` guard clauses (ladders of N score N−1)   | 2      |\n| `console`   | `console.*` calls                                                            | 2      |\n| `instof`    | `x instanceof Foo` expressions                                               | 1      |\n| `typeof`    | `typeof x` value-position checks                                             | 1      |\n| `?.`        | optional-chaining hops                                                       | 1      |\n| `??`        | nullish-coalescing operators                                                 | 1      |\n| `try`       | try/catch statements                                                         | 1      |\n| `obj-null`  | `typeof x === \"object\" && x !== null` JSON-guard bodies                      | 1      |\n| `is-helper` | `isRecord` / `isPlainObject` / `isObject` / `isString` / … defs & bare calls | 1      |\n| `dup-guard` | same type-guard helper name defined in two or more files                     | 1      |\n\nNone of these are bugs on their own; in aggregate they are a good smell for\nunreviewed, machine-generated code. The slop score does **not** affect the exit\ncode — only the CRAP threshold does. Adding your own heuristic is a one-file\nchange; see [CONTRIBUTING.md](CONTRIBUTING.md).\n\nThe report ends with a **findings section** locating every occurrence, so you\ncan jump straight to the smell and fix it:\n\n```text\nFindings\n========\nsrc/user-service.ts:42    any         Replace any with unknown, a generic, or the real type.\nsrc/user-service.ts:57    mute-catch  Never swallow errors: handle, log, or rethrow.\n```\n\n## Coverage Pipeline\n\nFor each module (directory owning a `package.json`), the tool:\n\n1. Deletes the stale `coverage/` directory.\n2. Runs the coverage command (default `npm test`, configurable via\n   `--coverage-command`).\n3. Reads the coverage report through a **coverage reader** — Istanbul\n   `coverage-final.json` and LCOV `lcov.info` ship built in, probed in that\n   order. `--coverage-format` forces one explicitly.\n4. Analyzes the selected TypeScript files in that module.\n\nExample coverage commands:\n\n- **Vitest**: `vitest run --coverage` with `coverage.provider = \"istanbul\"` and\n  the `json` reporter (or the lcov reporter with any provider).\n- **Jest**: `jest --coverage --coverageReporters=json` (or `lcov`).\n- **c8 / node:test**: `c8 --reporter=lcov node --test`.\n\nAlready ran coverage in an earlier CI step? Skip the re-run with\n`--coverage-file coverage/coverage-final.json`. Analyzing a repo you can't\nrun (or don't want to)? `--no-coverage` scores smells and complexity only,\nwith coverage and CRAP reported as N/A.\n\nCoverage reading is an interface (`CoverageReader`): implementing one file\nadds a format, and library consumers can pass their own readers without\nforking. See [CONTRIBUTING.md](CONTRIBUTING.md).\n\n## Install\n\n```bash\n# in a project\nnpm install --save-dev @alperlabs/crap4ts\n\n# or run without installing\nnpx @alperlabs/crap4ts\n```\n\nBoth put a `crap4ts` binary on your path. From a checkout: `npm install &&\nnpm run build`, then `node dist/main.js`.\n\n## CLI\n\n```text\nUsage: crap4ts [selection] [options]\n\nSelection (mutually exclusive):\n  (no args)                    Analyze all TypeScript files under the source roots\n  --changed                    Analyze files with uncommitted changes (git status)\n  --changed-since <ref>        Analyze files changed since merge-base with <ref>\n                               (committed and uncommitted), e.g. origin/main\n  <path...>                    Analyze these files; directory arguments are\n                               searched under their own source roots\n\nOptions:\n  --threshold <score>          Maximum allowed CRAP score (default: 8.0)\n  --format <name>              Report format: text, json, or github (default: text)\n  --no-coverage                Skip coverage; coverage and CRAP report as N/A\n  --coverage-file <path>       Read an existing coverage report instead of\n                               running the coverage command\n  --coverage-command <command> Command that generates coverage (default: npm test)\n  --coverage-format <name>     Coverage report format: istanbul or lcov\n                               (default: detect from the report file name)\n  --source-root <dir>          Source directory to search; repeatable\n                               (default: src)\n  --baseline <path>            Fail only on methods that are new or worse than\n                               this baseline file\n  --write-baseline             Write the baseline file from this run and exit 0\n  --config <path>              Config file (default: crap4ts.config.json, then\n                               the \"crap4ts\" key in package.json)\n  --version                    Print the crap4ts version\n  --help                       Print this help message\n```\n\nExamples:\n\n```bash\ncrap4ts                                   # gate the whole project\ncrap4ts --changed-since origin/main       # PR-scoped: only what the diff touched\ncrap4ts --format json > crap.json         # machine-readable report\ncrap4ts --coverage-file coverage/lcov.info --threshold 10\ncrap4ts src/foo/Sample.ts packages/a packages/b\n```\n\n## Configuration\n\nEvery flag has a config-file equivalent. Settings load from\n`crap4ts.config.json` at the project root (or a `crap4ts` key in\n`package.json`); CLI flags win over the file, the file wins over defaults:\n\n```json\n{\n  \"threshold\": 10,\n  \"format\": \"text\",\n  \"coverageCommand\": \"npx vitest run --coverage\",\n  \"sourceRoots\": [\"src\", \"lib\"],\n  \"baseline\": \"crap4ts-baseline.json\"\n}\n```\n\nOther keys: `coverage` (`\"run\"` | `\"file\"` | `\"off\"`), `coverageFile`,\n`coverageFormat`. Unknown keys and wrong types are errors, not silent\nfallbacks.\n\n## CI\n\n`--format github` prefixes the report with GitHub Actions annotations —\n`::error` per method over the threshold, `::warning` per smell finding — so\nresults land inline on the pull request diff:\n\n```yaml\n- run: npx @alperlabs/crap4ts --coverage-file coverage/coverage-final.json --format github\n```\n\n## Baseline (ratchet) mode\n\nReal codebases rarely pass a CRAP gate on day one. Record today's scores,\nthen fail only what is **new or worse**:\n\n```bash\ncrap4ts --write-baseline               # writes crap4ts-baseline.json\ncrap4ts --baseline crap4ts-baseline.json   # passes; existing debt is accepted\n```\n\nCommit the baseline. Methods over the threshold that are already recorded\npass until they get worse; new offenders and regressions fail. Re-run\n`--write-baseline` after paying debt down so the ratchet only tightens.\n\n## Library use\n\nThe npm package exposes everything the CLI does:\n\n```ts\nimport {\n  analyze,\n  istanbulJsonReader,\n  rendererNamed,\n  buildBaseline,\n  type CoverageReader,\n} from \"@alperlabs/crap4ts\";\n\nconst coverage = istanbulJsonReader.read(\"coverage/coverage-final.json\");\nconst metrics = analyze([\"src/service.ts\"], coverage);\nconsole.log(rendererNamed(\"json\").render(metrics, { projectRoot: process.cwd(), threshold: 8 }));\n```\n\nCustom coverage formats implement the `CoverageReader` interface and can be\npassed to `new CliApplication({ coverageReaders: [...] })` alongside the\nbuilt-ins.\n\n## Exit codes\n\n- `0` success, gate respected (also: empty selection, `--help`, `--version`,\n  `--write-baseline`)\n- `1` invalid CLI usage, broken config, or missing baseline\n- `2` CRAP gate failed (over the threshold, or worse than the baseline)\n\n## Architecture\n\n```\nsrc/\n  analysis/\n    complexity/   decision-rule registry + cyclomatic complexity counter\n    smells/       smell-detector registry (one file per heuristic)\n    parsing/      TypeScript AST → declared methods (extractor registry)\n    crap-score.ts  the CRAP formula\n    crap-analyzer.ts\n  coverage/       coverage runner + pluggable report readers (istanbul, lcov)\n  discovery/      source-file finder, changed-file detector, module resolver\n  report/         report renderers: text, json, github\n  baseline/       ratchet-mode baseline build/read/compare\n  config/         defaults, config file, CLI/file/default merging\n  cli/            argument parsing + the application orchestrator\n  index.ts        the public library API\n```\n\nSmells, complexity decision points, and method extraction are each expressed as\na **registry of small, single-purpose units** rather than a growing `switch`, so\nthe code stays flat and extending it means adding a list entry. A single\ntraversal contract (`parsing/method-traversal.ts`) defines which nodes belong to\na method, so the complexity and smell counters always agree.\n\n## Development\n\n```bash\nnpm test          # vitest + 100% coverage gate\nnpm run lint      # eslint (typescript-eslint)\nnpm run format    # prettier --write\nnpm run typecheck # tsc --noEmit\nnpm run crap      # run crap4ts on itself (must exit 0)\n```\n\n`crap4ts` is self-hosting and holds itself to its own bar: 100% test coverage\nand a max self-CRAP well under the threshold, both enforced in CI. See\n[CONTRIBUTING.md](CONTRIBUTING.md) to add a smell detector, complexity rule, or\ndeclaration shape.\n\n## Roadmap\n\n- Configurable smell weights.\n- SARIF output for GitHub code scanning.\n- A published reference corpus for density comparisons.\n\nHave an idea for a new heuristic? Open a\n[smell proposal](.github/ISSUE_TEMPLATE/smell-proposal.md) — a detector is a\none-file contribution.\n\n## Notes\n\n- If no coverage report is found (or `--no-coverage` is set), coverage is\n  reported as `N/A` (and CRAP `N/A`); the run still succeeds.\n- The report is sorted by CRAP descending, with `N/A` rows at the bottom.\n- `.d.ts` declaration files are ignored.\n- Constructors, overload/`abstract`/`declare` signatures (no body), and\n  anonymous inline callbacks are not reported as their own rows; a named arrow\n  or function expression bound to a variable/property **is**.\n\n## License\n\n[MIT](LICENSE)\n","readmeFilename":"README.md"}