{
  "markdown": "# tieline\n\n**Static frontend↔backend contract-drift checker. Pact without writing a single contract test.**\n\n[![npm](https://img.shields.io/npm/v/@nugehs/tieline?style=flat-square)](https://www.npmjs.com/package/@nugehs/tieline) [![CI](https://img.shields.io/github/actions/workflow/status/nugehs/tieline/test.yml?style=flat-square&label=CI)](https://github.com/nugehs/tieline/actions/workflows/test.yml) [![license: MIT](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](LICENSE) [![node](https://img.shields.io/badge/node-%3E%3D18-blue?style=flat-square)](#) [![tests](https://img.shields.io/badge/tests-71%20passing-brightgreen?style=flat-square)](#tests) [![dependencies](https://img.shields.io/badge/dependencies-0-brightgreen?style=flat-square)](#)\n\n![tieline demo](tieline-demo.gif)\n\n`tieline` reads the code you already wrote on both sides of an API boundary — the\nHTTP calls your frontend makes and the routes your backend exposes — and tells you\nwhere they disagree. No contract tests to author, no broker to run, no backend to\nboot. It finishes in well under a second and is built to run in CI as a gate.\n\n> Delete the LLM and a developer still installs it. `tieline` is a deterministic\n> tool first; an agent reading its output is a bonus.\n\n---\n\n## How it works\n\n```\n  frontend code                                  backend code\n  (axios, rtk-query, …)                          (express, nestjs, openapi, …)\n        │                                                │\n   client adapter                                   server adapter\n        │            normalize  ${id} :id {id} → {}       │\n        └───────────────►  join on (METHOD, path)  ◄──────┘\n                                   │\n              ✅ matched   ❌ drift   ⚠️ unverifiable   🟡 dead\n```\n\nBoth sides are reduced to a canonical `(METHOD, path)` and joined. The matcher is\n**adapter-agnostic** — any client adapter pairs with any server adapter, so the\nsame engine checks a React app against Express, an Angular app against Spring, or\nanything against an OpenAPI spec.\n\n| Result | Meaning |\n| --- | --- |\n| ✅ **matched** | FE call resolves to a real BE route |\n| ❌ **drift** | FE call resolves but the BE has no such route/method — **the bug bucket** |\n| ⚠️ **unverifiable** | FE url is built at runtime — reported, never guessed |\n| 🟡 **dead** | BE route no resolvable FE call reaches (informational) |\n\n---\n\n## tieline vs alternatives\n\nThese tools solve neighbouring problems — pick by what you have and what you want guaranteed.\n\n| Tool | How it works | What it catches | What it needs | Reach for it when |\n| --- | --- | --- | --- | --- |\n| **tieline** | Static analysis of FE call sites + BE route declarations, joined on `(METHOD, path)` | A frontend calling a route the backend doesn't expose (path/method drift), undocumented/phantom spec routes | Source code of both sides; nothing running, nothing authored | You want a sub-second CI gate with zero contract tests to write or maintain |\n| **[Pact](https://pact.io)** | Consumer-driven contract tests executed at runtime, shared via a broker | Request/response **payload** mismatches between specific consumer–provider pairs | Contract tests written on both sides, a Pact broker, provider verification builds | Independent teams need payload-level guarantees and a can-I-deploy workflow |\n| **[openapi-diff](https://github.com/OpenAPITools/openapi-diff)** | Diffs two OpenAPI documents | Breaking changes between two **spec versions** | Accurate specs for both versions; doesn't read source code | Your API surface is spec-first and you want to gate spec changes |\n| **[Optic](https://www.useoptic.com)** | Tracks your OpenAPI spec over time and diffs every change in CI | Breaking changes and style/standards violations in the **spec's history** | An OpenAPI spec kept in the repo | You govern an evolving spec and want each PR's API changes reviewed |\n| **[Schemathesis](https://schemathesis.io)** | Property-based fuzzing of a **running** API against its OpenAPI/GraphQL schema | Server crashes, schema violations, undocumented responses at runtime | A bootable backend + a schema | You want runtime conformance and robustness testing of the implementation |\n\nThey compose: tieline catches FE↔BE drift statically in every PR, `tieline doctor`\nkeeps code↔spec honest, and a runtime tool like Pact or Schemathesis can guard the\npayload/behaviour layer underneath.\n\n---\n\n## Quick start\n\nInstall (or run with `npx`):\n\n```bash\nnpm install -g @nugehs/tieline\n# or, from a clone:  git clone … && cd tieline && npm link\n```\n\nGenerate a `tieline.config.json` — `init` sniffs the surrounding directories\n(cwd, its children, and its siblings) for known stacks and writes a ready-to-run\nconfig:\n\n```bash\ntieline init\n```\n\n```\n  tieline · init\n\n  scanning ~/code for repos…\n  ✔ client: rtk-query     → web   (roots: src/redux/apis)\n  ✔ server: nestjs        → api   (roots: src)\n\n  📝 wrote tieline.config.json\n```\n\nIt detects adapters from `package.json` deps (`@reduxjs/toolkit`, `axios`,\n`@angular/core`, `@nestjs/core`, `express`, `fastify`, `next`),\n`requirements.txt` / `pyproject.toml` (`fastapi`, `flask`), `pom.xml` /\n`build.gradle` (`spring`), and any OpenAPI doc as a fallback. Anything it can't\ndetect is written as a placeholder for you to edit. The file is always\noverwritten, so re-run it whenever your layout changes.\n\nOr write it by hand — at the root of (or above) your repos:\n\n```jsonc\n{\n  \"client\": { \"adapter\": \"rtk-query\", \"repo\": \"../web\", \"roots\": [\"src/redux/apis\"], \"basePath\": \"/api/v1\" },\n  \"server\": { \"adapter\": \"nestjs\",    \"repo\": \"../api\", \"roots\": [\"src\"], \"globalPrefix\": \"api/v1\" },\n  \"failOn\": [\"drift\"]\n}\n```\n\nRun it:\n\n```bash\ntieline check\n```\n\n```\n  tieline · contract check\n\n  ❌  2 drift  (FE calls a route the backend does not expose)\n     GET    /users/{}\n            getUser  ·  web/src/redux/apis/user-api.ts:42\n            → did you mean \"user/{}\"?            # plural vs singular — a guaranteed 404\n     PUT    /orders/{}\n            updateOrder  ·  web/src/redux/apis/order-api.ts:88\n            → path exists but as GET, not PUT    # method mismatch\n\n  ✅ 274 matched   ❌ 2 drift   ⚠️  6 unverifiable   🟡 31 unused backend routes\n```\n\n`check` exits non-zero when any `failOn` bucket is non-empty — drop it into CI and\nthe build fails the moment the two sides disagree.\n\n---\n\n## Commands\n\n```bash\ntieline init       # auto-detect nearby repos and write tieline.config.json\ntieline check      # FE↔BE drift; exits non-zero on drift (the CI gate)\ntieline list       # the full resolved contract map (every endpoint + status)\ntieline orphans    # backend routes no frontend call reaches\ntieline doctor     # code↔spec drift (see below)\n```\n\n| Flag | Effect |\n| --- | --- |\n| `--config <path>` | Path to `tieline.config.json` (default: searched upward from cwd) |\n| `--json` | Machine-readable output |\n| `--html <file>` | Self-contained visual report (see [Visual report](#visual-report)) |\n| `--no-fail` | Always exit 0 (report only) |\n\n---\n\n## Supported stacks\n\nAny client adapter pairs with any server adapter — the matcher never changes.\n\n| Client (calls) | Server (routes) |\n| --- | --- |\n| `rtk-query` — Redux Toolkit Query | `nestjs` — decorators |\n| `axios-fetch` — axios / fetch, React Query & SWR `queryFn`s | `express` — `app.use()` mount graph, cross-file |\n| `angular-http` — Angular `HttpClient` | `fastify` — verb shorthand + `route({})` |\n| | `next` — file-based (app router + pages API) |\n| | `fastapi` — `APIRouter` prefix + `include_router` |\n| | `flask` — blueprints + `methods=[]` |\n| | `spring` — `@RequestMapping` + `@*Mapping` |\n| | `openapi` — **universal**: any OpenAPI 2/3 doc (file or URL) |\n\nThat covers **MERN** (rtk/axios ↔ express), **MEAN** (angular ↔ express), **MEVN**\n(axios ↔ express), **Next** full-stack, **Python** (fastapi/flask), and\n**enterprise** (angular ↔ spring) — plus `openapi` for any backend that emits a\nspec (Express+swagger-jsdoc, FastAPI, Spring springdoc, .NET Swashbuckle, …).\n\nNotes:\n\n- **`express`** walks the `app.use()` mount graph across `require`/`import`\n  boundaries and nested routers; routers it can't reach are flagged, never dropped.\n- **`next`** is file-system routing — app-router files export `GET`/`POST`/…;\n  pages-router handlers serve any verb (matched as `ALL`).\n- Runtime-built urls (e.g. `` `users/${id}?x=${q}` ``) are surfaced as\n  **unverifiable** rather than guessed.\n\n---\n\n## Configuration\n\n`tieline.config.json` — repo paths resolve relative to the config file.\n\n```jsonc\n{\n  \"client\": {\n    \"adapter\": \"rtk-query\",      // rtk-query | axios-fetch | angular-http\n    \"repo\": \"../web\",\n    \"roots\": [\"src/redux/apis\"], // dirs to scan for call sites\n    \"basePath\": \"/api/v1\"        // stripped from call sites before matching\n  },\n  \"server\": {\n    \"adapter\": \"nestjs\",         // nestjs | express | fastify | next | fastapi | flask | spring | openapi\n    \"repo\": \"../api\",\n    \"roots\": [\"src\"],\n    \"globalPrefix\": \"api/v1\",    // stripped from routes before matching\n    \"spec\": \"openapi.json\"       // openapi adapter & `doctor` only — file path or URL\n  },\n  \"ignore\": [\"internal/.*\"],     // regexes on the normalized path\n  \"failOn\": [\"drift\"]            // buckets that make `check` exit non-zero\n}\n```\n\n---\n\n## Visual report\n\n`tieline check --html report.html` writes **one self-contained file** (inline\nCSS/JS, no external assets) you can open in any browser or attach to a PR:\n\n- a **contract-flow diagram** — frontend resources on the left, backend on the\n  right, curved links coloured green (matched) / red (drift), with a `∅ no route`\n  node catching calls that land nowhere; hover a resource to highlight its links\n- a health ring, summary cards, and live-filterable drift / unverifiable / unused\n  tables\n\n---\n\n## `tieline doctor` — does your code match your published docs?\n\n`doctor` diffs routes parsed from source (a native adapter like `nestjs`) against\nthe routes declared in your OpenAPI spec (`server.spec`):\n\n```\n  tieline · doctor   code (nestjs)  ↔  spec (http://localhost:9999/doc-json)\n\n  ❌  4 undocumented  (in code, missing from the published spec)\n     GET    /billing/invoices       src/billing/billing.controller.ts:54\n     POST   /webhooks/stripe        src/webhooks/webhooks.controller.ts:21\n     ...\n  👻  1 phantom  (in the spec, no matching route in code)\n\n  ✅ 312 agree   ❌ 4 undocumented   👻 1 phantom   (316 code routes, 313 spec routes)\n```\n\n- **undocumented** — working routes invisible to anyone generating an SDK or\n  partner integration from the spec.\n- **phantom** — the spec promises a route the code no longer serves (stale docs).\n\nRun it alongside `check` and your spec can never silently drift from your code.\n\n---\n\n## Use as an MCP server\n\ntieline ships an [MCP](https://modelcontextprotocol.io) server so an agent can ask\n\"do these two repos still agree?\" and get the same structured result the CLI\nproduces — **no LLM does the analysis, the deterministic engine does**. It's still\nzero-dependency: the server is hand-rolled stdio JSON-RPC, no SDK.\n\nRegister it with any MCP client (Claude Code, Claude Desktop, …):\n\n```jsonc\n{\n  \"mcpServers\": {\n    \"tieline\": { \"command\": \"npx\", \"args\": [\"-y\", \"-p\", \"@nugehs/tieline\", \"tieline-mcp\"] }\n  }\n}\n```\n\nOr, from a global install (`npm i -g @nugehs/tieline`), just `\"command\": \"tieline-mcp\"`.\n\nIt exposes five tools, each returning JSON:\n\n| Tool | Args | Returns |\n| --- | --- | --- |\n| `tieline_check` | `config?` | totals + `drift` + `unverifiable` (the drift gate) |\n| `tieline_list` | `config?` | the full resolved contract map |\n| `tieline_orphans` | `config?` | backend routes no frontend call reaches |\n| `tieline_doctor` | `config?` | `undocumented` + `phantom` (code ↔ spec) |\n| `tieline_init` | `cwd?` | auto-detect nearby repos, write a config |\n\n`config` defaults to searching upward from the server's working directory, exactly\nlike the CLI — so an agent dropped into a repo with a `tieline.config.json` can\njust call `tieline_check`.\n\n---\n\n## Architecture\n\nEach side implements a single extractor; everything downstream is shared.\n\n```\nClientAdapter.extract() → Endpoint[] { method, rawPath, resolvable, file, line }\nServerAdapter.extract() → Route[]    { method, rawPath, file, line }\n```\n\nA new framework is a new adapter (~80 lines) — the normalizer, matcher, reporters,\nand CLI never change. Path-existence drift ships today; OpenAPI **DTO-shape**\ndiffing (`--deep`) and SARIF/PR annotations are on the roadmap.\n\n---\n\n## Tests\n\n```bash\nnpm test    # node --test — zero dependencies, nothing to install\n```\n\n71 tests on Node's built-in runner:\n\n- **normalize** — every param syntax (`${id}`/`:id`/`<int:id>`/`[id]`/`{id}`),\n  query stripping, basePath, path joining\n- **matcher** — all four buckets, drift hints (method-mismatch, \"did you mean\"),\n  `ignore`, `ALL`/`ANY` any-verb routes, cross-syntax param matching\n- **adapters** — every client + server adapter against a fixture, plus edge cases\n  via throwaway temp repos (Express `app.all` + unmounted router, Next route groups\n  + catch-all, Spring `@RequestMapping(method=…)`, Flask default-GET, runtime urls\n  → unverifiable, non-HttpClient `.get()` ignored)\n- **openapi** — `servers[].url` prefix, Swagger 2 `basePath`, `stripPrefix`\n- **doctor** — undocumented / phantom / matched + hints\n- **init** — stack auto-detection (node deps, Python, Spring, OpenAPI fallback),\n  dir-name bias, sibling/child scanning, placeholder fallback, config round-trip\n- **integration** — three cross-stack proofs (RTK↔Express, Angular↔Spring,\n  axios↔FastAPI) and the real CLI (exit codes, `--json`, `--html`, `doctor`)\n- **mcp** — the stdio JSON-RPC server: handshake, `tools/list`, a real\n  `tieline_check` over a fixture, in-band tool errors, method-not-found\n\n---\n\n## Roadmap\n\n- **SARIF output** — inline drift annotations on GitHub PRs\n- **`--deep`** — diff request/response **DTO shapes**, not just paths, via OpenAPI\n  (catches a renamed field or changed enum, where the expensive bugs live)\n- **More adapters** — `react-query`/`swr` first-class clients, `koa`/`django` servers\n- **AST extraction** — replace regex parsing for exotic declarations\n\nKnown limits today: regex-based extraction (robust on conventional code), one\n`@Controller` per file, path/method existence only. GraphQL is out of scope.\n\n---\n\n## License\n\nMIT © Segun Olumbe\n\n---\n\n## Part of the toolchain\n\n**tieline** is one of four tools that form a deterministic trust layer for AI-assisted development. Each answers a question people keep handing to an LLM — with static analysis instead.\n\n- [repoctx](https://www.npmjs.com/package/@nugehs/repoctx) — context: what does this change actually touch?\n- **tieline** (this tool) — contracts: did the front end and back end quietly stop agreeing?\n- [bouncer](https://www.npmjs.com/package/@nugehs/bouncer) — compliance: could you defend this to Ofcom?\n- [aiglare](https://www.npmjs.com/package/@nugehs/aiglare) — governance: where can the model do something you can't undo?\n\nMore at [segunolumbe.com](https://segunolumbe.com). *static analysis, never the model.*\n",
  "bytes": 15116,
  "sha": "bb33676d5ec01998156c6bf284155b1167c93a5484c56c1593e466a7b0e7347e",
  "repo_slug": "nugehs/tieline",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_nugehs_tieline_6bddbb7d/readme"
}