DACIP DACIP

What DACIP checks

DACIP is a contract engine. For every boundary it supports, it statically extracts both sides, links them, and reports mismatches — 19 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, including braced exports (export const { GET, POST } = toNextJsHandler(…)) and catch-all segments ([...all])
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. A Next.js catch-all that only forwardsapp/api/[...path]/route.ts proxying to a Flask backend — is not treated as a handler, because a forward is no evidence the far side serves the path; the calls beneath it stay unmatched. 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. TypeScript symbols are extracted by name and position, never by the type checker: re-export chains, aliased default exports and dynamic import() are reported as unresolved rather than guessed.

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, including a namespace bound at the hook (useTranslations('cart') + t('checkout')cart.checkout) 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
Server Actions <form action={X}> field names vs the action's formData.get() keys and zod schema ACTION_FIELD_MISSING (defect), ACTION_FIELD_UNUSED (warning), REVALIDATE_PATH_DEAD (warning) The action or a field name is computed; useActionState, formAction= and direct calls are not linked
Prisma migrations schema.prisma models and scalar/enum fields vs the columns prisma/migrations/*/migration.sql actually create PRISMA_MODEL_NOT_MIGRATED, PRISMA_FIELD_NOT_MIGRATED (proven), PRISMA_REPLAY_INCOMPLETE (warning) No prisma/ directory or no migrations; any unmodelled DDL suppresses the whole check
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 AND Server Action census × extracted auth markers, plus client_tiers — the next-safe-action builder each action is made from, which is where .use(middleware) lives No finding — a descriptive artifact Frameworks without marker extraction show not_extracted; an empty cell never reads as "public"

The auth matrix: a census, never a verdict

One artifact on this page is not a finding. auth_matrix.json lists every endpoint DACIP extracted — HTTP routes and Next.js Server Actions, because a "use server" export is a POST endpoint anyone who can reach your app can call — against whatever authorization markers it could see.

Each row carries a three-valued status, and the third value is the point:

  • has_markers — an auth decorator or dependency was found on this endpoint.
  • no_marker_detected — none was found on the endpoint itself. Auth may still be enforced by middleware, a base class, a parameter-level dependency, or a gateway DACIP cannot see. This is not a claim that the endpoint is public.
  • not_extracted — this endpoint's framework has no marker extraction in DACIP; auth was not assessed at all.

The string public is not a status a cell can hold. That restraint is what makes the matrix zero-false-positive by construction: a descriptive artifact can only ever over-describe.

For Server Actions there is a second dimension. Libraries like next-safe-action put authorization in the builderactionClientUser.inputSchema(S).action(fn), with .use(middleware) on the client — not in a call inside the action body. So client_tiers counts the builder each action is made from:

"client_tiers": { "actionClient": 131, "actionClientUser": 33, "adminActionClient": 17 }

That is 181 publicly callable endpoints across three authorization tiers, from one real repo. DACIP will not tell you which tier is the unauthenticated one — the presence of middleware is not proof of authorization, since it could be logging or rate limiting. It tells you the tiers exist and which endpoints sit on each. You know what your base client does.

If your codebase names its own assertion helper, teach it: dacip scan --action-auth requireRole.

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.