{"slug": "show-hn-badbox-a-deterministic-bad-pattern-detector-for-codebases", "title": "Show HN: Badbox – A deterministic bad-pattern detector for codebases", "summary": "Badbox, a deterministic bad-pattern detector for codebases, has reached npm version 0.3.0, letting repositories encode questionable engineering patterns as structural rules and check for them without an LLM. The tool ships no default policies and performs no automatic rewriting, requiring each project to own its rules under `.badbox/` and run on Bun 1.3.14 or newer. Badbox reports evidence on where a pattern occurred, who owns it, and how often, while leaving the decision to a developer or coding agent.", "body_md": "Deterministic bad-pattern detection for codebases.\n\nBadbox lets a repository encode questionable engineering patterns as structural rules, then check for them without an LLM. It reports evidence—where a pattern occurred, who owns it, and how often— while leaving the decision to a developer or coding agent.\n\nBadbox ships no default policies and performs no automatic rewriting. Each project owns its rules\nunder `.badbox/`.\n\nCurrent npm version: `0.3.0`\n\nCurrent rule format: `#badbox 1` (experimental)\n\nBadbox requires [Bun](https://bun.sh/) 1.3.14 or newer.\n\nInstall it in a project:\n\n```\nbun add --dev badbox\n```\n\nThen run it through `bunx`:\n\n```\nbunx badbox check\n```\n\nYou can also install the CLI globally:\n\n```\nbun add --global badbox\nbadbox check\n```\n\nPublished packages include prebuilt native engines for these platforms:\n\n| Platform | Architecture | Native package | \n|---|---|---|\n| macOS | Apple Silicon | `badbox-darwin-arm64` | \n| macOS | Intel | `badbox-darwin-x64` | \n| Linux GNU | arm64 | `badbox-linux-arm64-gnu` | \n| Linux GNU | x64 | `badbox-linux-x64-gnu` | \n| Windows | x64 | `badbox-windows-x64` | \n\nInstalling from npm on a supported platform does not require a local Rust toolchain.\n\nRun `create` from the root of the project you want to check:\n\n```\nbunx badbox create project-rules.badbox\n```\n\nBadbox creates `.badbox/project-rules.badbox` with the correct version header and an editable\nexample. Replace that example with a rule for your project:\n\n```\n#badbox 1\n\nrule rust/excessive-clones for rust {\n  summary \"Function contains more clone calls than the configured limit\"\n  param limit = 4\n\n  find code(value) `value.clone()`\n  group by nearest callable\n  when count > limit\n\n  report {\n    severity warning\n    message \"Function contains excessive clone calls\"\n    evidence \"clone call sites\"\n  }\n}\n```\n\nCheck the project:\n\n```\nbunx badbox check\n```\n\nOr check specific source roots while still loading rules from the project's `.badbox/` directory:\n\n```\nbunx badbox check src packages\n```\n\nA finding is an observation, so findings do not make the command fail. Invalid rules, invalid input, unreadable files, I/O errors, and syntax diagnostics make the check incomplete and return a nonzero exit code.\n\nA rule selects syntax, assigns each match to an owner, applies structural conditions, counts the remaining matches, and reports owners above a threshold.\n\n``` php\nfind -> where -> nearest owner -> distinct-range count -> threshold -> finding\n```\n\nThis makes rules useful for patterns such as:\n\n- excessive cloning, casting, unwrapping, or goroutine creation per function;\n- syntax inside a loop or another structural boundary;\n- an owner that contains or lacks supporting evidence;\n- one statement following or preceding another in the same lexical block;\n- repository-specific architectural patterns that should not reappear.\n\nBadbox detects which supported languages occur under the requested source roots and runs only the relevant rules.\n\nThe tiny DSL is the primary rule format. Every file starts with a version header:\n\n```\n#badbox 1\n```\n\nNames declared in `code(...)` become structural captures:\n\n```\nfind code(value) `value.clone()`\n```\n\nUndeclared identifiers remain literal source syntax. Captures can also be constrained by text:\n\n```\nwhere text(method) in [\"unwrap\", \"expect\"]\n```\n\nSupported text operators are `==`, `!=`, `in`, `not in`, and `matches`.\n\nRules can inspect containment and evidence around a selected match:\n\n```\nwhere match inside any {\n  node for_statement\n  node while_statement\n}\n\nwhere group lacks any {\n  code(ctx) `ctx.cancel()`\n}\n```\n\nOrdering relations compare statements in the same nearest callable and lexical block:\n\n```\nfind code(statement) `statement.clearBindings()`\ngroup by nearest callable\n\nwhere match follows any {\n  code(statement) `statement.reset()`\n}\n```\n\nRepeating `statement` in both selectors requires the captured source text to be equal. Therefore,\n`first.reset()` does not satisfy a later `second.clearBindings()` match. Intervening sibling\nstatements are allowed; nested callables and different branch, loop, switch, or error-handling\nblocks remain separate.\n\nSee the [tiny DSL reference](https://github.com/0ctacity/badbox/blob/main/native/tiny-dsl/README.md) for the complete grammar, validation rules,\nrelations, parameters, and fixture declarations. Runnable DSL examples live under\n[`examples/rules`](https://github.com/0ctacity/badbox/blob/main/examples/rules). YAML equivalents under [`examples/yaml`](https://github.com/0ctacity/badbox/blob/main/examples/yaml) exercise\nthe compatibility frontend; they are never loaded automatically.\n\n| Area | Supported today | \n|---|---|\n| Selection | Source-shaped `code(...)` patterns and raw syntax-node kinds | \n| Ownership | Nearest callable for Rust, Go, PowerShell, and Zig; explicit nearest node kinds elsewhere | \n| Capture predicates | `==` ,`!=` ,`in` ,`not in` , and Rust-regex`matches` | \n| Containment | `where match inside any\\|all` | \n| Owner evidence | `where group has any\\|all` and`where group lacks any\\|all` | \n| Ordering | Statement-level `follows` and`precedes` within one callable and lexical block | \n| Aggregation | Distinct selected ranges counted per owner with strict `count > threshold` | \n| Parameters | Rule-local scalar defaults with programmatic overrides | \n| Frontends | Tiny DSL plus YAML compatibility, both compiled to the same Badbox Rule IR | \n| Output | Deterministically ordered, bounded findings with exact total counts | \n\nBadbox bundles 30 parsers.\n\nThe 28 ast-grep built-in languages are Bash, C, C++, C#, CSS, Dart, Elixir, Go, Haskell, HCL, HTML, Java, JavaScript/JSX, JSON, Kotlin, Lua, Markdown, Nix, PHP, Python, Ruby, Rust, Scala, Solidity, Swift, TSX, TypeScript, and YAML.\n\nBadbox also statically links PowerShell and Zig parsers.\n\nFile extensions select relevant language rules. This is language detection, not framework, dependency, build-configuration, or semantic type detection.\n\n```\nbadbox create <name.badbox>\nbadbox check [path ...]\n```\n\nCreates `.badbox/<name.badbox>` with the current version header and a Rust example. The name may\ninclude subdirectories under `.badbox/`. Badbox refuses absolute paths, parent traversal, names\nwithout the `.badbox` suffix, and existing files.\n\nRecursively loads `.badbox`, `.yaml`, and `.yml` rule files from the current project's `.badbox/`\ndirectory. With no source paths, it checks the current project. Explicit source paths narrow source\ndiscovery but do not change where rules are loaded from.\n\nDiscovery respects ignore files and hidden paths, skips common build and vendor directories, and does not follow nested symlinks.\n\nUse `badbox/checker` when another tool or coding agent needs structured results:\n\n``` js\nimport { inspect, iterateFindings } from \"badbox/checker\";\n\nconst result = await inspect({\n  paths: [\"./src\"],\n  rulePaths: [\"./.badbox\"],\n  parameters: {\n    \"rust/excessive-clones.limit\": 6,\n  },\n  maxFindings: 1_000,\n});\n\nfor (const finding of iterateFindings(result)) {\n  console.log({\n    rule: finding.rule.id,\n    file: finding.file,\n    owner: [finding.ownerStart, finding.ownerEnd],\n    observed: finding.observed,\n  });\n}\n\nif (result.diagnostics.length > 0) {\n  console.error(result.diagnostics);\n}\n```\n\n`inspect()` also accepts:\n\n- `threshold` to replace every loaded rule's threshold;\n- `profile: true` to include phase timings and cache/execution counters;\n- `maxFindings` to bound returned records while retaining the exact`findingCount` .\n\nFindings are stored as five `u32` values: file ID, rule ID, owner start, owner end, and observed\ncount. File paths and rule metadata are interned separately. Use `iterateFindings()` to decode the\nrecords without materializing another result array.\n\n```\nflowchart TD\n    CLI[\"CLI or TypeScript inspect()\"]\n    Native[\"Native N-API boundary\"]\n    Frontend[\"DSL or YAML frontend\"]\n    IR[\"Badbox Rule IR\"]\n    Plan[\"Per-language shared finder plan\"]\n    Parser[\"Tree-sitter parsing and bounded caches\"]\n    Match[\"ast-grep structural backend\"]\n    Eval[\"Ownership, relations, counting, thresholds\"]\n    Result[\"Compact findings and metadata\"]\n\n    CLI --> Native --> Frontend --> IR --> Plan --> Parser --> Match --> Eval --> Result\n```\n\nParsing, matching, ownership, relational evaluation, counting, thresholds, and compact result construction run in Rust. TypeScript invokes the native engine and decodes metadata.\n\nBadbox owns the Rule IR and structural-backend interface; ast-grep is an implementation detail. Identical primary and relational selectors are interned across a language plan and dispatched once per relevant AST node. Rule-specific predicates, ownership, thresholds, and reporting remain independent.\n\nFile work uses at most four native workers. Compiled-rule, parsed-file, and compact-result caches are content-validated, process-local, and bounded. Parallel results are sorted before returning so output remains deterministic.\n\n- Badbox reports lexical and structural evidence, not runtime behavior or developer intent.\n- There is no type resolution, macro expansion, conditional-compilation filtering, SQL query-plan analysis, leak proof, or transaction-atomicity proof.\n- Counts describe syntax sites, not runtime execution counts or costs.\n- Immediate-sibling ordering is not implemented. `follows` and`precedes` allow intervening sibling\nstatements.\n- Ordering by nearest callable currently targets Rust, Go, PowerShell, and Zig.\n- Only strict greater-than count aggregation is implemented.\n- Syntax-error files produce diagnostics and no partial findings.\n- Caches do not survive a CLI process and still read file contents for validation.\n- Findings are bounded to 10,000 records by default. Increasing `maxFindings` adds 20 bytes per\nreturned finding.\n- Rule format version `1` remains experimental.\n\nBuilding from source requires Bun, Rust/Cargo, `cargo-nextest`, and a C compiler for Tree-sitter\ngrammars.\n\n```\nbun install\nbun run build:native\nbun run ci\n```\n\n`bun run ci` is the canonical full check. It runs Rust formatting, Clippy, both `cargo-nextest`\nsuites, the native release build, TypeScript checking, and all Bun tests.\n\nAfter changing Rust, rebuild the `.node` addon before running Bun tests:\n\n```\nbun run build:native\nbun test tests/scanner.test.ts\n```\n\nFor reproducible latency, rule-scaling, and memory measurements:\n\n```\nbun run build:native\nbun run benchmark\nbun run benchmark ../zova\n```\n\nSee the [benchmark methodology](https://github.com/0ctacity/badbox/blob/main/benchmarks/README.md),\n[optimization report](https://github.com/0ctacity/badbox/blob/main/benchmarks/OPTIMIZATION.md), and\n[frozen Zova corpus](https://github.com/0ctacity/badbox/blob/main/tests/fixtures/zova/README.md).\n\n| Path | Purpose | \n|---|---|\n| `src/cli.ts` | `check` and`create` commands | \n| `src/scanner/` | Public TypeScript API and native-addon loading | \n| `native/src/rule_ir.rs` | Backend-independent rule model | \n| `native/src/frontends/` | DSL/YAML lowering and validation | \n| `native/src/backend/` | Structural backend contract and ast-grep implementation | \n| `native/src/evaluator.rs` | Language-independent aggregation and findings | \n| `native/tiny-dsl/` | Rust parser crate, tests, and DSL reference | \n| `examples/` | Runnable rules that are never defaults | \n| `tests/` | CLI, integration, packaging, corpus, and benchmark-contract tests | \n| `benchmarks/` | Reproducible performance harness and reports | \n\nAll six npm packages and both Rust manifests must use the same release version. Validate that invariant before publishing:\n\n```\nbun run scripts/validate-release.ts 0.3.0\nbun run ci\n```\n\nAfter committing and pushing the version, run the **Release npm packages** GitHub Actions workflow\nwith that version. It builds and tests all five native targets, packages and smoke-tests the npm\ntarballs, publishes the platform packages first, and publishes `badbox` last.\n\nAll six npm packages use npm trusted publishing through GitHub OIDC; no `NPM_TOKEN` is required.\nStable versions publish under `latest`, while prerelease versions publish under `next`.\n\n[MIT](https://github.com/0ctacity/badbox/blob/main/LICENSE) © 2026 Octacity", "url": "https://wpnews.pro/news/show-hn-badbox-a-deterministic-bad-pattern-detector-for-codebases", "canonical_source": "https://github.com/0ctacity/badbox", "published_at": "2026-09-20 21:04:14+00:00", "updated_at": "2026-09-20 21:22:54.908195+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents"], "entities": ["Badbox", "Bun", "npm", "Rust"], "alternates": {"html": "https://wpnews.pro/news/show-hn-badbox-a-deterministic-bad-pattern-detector-for-codebases", "markdown": "https://wpnews.pro/news/show-hn-badbox-a-deterministic-bad-pattern-detector-for-codebases.md", "text": "https://wpnews.pro/news/show-hn-badbox-a-deterministic-bad-pattern-detector-for-codebases.txt", "jsonld": "https://wpnews.pro/news/show-hn-badbox-a-deterministic-bad-pattern-detector-for-codebases.jsonld"}}