DACIP DACIP

Coverage & the honest ceiling

DACIP is deterministic: it only reports what it can prove from the code. The flip side is that its view has a ceiling — some paths are built at runtime, some files can't be parsed, some client wrappers aren't recognized. DACIP's rule for all of these is the same: list the gap, never guess across it. A short report from DACIP doesn't mean "nothing is wrong"; it means "here is what I saw, and here is exactly what I couldn't."

The coverage block

Every run writes a coverage.json next to the findings, and every report embeds the same block under a ## Coverage heading:

{
  "files": { "total": 412 },
  "symbols": { "total": 3180 },
  "routes": { "total": 307, "resolved": 301, "partial": 6 },
  "calls": { "literal": 118, "template": 64, "dynamic": 9, "total": 191 },
  "links": { "matched": 168, "no_backend_route": 5, "page_route": 2, "uncomparable": 9 },
  "frontend_base_urls": ["/api/v1"]
}

What the fields mean:

Field Meaning
symbols.total Declarations extracted — Python (AST) and TypeScript/JavaScript (name-and-position, not type-resolved). Generated code is excluded.
routes.resolved Backend routes whose full URL prefix was statically resolved
routes.partial Routes with an unresolved prefix — matched to frontend calls by suffix only
calls.literal / calls.template Frontend calls whose path is a string literal, or a template with static segments (/users/{param})
calls.dynamic Calls whose path is fully runtime-computed — listed, never compared
links Match status of each comparable call against the extracted routes
action_links Server Action bindings: matched, field_mismatch, unresolved_action. Kept separate from links so the HTTP match rate stays a measure of HTTP only.
links.page_route Calls whose path is one of the app's own pages — an SSR fetch, a Playwright request.get('/login') — not a missing API endpoint, and excluded from the match rate

Gaps also surface as warning diagnostics in diagnostics.json and the report's "Analysis notes" section — codes like PARTIAL_ROUTES, DYNAMIC_CALLS, UNMATCHED_CALLS, LOW_MATCH_RATE, and NO_BACKEND_ROUTES. These are notes, not findings: only error-grade evidence becomes a finding. If fewer than 85% of comparable calls match a route, LOW_MATCH_RATE tells you route extraction may be incomplete for your repo — treat cross-stack results accordingly.

Dynamic paths: listed, never guessed

A frontend call like client.get(buildUrl(entity)) has no statically knowable path. DACIP marks it dynamic, links it as uncomparable ("Frontend path is fully dynamic; cannot compare."), and moves on. It will not pattern-match or infer what the URL "probably" is — a guessed match is how false positives happen.

Teaching DACIP your HTTP client

The TypeScript analyzer recognizes fetch, axios, and method calls (.get/.post/.put/.patch/.delete) on common client names such as api, http, client, apiClient, fetcher, and $api. If your codebase wraps requests in something it doesn't know — say _data.get(...) — those calls are invisible until you name the wrapper:

dacip scan --client _data --client legacyApi

The flag is repeatable and works on scan, investigate, diff, and the other analysis commands.

The TypeScript analyzer needs Node

Frontend extraction runs the repo's own typescript package via Node. If neither the repo (checked in node_modules/, including frontend/, web/, client/ subdirectories) nor DACIP's toolchain cache has a usable typescript, the analyzer emits TS_ANALYZER_UNAVAILABLE — and because losing the whole frontend side must never look like "no findings", the CLI prints a loud stderr notice telling you frontend calls were NOT extracted and cross-stack findings are incomplete. The one-time fix:

dacip toolchain install-ts

This provisions a DACIP-managed typescript in the user cache, so it also covers repos with no node_modules at all.

Unparseable files

A file DACIP can't parse is DACIP's reading ceiling, not evidence of a defect in your code. Python files that fail to parse — for example, syntax newer than the interpreter this DACIP build ships with — are excluded from analysis and reported as a PYTHON_PARSE_ERROR warning naming the file and line. They never become findings.

Reading a thin report

When a report looks sparse, check the coverage block first. routes.total: 0 alongside real frontend calls means contract checking never ran, not that your contracts are clean. That distinction is the point: DACIP tells you what it can't see, so you know exactly how much weight the rest of the report carries.

Next: Quickstart to run your first scan, Troubleshooting for analyzer setup issues, or the proof dashboard for how this plays out on real repos.

Incremental analysis, and its two ceilings

Extraction is memoized per file, keyed on the analyzer, its version, the file's path and a hash of its bytes. A one-file edit therefore re-extracts that file and reads the rest from the cache: on Redash (861 files) a cold run performs 8,932 extractions and a one-file Python edit performs 15. The counters are written to incremental.json beside the report, so the claim is checkable on your own repository rather than taken on trust:

{"files_scanned": 861, "extractions_run": 15, "cache_hits": 8917,
 "tree_analyzers": 9, "tree_cache_hits": 8, "tree_extractions_run": 1}

Two analyzers cannot be incremental, and DACIP says so rather than implying otherwise:

  • TypeScript is a whole-program pass. One file's types depend on the others, so any .ts/.js edit re-runs the compiler over the program. Its result is still cached, which means a Python-only edit skips it entirely.
  • The compose/port check reads both sides. It compares the ports your app binds against what docker-compose maps, so any source edit re-runs it. Declaring less would let it serve a stale verdict.

Analyzers that read files outside the snapshot — .env*, locale JSON, .graphql documents, flag registries, OpenAPI specs — are cached on a digest of exactly the files they consume, so editing your source never invalidates them and editing a .env invalidates only what reads it.