What DACIP checks
DACIP is a contract engine. For every boundary it supports, it statically extracts both sides, links them, and reports mismatches — 22 analyzers, no code execution, no LLM anywhere in the pipeline.
Output comes in two grades:
- Full scan (
dacip scan,dacip investigate) produces boundary censuses. Most mismatches surface as warnings or observations, because the other side of the contract may legitimately live outside the repo. A few full-scan checks are proven outright — an unregistered Celery beat task, a broken local import, CI that never runs your tests — because the evidence is complete in-repo. - The PR diff gate (
dacip diff) produces defect-grade assertions: a PR that removes one side of a contract the rest of the code still uses. See the findings model for severity and confidence semantics.
Every analyzer below also names its ceiling. When DACIP can't see something, it says so instead of guessing.
HTTP routes vs frontend calls
The original boundary: backend route registrations vs the frontend calls that target them.
| Side | Supported |
|---|---|
| Backend | Flask and Flask-RESTX; Django and DRF (include() trees, router expansion); drf-extensions nested router graphs; Flask-AppBuilder (ModelRestApi CRUD + @expose); FastAPI (include_router graphs with stacking prefixes); Express (app.get/post/… registrations, including servers living inside frontend/); Next.js App Router route.ts handlers |
| Frontend | TypeScript/JavaScript fetch and axios calls, with baseURL resolution |
Full-scan signals: UNMATCHED_CALLS, PARTIAL_ROUTES, DYNAMIC_CALLS, LOW_MATCH_RATE, NO_BACKEND_ROUTES, UNRESOLVED_FRONTEND_BASE.
Ceilings: a route prefix that isn't a constant makes the route partial, never a guess. Next.js Pages Router (pages/api/*) is not extracted — the coverage line says so. Frontend calls hidden inside wrapper functions are invisible until you teach them via coverage and teachable clients. Frontend extraction uses your repo's own TypeScript via Node; if unavailable, you get a TS_ANALYZER_UNAVAILABLE diagnostic, not silent gaps.
Every other boundary
| Boundary | Compares | Example codes | Stays silent when |
|---|---|---|---|
| Env vars | os.environ/getenv, process.env reads vs .env*, compose environment:, Dockerfile ENV |
ENV_VAR_UNUSED (warning), ENV_VAR_UNDECLARED (one info line) |
No declaration file exists; dynamic key access suppresses UNUSED |
| OpenAPI spec | Committed OpenAPI/Swagger paths vs routes extracted from source | SPEC_ROUTE_NOT_IN_SOURCE, ROUTE_NOT_IN_SPEC |
The two route spaces don't demonstrably overlap; partial routes never compared |
| GraphQL | Operation selection sets (.graphql/.gql, gql tags) vs SDL schema |
GRAPHQL_FIELD_NOT_IN_SCHEMA |
Enclosing type unknown; @client, introspection, interpolated fragments; more than one root schema |
| Events (Socket.io) | emit vs on handlers, cross-language |
EVENT_EMITTED_NO_HANDLER (warning), EVENT_HANDLER_NEVER_EMITTED (info) |
Dynamic event names; reserved events (connect/disconnect) excluded |
| Celery | Beat-scheduled and send_task names vs registered @shared_task/@task |
CELERY_TASK_NOT_FOUND, CELERY_TASK_NAME_MISMATCH (proven), CELERY_SEND_UNREGISTERED (warning) |
Sent tasks may be registered by a remote worker — warning, not defect |
| Kafka | Produced topic literals vs in-repo consumers | KAFKA_TOPIC_NO_CONSUMER (warning) |
Repo lacks either side; dynamic topic names |
| Django migrations | Statically replayed migration state vs declared model fields | ORM_FIELD_NOT_MIGRATED |
Whole app suppressed on squashed graphs or non-literal operations; only literal models.X(...) fields count |
| Feature flags | In-source flag registry vs code references | FLAG_UNUSED (info) |
No in-repo registry; referenced-but-undeclared is never asserted (remote/dashboard flags exist) |
| i18n keys | t() / <FormattedMessage id> keys vs the union of locales/*.json |
I18N_KEY_MISSING (warning), I18N_KEY_UNUSED (info) |
No locale files; dynamic keys suppress the unused direction |
| Internal links | <Link href>, router.push vs Next.js pages and react-router routes |
DEAD_INTERNAL_LINK (warning) |
Dynamic hrefs; rewrites and middleware are invisible, so warning only |
| Compose ports | Compose container-side port vs the port the source binds | COMPOSE_PORT_UNBOUND (warning) |
Any bind is dynamic or $PORT — which covers most production apps |
| Local imports | Import paths vs the repo's file tree | UNRESOLVED_IMPORT |
Third-party and stdlib imports — never checked, that's a type-checker's job |
| ORM manager contracts | Managers whose create() provably requires a kwarg vs call sites |
MISSING_REQUIRED_KWARG (proven) |
Calls passing **kwargs; managers without a provable guard |
| CI config | Test files present vs what workflows actually run | CI_NO_TEST_RUN, CI_TEST_DISCOVERY (proven) |
— (reads workflow YAML directly) |
| Auth matrix | Route census × extracted auth markers, as auth_matrix.json and a report section |
No finding — a descriptive artifact | Frameworks without marker extraction show not_extracted; an empty cell never reads as "public" |
What the diff gate asserts
On a PR, dacip diff promotes these to defect grade: BREAKING_ROUTE_REMOVED, CONTRACT_REGRESSION, AUTH_MARKER_REMOVED, REQUIRED_FIELD_ADDED, ENV_VAR_DECLARATION_REMOVED, ENV_VAR_INTRODUCED_UNDECLARED, EVENT_HANDLER_REMOVED, EVENT_EMIT_INTRODUCED_NO_HANDLER, GRAPHQL_SCHEMA_FIELD_REMOVED, SPEC_DRIFT_ROUTE_REMOVED, SPEC_DRIFT_UNDOCUMENTED_ADDED, ORM_FIELD_NOT_MIGRATED, FLAG_REGISTRY_REMOVED.
Every verdict is byte-identical across runs — verify that yourself on the proof dashboard, or start with the quickstart.

