{
  "markdown": "# zzop ( Zero Zone Of Pain )\n\n[![CI](https://github.com/eezz4/zzop/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/eezz4/zzop/actions/workflows/ci.yml)\n[![npm](https://img.shields.io/npm/v/@zzop/cli?logo=npm)](https://www.npmjs.com/package/@zzop/cli)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)\n\n**Your AI coding agent can't read your whole codebase. zzop reads it — and answers the same way every\ntime.**\n\nPoint zzop at one repository, or at your frontend and backend together, and it returns a single JSON\ndocument describing what is actually there: which frontend calls reach which backend routes and which\nreach nothing, what looks risky, what is dead, where to refactor first — and what this run could not\nsee. An agent starts from that instead of guessing from the handful of files it had room to open.\n\nzzop does not write code. It makes the *understanding* a code generator works from accurate and\nrepeatable — same commit in, byte-identical findings out — so what your agent writes rests on what your\ncode does rather than on what it inferred from a partial read. The thing being improved is\ncomprehension, not capability.\n\n## See it break something\n\n**[Break a route](docs/demo/break-a-route.md)** is the whole product in one change: rename\none backend route in a frontend/backend pair that share no code and no types. The frontend still\ncompiles, its tests still pass — and zzop names both ends of the break, file and line (abridged here;\nthe demo page shows the run's own format):\n\n```\n=== unprovided consumes ===\n  \"PUT /api/user\"      @ fe-vite     src/pages/Settings.jsx:19    ← the call now hits nothing\n\n=== unconsumed provides ===\n  \"PUT /api/users/me\"  @ be-express  src/app/routes/auth/auth.controller.ts:61   ← the route nobody calls\n```\n\nThat page is a **narrated walkthrough**: every command and the output it produced are written out, so it\nreads end to end without you running anything. The script behind it, `docs/demo/break-a-route.sh`, is a\nmaintainer tool rather than a first-run command — it builds a `cargo` example (so it needs a **source\ncheckout**, not a released binary) and analyzes two repositories **you supply** at\n`corpus/oss/fe-vite` and `corpus/oss/be-express`. `corpus/oss/` is gitignored and nothing in this repo\nships those trees — they are third-party checkouts, not ours to redistribute; see\n[CONTRIBUTING.md](CONTRIBUTING.md) on bringing your own corpus. (The synthetic corpus we *did* write\nis committed, at [`cases/`](cases/README.md) — every file of it but one, a fixture\nthat has to carry a live vendor-token literal and so cannot be committed at all; its README says what\nthat costs the benchmark score.)\n\n## Which of the two binaries do you want?\n\nzzop ships as two Node-free binaries. Decide which one you need before you install anything:\n\n| If you want | Use | How you drive it |\n|---|---|---|\n| An AI agent (Claude Code, Claude Desktop, any MCP client) to answer questions about your repos | **`zzop-mcp`** — an MCP server over stdio | Install the plugin or the `.mcpb` bundle and the agent calls the tools. You run no commands. → [Use in Claude Code](#use-in-claude-code-mcp-plugin) |\n| To run analyses yourself — a terminal, a CI job, a script | **`zzop`** — a plain CLI | `zzop init` once per tree, then `zzop analyze .` or `zzop cross --config …`. JSON to stdout. → [Use in a terminal or CI](#use-in-a-terminal-or-ci-zzop-cli) |\n\nBoth binaries dispatch to the same shared handlers over the same engine, so a tool call and a CLI run\nagainst the same path give the identical answer. Neither one makes a network request of any kind — they\ncarry no HTTP dependency at all ([privacy](https://eezz4.github.io/zzop/privacy.html)).\n\n- Documentation site: <https://eezz4.github.io/zzop/> (authored in [`site-src/`](site-src/), generated by `scripts/gen-site.mjs`; [`site/`](site/) is the committed output — a guard rejects hand edits to it)\n- Documentation (in-repo): [`docs/README.md`](docs/README.md)\n- How it works, in depth: [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md)\n- External parser protocol: [`docs/NORMALIZED_AST.md`](docs/NORMALIZED_AST.md)\n\n## Quick start\n\nNeither binary needs Node.js, npm, or a compiler. Get them one of four ways:\n\n<!-- Canonical install-lane list for repo readers. docs/getting-started.md, docs/modules/mcp.md and\n     VERSIONING.md link here instead of restating it; the site's Usage tab is the site-side twin (one\n     copy per audience, not one per page) — its sentences live in site-src/content/usage.mjs and are\n     generated into site/index.html, since site/usage.html became a redirect stub on 2026-08-14. Add a\n     lane here first, then link. -->\n\n- **Download the binaries.** Grab the `zzop-cli-<platform>[.exe]` (CLI) and/or `zzop-mcp-<platform>[.exe]`\n  (MCP server) assets for your platform from [GitHub Releases](https://github.com/eezz4/zzop/releases)\n  and run them directly, or put them on `PATH`. Each release also carries a `SHA256SUMS` asset covering\n  every one of those assets — **from v0.30.0 onward**; releases up to and including v0.29.1 do not have\n  one, so on an older pin check that the file is there before relying on it. Verify with\n  `sha256sum -c SHA256SUMS --ignore-missing`, or `shasum -a 256 -c SHA256SUMS --ignore-missing` on a\n  macOS box that has no `sha256sum`. Its scope is narrow and worth stating: it catches a corrupted\n  download, and it is a hook for anyone who obtained the digest through another channel. It does\n  **not** defend against a compromised release origin — an attacker who can swap an asset can swap\n  `SHA256SUMS` beside it — and TLS already refuses MITM.\n- **Claude Code plugin.** `/plugin marketplace add eezz4/zzop`, then `/plugin install zzop@zzop` —\n  see [Use in Claude Code](#use-in-claude-code-mcp-plugin) below. (Windows: the install hook needs a\n  POSIX shell — Git for Windows is the supported path; details in\n  [packages/README.md](packages/README.md#install-as-a-claude-code-plugin).)\n- **Claude Desktop.** One-click `.mcpb` bundle (drag-and-drop install) — what an installer should\n  know BEFORE installing (updates are manual; on macOS the unsigned binary is expected to hit\n  Gatekeeper; the privacy statement) is [packages/mcpb/BUNDLE-README.md](packages/mcpb/BUNDLE-README.md) —\n  bundles from releases after v0.32.0 carry that file as their own README; bundles up to and\n  including v0.32.0 ship without it, which is exactly why the pre-install pointer here matters.\n  Packaging internals: [packages/mcpb/README.md](packages/mcpb/README.md).\n- **npm.** `npm i -g @zzop/cli` installs the exact same `zzop` binary above, fetched for your platform\n  as an npm dependency — every subcommand `zzop help` lists, byte-for-byte the\n  same output, no Node runtime involved beyond a tiny launcher script and no separate JS implementation\n  that could drift from the native binary. Convenient when a project already manages its toolchain\n  through npm. See [packages/cli/README.md](packages/cli/README.md).\n\n## Use in Claude Code (MCP plugin)\n\nThe agent-facing lane. `zzop-mcp` is a self-contained binary with an MCP server built in; you install it\nonce and then ask questions in plain language — the agent picks the tool.\n\n1. `/plugin marketplace add eezz4/zzop` — then `/plugin install zzop@zzop` (two separate steps).\n2. Start a new session. The plugin downloads the binary for your platform on first run; nothing goes\n   on `PATH`. **That first session does not list the zzop tools yet** — the tool list is settled\n   before the download finishes — so restart Claude Code once and they appear (the hook says so on\n   stdout too). Once installed, a newer release is reported to you, never installed behind your back.\n\nThe server exposes the tools `analyze_repo`, `cross_repo`, `check_file`, `check_endpoint`,\n`analyze_envelope`, `validate_envelope`, `validate_rule_pack` — plus the `zzop://contract/*` resources\ncarrying the authoring contracts (among them the envelope schema, the DSL reference, the rule catalog,\nthe config surface and an annotated starter config), so an adapter or rule pack can be written with\nnothing but the binary. `zzop-mcp` itself takes no analysis subcommands: bare or `mcp` serves stdio,\nand `version`/`help` are the only other forms.\n\nSee [packages/README.md](packages/README.md) for the full install/tool/resource reference, and\n[docs/modules/mcp.md](docs/modules/mcp.md) for exact argument shapes.\n\n## Use in a terminal or CI (`zzop` CLI)\n\nThe human-facing lane. Write a `zzop.config.jsonc` and run it, ESLint-style. **A config is required** —\nevery analysis lane refuses a tree that has none, on both binaries alike, because the names zzop would\notherwise guess about your project (what you call your auth guards, which banners mark your generated\nfiles) live in that file, and a key you do not declare is a judgment zzop does not make:\n\n```sh\nzzop init                               # write the starter config — do this first, once per tree\nzzop analyze .                          # analyze one repo/tree -> JSON findings summary\nzzop analyze --config ci/zzop.config.jsonc   # same, for a config that does not sit at the tree root\nzzop analyze . --severity critical --limit 10  # narrow the findings LIST (counts always cover everything)\nzzop cross --config zzop.config.jsonc   # cross-layer join, driven by that config\nzzop analyze . --fail-on critical       # THE CI GATE: exit 3 when anything at/above that severity exists\nzzop <subcommand> --help                # that one subcommand's own line (exit 0); `zzop help` prints them all\n```\n\n`zzop --help` is the canonical subcommand list — this README does not repeat it.\n\n**The exit code is `0` unless you ask otherwise.** Without `--fail-on`, a run answers only \"did zzop\nrun\", so a tree full of criticals still exits `0`; with it, findings at or above the named severity\nexit `3` (a third code, so a CI log can tell a broken config on `1` apart from a real finding) while\nthe whole reply still goes to stdout. Check what your build does rather than this paragraph:\n`zzop analyze . --fail-on critical; echo $?`. The full code table, including the `2` a provably\nunmatchable `--rule` id lands on, is in\n[docs/getting-started.md](docs/getting-started.md#reading-the-output).\n\n**A run writes into the tree it analyzes.** The first analysis creates `.zzop/cache/` beside your\nconfig (the default `cacheDir`) and keeps the per-file analysis cache there — pure derived state,\nsafe to delete, regenerated on the next run, and it grows with the tree rather than staying small\n(measure yours with `du -sh .zzop`; this repository's own is not a number worth printing here because\nit moves with every run). `zzop init` adds the anchored `**/.zzop/` line to that directory's\n`.gitignore` for you; set `\"cacheDir\": null` to write nothing at all.\n\nTo track contract drift over time rather than at one instant, commit a **structural manifest** and diff\na later run against it — the same shape a lint baseline file has, kept by you, not by zzop:\n\n```sh\nzzop manifest ./api ./web > contracts.json   # identity only: provides/edges/bucket membership\nzzop diff contracts.json contracts.new.json  # read `transitions` first — a key that left `edges`\n                                             # for `unprovidedConsumes` is a broken contract\nzzop facts ./api ./web > facts.json          # post-assembly facts (per-tree CommonIr + the whole join,\n                                             # uncapped) for your own rule program\nzzop graph ./api ./web > join.mmd            # the cross-layer join as mermaid, for any renderer —\n                                             # scoped with --scope/--top, every cap disclosed in the file\nzzop graph . --domain dep > imports.mmd      # the FILE import graph instead — cycles drawn as hexagons\n                                             # with thick arrows, from the engine's own circular findings\nzzop graph . --domain risk > risk.mmd        # blast-radius hubs + extraction seams. The health SCORES\n                                             # are NOT drawn — a table of numbers is not a graph\nzzop graph . --domain posture               # the mutating attack surface and its guard status —\n                                             # a box means GUARDED-OR-EXEMPT, never proven guarded\nzzop graph . --domain cochange > churn.mmd   # which files keep changing TOGETHER, from git history —\n                                             # not imports, so it finds the coupling no dep graph can see\nzzop graph . --domain dep --fold 2           # the SAME import graph with each path's first 2 segments\n                                             # drawn as one box — the module map you were trying to see\n                                             # IS the picture, and every edge says how many file edges\n                                             # it collapsed. --fold 1 for the top-level view\n```\n\nThe manifest is deliberately uncapped and carries no file or line, so a pure refactor diffs empty while\na route leaving the join cannot hide above a summary's caps. `diff` refuses two manifests from different\nzzop builds unless you pass `--allow-tool-drift` (which then discloses the drift), and tags a removal\nattributable to a source that lost coverage as `blindnessSuspect` rather than calling it a deletion.\n\n`facts` is the other uncapped lane, and the consumer half of the custom-rule extension point: when the\nDSL cannot express your rule, zzop emits everything it knows after assembly and the cross-layer join —\neach tree's whole `CommonIr` plus every join bucket, verbatim — and your own program decides what counts\nas a problem. zzop neither runs your program nor reads its findings back; see\n[docs/modules/facade.md](docs/modules/facade.md#custom-rules-consumer-side-zzop-facts) for the shape.\n`manifest`, `diff`, `facts`, `coverage`, `graph`, `explain` and `init` are CLI-only lanes with no MCP tool twin.\n\nThe rest of the surface: `analyze-envelope`, `validate-envelope`, `validate-rule-pack`, `endpoint`,\n`file` (everything zzop knows about ONE file — its tree, symbols, io facts, dependency edges both\nways, and every finding anchored there; its verdict says whether the file was ANALYZED, so an empty\nfindings list is never mistaken for \"clean\" on a file nothing structural ran on),\n`init` (write the annotated starter `zzop.config.jsonc`; the same document MCP serves as the\n`config-template` resource), `contract`, `explain`, `version`, `help`. See\n[packages/README.md](packages/README.md) for the full CLI and config reference.\n\nTo embed the engine instead of running either binary, call the `zzop-facade`/`zzop-summary` crates'\nJSON-in/JSON-out contract directly — they are workspace-internal and not published to crates.io, so an\nin-process Rust dependency means vendoring this workspace, not `cargo add`. Shelling out to the CLI's\nJSON subcommands needs no linkage at all:\n\n```rust\nlet report: serde_json::Value =\n    serde_json::from_str(&zzop_facade::analyze_json(r#\"{\"root\":\".\"}\"#)?)?;\n```\n\n## Result (abridged)\n\nThis is what the two binaries above actually print — `findings` is a **census object**, not an array,\nbecause the reply is a shaped summary rather than a raw dump. The numbers below are a real run against\nthis repository's own `cases/trees/api-be` fixture, **measured with a `zzop` built from this\ncheckout**: `zzop analyze --config cases/trees/api-be/zzop.config.jsonc`.\n\n**Which binary you reproduce them with is part of the claim.** `zzop version` prints the release\nnumber alone, and `main` keeps that number between releases — so an installed `@zzop/cli` and a build\nof this checkout can both answer `0.33.0` and legitimately report different findings, because they\nare different builds of one version string. `zzop version --verbose` is what tells them apart: it\nprints each parser's fingerprint and the engine hash. If your counts differ from the block below,\ncompare that line before assuming either side is wrong.\n\n(To the next editor: these numbers move whenever `cases/trees/api-be` changes **and whenever a\nrelease changes what is measured** — re-run that command and re-measure them, never patch one in\nisolation. Three of them were stale for exactly that second reason until 2026-08-11 — and the whole\nblock was again on 2026-08-15, after v0.31.0 exported the `code-hygiene` pack out of the bundle and\n114 findings became 85. It happened a THIRD time on 2026-08-21, and that time nothing about the\nfixture moved: a Prisma delegate-accessor fix changed what `schema/unreferenced-field-name` counts as\nreferenced, one info finding went away, and 85 became 84 with this block untouched. The lesson the\nfirst two did not teach is in this paragraph's first sentence — \"whenever a release changes what is\nmeasured\" includes every rule change, not only the loud ones, and no guard here can catch it,\nbecause the only machine that knows the number is the run.\n\nAn outside reader reported 87/73 for this block on the same day and was NOT reading a stale README:\nthey measured with the published `@zzop/cli` 0.33.0, a different BUILD of the same version string,\nand got a legitimately different answer. That is what the paragraph above this one is for. The\n`disclosure` line below is deliberately no longer one of these numbers: see its own comment.)\n\n`pain` never travels alone. `painMeasuredWeight` / `painTotalWeight` is how much of the weight table this\ntree could actually be measured on, and `pain: null` means no metric had a population at all — absence of\ndata, never a clean bill.\n\n**And `pain` is not a defect score.** It contains no rule findings whatever: the run below reports 84\nfindings, 5 of them critical, while its `defect` pain is `0`. `painByAxis` splits the number so that is\nvisible instead of implied — `defect` (import cycles, the only entry), `opinion` (barrel discipline, FSD\nlayering, SDP/Main Sequence, Newman modularity, LOC ceilings — a project that deliberately does the\nopposite is not wrong, it scores low), and `history` (rename churn, bus factor). The three sit on `pain`'s\nown scale and sum to it. Read `findings` for defects; read `pain` for how much zzop disagrees with how the\ncode is arranged.\n\n```json\n{\n  \"fileCount\": 84,\n  \"findings\": {\n    \"total\": 84,\n    \"bySeverity\":  { \"critical\": 5, \"warning\": 71, \"info\": 8 },\n    \"byRule\":      { \"security/weak-crypto\": 6, \"db/unawaited-write\": 1 },\n    \"shown\":       [ /* 50 here — the listed slice, capped by --limit; each entry has ruleId, severity, file, line, message */ ]\n  },\n  \"architecture\": { \"pain\": 7.5, \"painMeasuredWeight\": 13.8, \"painTotalWeight\": 18.6,\n                    \"painByAxis\": [ { \"axis\": \"defect\",  \"pain\": 0.0, \"totalWeight\": 3.0 },\n                                    { \"axis\": \"opinion\", \"pain\": 7.5, \"totalWeight\": 15.0 },\n                                    { \"axis\": \"history\", \"pain\": 0.0, \"totalWeight\": 0.6 } ],\n                    \"topRecommendation\": null, \"criticalTop\": [],\n                    /* + painMeaning / topRecommendationMeaning / criticalTopMeaning: the sentences\n                       that say what each of the three above is, and is NOT, on the wire */ },\n  \"coverage\":     { /* how much of the tree zzop actually saw, per extension */ },\n  \"coverageGaps\": { /* which principal extensions reached no resolved import edge, always present —\n                       each row's `kind` (\"source\" vs \"data-config\") is what says whether the zero\n                       means a missing parser or a filetype you have to open to judge */ },\n  \"disclosure\":   { /* the census of zzop's OWN known silent-failure classes, keyed\n                       classes / asserted / partial / notYetDetected, plus the `note`,\n                       `command` and `resource` that lead to the full text. No counts are\n                       copied here: the reply carries its own, and the two that were copied\n                       here went stale while the two beside them stayed right — which is\n                       indistinguishable from correct until someone re-runs it. Read them\n                       with `zzop contract disclosure-classes`, the command the field itself\n                       names. */ },\n  \"warnings\":     [ /* anything this run could not provide */ ]\n}\n```\n\nEvery finding carries a rule id, severity, a `file:line` location, and a message naming the config key\nthat silences it — the records themselves ride in `shown`. `bySeverity`/`byRule` always count the WHOLE\nrun, so a `--limit` that shortens that list never changes them; that split is why `findings` is an object.\n\nThe per-metric `scores` block, the `health` object and `recommendations` are **not** on this wire: the\nshaped summary folds them into the compact `architecture` object above. They exist in full only in the\n`zzop-facade` embedding lane (`zzop_facade::analyze_json`, the snippet just above) — see\n[docs/modules/facade.md](docs/modules/facade.md) for that lane's own shape.\n\n`analyzeTrees` (multi-tree) additionally returns `crossLayerFindings` — frontend fetch <-> backend\nroute joins — which has no single-tree equivalent.\n\n## How it works\n\nEach repository is parsed into one language-neutral IR, so a Python route and a TypeScript `fetch` end\nup as the same kind of fact. The headline move is the cross-repo join: frontend calls are exact-matched\nagainst backend routes across the repo boundary, and the leftovers are named rather than dropped — a\ncasing or base-path difference, a version drift, a method mismatch each come back as a near-miss finding\nnaming the dimension that differs, instead of a diff you have to do by hand. A near miss is judged on\nthose axes, never on spelling: a plural or a typo (`/api/userss` against `/api/users`) is reported as an\nunmatched call, not paired with the route it probably meant — the per-rule scope is in\n[the catalog](docs/rules/catalog.md). Alongside the join, the same engine runs a layered rule system (native\nwhole-graph analyses plus declarative JSON rule packs) over each repo individually, adding structural\nfindings, dependency/dead-code analysis, and health scores to the same JSON document.\n\nEvery run is deterministic — same code in, same findings out, byte-stable enough to diff two runs\nagainst each other. That is what makes zzop usable as a CI gate (fail a PR on contract drift by reading\nthe JSON severity counts) and as a substrate an agent can re-run without chasing a moving answer. Just\nas important, a run reports its own blind spots: `warnings` and the per-tree `coverage` census say what\ndid **not** run, so a short findings list can be told apart from a blind engine.\n\nFull design: [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md). Reading the output, severity semantics and\nsuppression: [`docs/getting-started.md`](docs/getting-started.md). When your stack does not match the\ndefaults — a house extension, your own guard names, a gateway prefix, a rule that does not exist yet —\n[`docs/extending.md`](docs/extending.md) lists every plug-in point in the order you hit them.\n\n## Supported languages\n\n| Language | Support |\n|---|---|\n| TypeScript / JavaScript (`.ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts`) | Native, full AST (swc): symbols, imports, calls, HTTP routes/egress, TypeORM `@Entity`/`@Column` `db-table` **provides** (the schema side, joining the client-side consumes), `db-table` consumes from ORM accessors AND from raw SQL statement strings |\n| Python (`.py, .pyi`) | Native, full AST (ruff, Python 3 — Python-2-only syntax falls back to lexical): symbols, imports, FastAPI route provides, Django URLconf route provides (`urlpatterns` `url()`/`re_path()`/`path()` entries with cross-file `include('<dotted.module>')` mounts, emitted verb-unknown since the method lives in the view class), `requests`/`httpx` consumes (module-level calls plus `Session`/`Client`/`AsyncClient` instances), SQLModel/SQLAlchemy + Django ORM `db-table` provides and their query-site consumes, call sites, auth-guard evidence (FastAPI `Depends` + DRF `permission_classes`) — v1 scope |\n| Rust (`.rs`) | Native, full AST (syn 2): symbols, imports/`mod` tree (incl. same-workspace crate resolution), axum route provides, `reqwest` consumes, raw-SQL `db-table` consumes (sqlx/tokio-postgres/rusqlite/…), call sites for the whole-repo call graph, extractor-based auth-guard evidence — v1 scope |\n| Go (`.go`) | Native, full CST (tree-sitter-go 0.25): symbols, imports/dep graph (`go.mod` module resolution, package-directory-wide edges), gin + `net/http` route provides (cross-file mount composition — a function-parameter router mounted from another file's call site — incl. Go 1.22 `\"METHOD /path\"` mux syntax), `net/http` literal egress consumes (package free functions plus bound `http.Client` values), GORM `db-table` facts, call sites — v1 scope |\n| Java (`.java`) | Native, full CST (tree-sitter-java 0.23.5, Java 21 grammar): symbols (incl. nested types, dot-qualified method names, real visibility), imports/dep graph (`(package, type)`-indexed resolution, glob package-directory-wide edges), Spring MVC route provides (cross-file `extends`-chain + constant-prefix resolution), `RestTemplate`/`WebClient` literal egress consumes (Feign and `java.net.http` not recognized; `RestTemplate.put`/`.delete` deliberately not recognized, generic names that would false-key `Map.put` — disclosed), JPA `@Entity`/`@Table` `db-table` provides, call sites, Spring Security auth-guard evidence (`@PreAuthorize`/`@PostAuthorize`/`@Secured`/`@RolesAllowed`) — v1 scope |\n| C# (`.cs`) | Native, full CST (tree-sitter-c-sharp 0.23.5): symbols (incl. nested types, dot-qualified method names, `public` visibility), imports/dep graph (namespace→files index, `using` package-directory-wide edges), ASP.NET Core route provides (attribute controllers with `[Route(\"api/[controller]\")]` + `[HttpGet]`/… composition, plus same-file Minimal-API `app.MapGet`/`MapGroup`), `HttpClient` literal egress consumes, EF Core `DbSet<T>`/`[Table]` `db-table` provides, call sites — v1 scope |\n| Prisma schema (`.prisma`) | Native, lexical schema: models/fields (structural + usage-aware schema rules) + `db-table` provides joining the client-side consumes |\n| SQL (`.sql`) | Native, lexical: `CREATE TABLE` → `db-table` provides (migration files light up the db-table channel for MyBatis/JDBC-style stacks). The crate also owns the channel's consume-side statement reader, which other parsers call on the SQL strings they hold |\n| Anything else (Ruby, JSP, ...) | Lexical fallback in-tree: the files are walked and counted, and only `line-scan` rules can reach them. **That is a ceiling, not a promise of coverage** — a bundled rule reaches your language only if its own `file_pattern` names the extension, and for most languages outside the rows above none does, which is a zero this row deliberately does not spell as a number (it is per-language and it moves). Measure it on YOUR tree instead: `zzop analyze <tree>` and read `packsLoaded[].filesInScope` — `0` on a pack means not one of its rules' path gates admits a byte here, so that pack's zero findings are scope, never a clean bill — beside `coverageGaps`, whose row for the extension carries `kind: \"source\"`. Measured that way on a five-file Ruby tree, no bundled pack admitted a single `.rb` file. First-class support is an external parser adapter conforming to the [Normalized AST protocol](docs/NORMALIZED_AST.md). |\n\nFull precision-tier breakdown — exactly what each native parser extracts, Python's v1 scope note, and\neach parser's fingerprint — in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md#language-support). (Those\nfingerprints are not in `zzop version`'s default output, which prints the bare release number so scripts\ncan parse one token; `zzop version --verbose` — and `zzop-mcp version --verbose`, the identical string —\nprints them, and `zzop manifest`'s `tool` field carries the same string inside the artifact.)\n\nRust carries one reporting rule no other language has: a finding whose line sits inside a\n`#[cfg(test)]`/`#[test]`-gated item is dropped, because Rust's unit tests live inside the shipping file\nwhere the path-shaped test exclusion every other language relies on (`foo.test.ts`, `tests/test_foo.py`)\ncannot see them. The credential-at-rest rules opt out and keep judging those regions — a committed key is\nleaked whether or not the compiler keeps it — and each says so in its own catalog row. Both halves:\n[docs/rules/catalog.md](docs/rules/catalog.md).\n\nA normal-sized file whose extension has no native parser also self-reports in the output's `warnings`\n— naming the extension, a file count, and a path sample — instead of vanishing silently; point it at an\nadapter (`overlays: [...]` in `zzop.config.jsonc`) if that language matters for the analysis.\n\n## Versioning & stability\n\nzzop is **pre-1.0 (`0.x`) and unstable** — any release may change behavior, output, rules, or\ndefaults, so pin an exact version (not a `^`/`~` range) and re-test before upgrading. Semantic\nVersioning begins at `1.0.0`. What is promised before then is narrower, and is a promise about the\nrecord rather than the rate: a break to one of the surfaces [VERSIONING.md](VERSIONING.md) names is\nwritten down, old and new spelling both, in [CHANGELOG.md](CHANGELOG.md). VERSIONING.md is also\nwhere the properties *inside* those surfaces that are still moving get named — rule ids,\n`SourceSymbol.id` uniqueness, native id namespaces — rather than left for you to infer from a run.\n\n## Layout & development\n\nContributing, or just want the crate map? [`CONTRIBUTING.md`](./CONTRIBUTING.md) carries both — the\nper-crate responsibility list and the build/test/measure commands. They live there rather than here\nbecause this page is read to DECIDE whether to adopt zzop, and a dependency graph answers a question\nnobody asks before installing.\n\n## License\n\nMIT — see [`LICENSE`](./LICENSE).\n\n",
  "bytes": 29880,
  "sha": "e971c866bb998f7e8d6eb986a12f7e2d6ddf9cb66c0988c7b1d96917f0259255",
  "repo_slug": "eezz4/zzop",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_eezz4_zzop_7220c2c6/readme"
}