Quickstart
Three commands read a project and print what it does on every path.
npx @suss/cli init
npx @suss/cli extract -o summaries/code.json
npx @suss/cli inspect summaries/code.jsonA fourth, npx @suss/cli map, writes the whole project as one page you can open in a browser.
suss does not run your code, and you do not have to annotate anything first. The output below comes from a small Hono API with a Prisma schema behind it, on Prisma 5.22.0, which is what ^5.0.0 installs.
package.json:
{
"name": "orders-api",
"private": true,
"type": "module",
"dependencies": {
"@prisma/client": "^5.0.0",
"hono": "^4.0.0"
},
"devDependencies": {
"prisma": "^5.0.0"
}
}prisma/schema.prisma:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Order {
id Int @id @default(autoincrement())
reference String @unique
total Int
placedAt DateTime
}src/api.ts:
import { PrismaClient } from "@prisma/client";
import { Hono } from "hono";
const app = new Hono();
const db = new PrismaClient();
app.get("/orders/:reference", async (c) => {
const order = await db.order.findUnique({
where: { reference: c.req.param("reference") },
select: { id: true, reference: true, total: true },
});
if (!order) {
return c.json({ error: "no such order" }, 404);
}
return c.json(order);
});
app.post("/orders", async (c) => {
const body = await c.req.json();
if (typeof body.total !== "number") {
return c.json({ error: "total is required" }, 400);
}
const order = await db.order.create({
data: {
reference: body.reference,
total: body.total,
placedAt: new Date(),
},
});
return c.json({ reference: order.reference }, 201);
});
export default app;Install the project
npm install
npx prisma generateThe Prisma pack knows a query by the type of the client it runs on, so it needs the client that prisma generate writes. Without it, extract lists prisma under Pack health as having read nothing, and nothing pairs with the schema.
Work out which packs the project needs
npx @suss/cli initsuss reads code through one pack per library. init looks at your dependency manifest and at the schemas and deploy templates on disk, then lists what it can read:
Your code
hono hono in dependencies
fetch TypeScript sources, and fetch reads what the language itself ships
What your code reaches
prisma @prisma/client in dependencies
node TypeScript sources, and node reads what the language itself ships
Declared contracts
prisma a Prisma schema at prisma/schema.prismaThen it prints the commands for what it found. If you are sitting at a terminal, it offers to set them up for you. If the output is piped somewhere or the run is in CI, it prints the commands and stops.
suss extract -f hono -f fetch -f prisma -f node -o summaries/code.json
suss contract --from prisma prisma/schema.prisma -o summaries/prisma.jsonRead the code
npx @suss/cli extract -f hono -f fetch -f prisma -f node -o summaries/code.jsonWrote 2 summaries to summaries/code.json in 0.24sEach summary describes one unit of code that suss read. Here each unit is a route handler. In another project it might be a queue consumer or a Lambda. The file is JSON, and inspect prints it in a form you can read.
Those -f flags are the ones init printed. If you leave them off, extract reads the packs from suss.json, and when there is no such file it picks the same ones init would have. When suss.json lists more than one read, such as a Python API and a TypeScript client, a bare extract runs all of them and prints one array. Add -o summaries/code.json to write that array to a file, or --out-dir summaries to get one file per read.
Print what it found
npx @suss/cli inspect summaries/code.jsonsrc/api.ts
├─ GET /orders/{reference} (hono handler | line 7)
│ if !db.order.findUnique()
│ -> 404 { error }
│ + c.req.param
│ + db.order.findUnique
│ + reads postgresql:Order
│ else
│ -> 200 { id, reference, total }
│ + c.req.param
│ + db.order.findUnique
│ + reads postgresql:Order
│
└─ POST /orders (hono handler | line 20)
if typeof body.total !== "number"
-> 400 { error }
+ c.req.json
else
-> 201 { reference }
+ c.req.json
+ db.order.create
+ writes postgresql:Order
2 summaries.Under each route you get the branches it takes, the condition that leads to each one, and the status and body it returns there. The + lines are the calls that branch makes, and the tables it reads and writes. The 200 body lists the three columns the query selects, and POST /orders only writes the Order table on the branch that returns 201.
See the whole project on one page
npx @suss/cli mapsuss map wrote .suss/map.html
Routes 2 · 1 write a table
Tables 1
Env vars 0
Checked 3 pairs · 1 finding
Not followed 0 calls in 0 functions · 0 routes affectedmap reads the project, runs check over it, and writes one HTML page with what it found: the routes and the tables they touch, the env vars, what check compared, and any calls suss could not follow. Open .suss/map.html in a browser. It loads nothing from the network, so it works from disk and you can send it to someone as a file. The lobsters example is the same page for a bigger project. suss map lists the flags.
Compare it against the schema
check needs both sides of a boundary in the same folder. init already printed the command that turns schema.prisma into the other side of every Prisma query, and this project has a caller of its own too.
src/client.ts:
export async function loadOrder(reference: string) {
const response = await fetch(`/orders/${reference}`);
if (response.status === 200) {
return { state: "ready", order: await response.json() };
}
return { state: "error" };
}npx @suss/cli extract -f hono -f fetch -f prisma -f node -o summaries/code.json
npx @suss/cli contract --from prisma prisma/schema.prisma -o summaries/prisma.json
npx @suss/cli check --dir summaries/ --allCompared 2 of 3 boundaries:
GET /orders/{reference}
orders-api::src/api.ts::get#GET /orders/{reference} <-> orders-api::src/client.ts::loadOrder
postgresql:Order
prisma/schema.prisma::Order <-> orders-api::src/api.ts::get#GET /orders/{reference}
prisma/schema.prisma::Order <-> orders-api::src/api.ts::post#POST /orders
Providers with no client to compare against:
POST /orders
orders-api::src/api.ts::post#POST /orders
────────────────────────────────────────────────────────────
[WARNING] unhandledProviderCase
Provider produces status 404 but no consumer branch handles it
provider: src/api.ts::get (src/api.ts:7)
consumer: src/client.ts::loadOrder (src/client.ts:1)
boundary: hono (http) GET /orders/:reference
to silence this one, add to the rules in .sussignore.yml:
- kind: unhandledProviderCase
boundary: "GET /orders/{reference}"
provider: { transitionId: "get:response:404:ca40ca7" }
reason: TODO say why you accept this
────────────────────────────────────────────────────────────
[WARNING] boundaryFieldUnused
Order declares "placedAt" and code here writes to it, but no query reads it. suss counts a column as read only when a query selects it or filters on it, so before you treat the write as pointless, look for code that takes "placedAt" off a record it already fetched.
provider: prisma/schema.prisma::Order (prisma/schema.prisma:1)
consumer: prisma/schema.prisma::Order (prisma/schema.prisma:1)
boundary: prisma (postgresql)
────────────────────────────────────────────────────────────
2 findings: 0 error, 2 warning, 0 infoTwo boundaries had both sides in this run. At the route, loadOrder treats every status other than 200 the same way, so a missing order and a failed request both end up on the screen as one generic error. At the table, both handlers are compared against the schema, and nothing in the project selects or filters on placedAt after POST /orders writes it. POST /orders has no caller here, so suss compared nothing across that route. An empty finding list on its own does not mean the two sides agreed.
Every finding gives you the boundary, both sides, a file and line to open, and a rule you can paste if you decide to live with it. The findings catalog explains what each kind means.
Next
- Add suss to a project for a repository with more than one service, or a language other than TypeScript.
- Fix a run that found nothing if
initdid not match a pack, orextractcame back empty. - Read a pull request diffs two of these runs and posts what changed as a comment.
- Give your agent suss lets a coding agent ask the same questions over MCP.
- Four ideas explains the four words the rest of the documentation leans on.