Add suss to a project
Install suss in a repository you already have and get a first answer out of it. You do not annotate anything or start any servers. suss writes a folder of summary files and a small suss.json to disk, and nothing else.
npm install --save-dev @suss/cli
npx @suss/cli initEverything ships inside @suss/cli, so there is one install, and you refer to each pack by name after that.
What init finds
init reads your dependency list, looks for schemas and deploy templates on disk, and prints what it can read. On a small Express service with a web client in the same repository:
✓ Found 3 things to read in articles
Your code
express express in dependencies
fetch TypeScript sources, and fetch reads what the language itself ships
What your code reaches
node TypeScript sources, and node reads what the language itself shipsThen it asks five questions, one at a time:
- Install N packages as devDependencies? Yes by default. If npm fails it stops there, prints what npm said, and leaves you the command.
- Read the code now and compare what it finds? Yes by default. It runs
extract,contractandcheckfor you. - Add a
.sussignorefor findings you decide to accept? No by default. - Add a GitHub Actions workflow that runs this on every pull request? No by default.
- Write
suss.json, so later runs know what this project declares? Yes by default.
init writes nothing to disk unless you say yes.
If the output is piped, if the run is in CI, or if you pass --plain, init prints the commands instead of asking:
npx @suss/cli init --plain1. Install suss
npm install --save-dev @suss/cli
2. Read each side into one folder
suss extract -f express -f fetch -f node -o summaries/code.json
3. Compare them
suss check --dir summaries/Two more steps follow: accepting a finding, and running the same two commands in CI.
What init writes
init writes suss.json at the repository root. It lists which packs this project needs and which documents it declares:
{
"version": 1,
"read": [
{
"kind": "extract",
"language": "typescript",
"project": "tsconfig.json",
"packs": ["express", "fetch", "node"]
},
{
"kind": "contract",
"from": "openapi",
"file": "openapi.yaml"
}
]
}Commit it. It records what the project contains, which is the same for everybody working on it. With the file committed, a later run tells you when a document stops being compared, so the document does not go unpaired without anyone noticing.
init can also write a .sussignore with one example rule, as .sussignore.json (Accept a finding has the syntax) and .github/workflows/suss.yml (Run suss in CI has the whole thing). Both are off by default.
Run it
With suss.json in place, extract, inspect and check need no flags. Each one prints the command it worked out before it runs it:
npx @suss/cli checkReading what suss.json says.
suss extract --lang typescript -p tsconfig.json -f express -f fetch -f node
suss contract --from openapi openapi.yamlIf there is no suss.json, they pick what init would have picked and print that, so your first run works before you have written anything down:
No suss.json in articles, so this reads what `suss init` would pick. Run `suss init` to write that down.
suss extract --lang typescript -p tsconfig.json -f express -f fetch -f nodeRead the first run
inspect describes what each unit does, with no findings to triage:
src/articles.ts
└─ GET /articles/{slug} (express handler | line 14)
if !db.bySlug()
-> 404 { error }
+ db.bySlug
elif db.bySlug().archivedAt
-> 410 { error }
+ db.bySlug
else
-> 200 { id, slug, title }
+ db.bySlug
Could not follow:
The call to db.bySlug lands on a declaration with no body, so whatever runs there is missing from this summarycheck compares the two sides of each boundary and prints what disagrees:
Compared 1 boundary.
────────────────────────────────────────────────────────────
[WARNING] unhandledProviderCase
Provider produces status 404 but no consumer branch handles it
provider: src/articles.ts::get (src/articles.ts:14)
consumer: web/articleView.ts::loadArticle (web/articleView.ts:1)
boundary: express (http) GET /articles/:slug
to silence this one, add to the rules in .sussignore.yml:
- kind: unhandledProviderCase
boundary: "GET /articles/{slug}"
provider: { transitionId: "get:response:404:6405be7" }
reason: TODO say why you accept this
────────────────────────────────────────────────────────────
[WARNING] unhandledProviderCase
Provider produces status 410 but no consumer branch handles it
provider: src/articles.ts::get (src/articles.ts:14)
consumer: web/articleView.ts::loadArticle (web/articleView.ts:1)
boundary: express (http) GET /articles/:slug
to silence this one, add to the rules in .sussignore.yml:
- kind: unhandledProviderCase
boundary: "GET /articles/{slug}"
provider: { transitionId: "get:response:410:c3d4fe5" }
reason: TODO say why you accept this
────────────────────────────────────────────────────────────
2 findings: 0 error, 2 warning, 0 info
suss met a call it could not follow in one unit, of 2, so that one is described in part. `suss inspect` says which calls.Errors fail the run and warnings do not, so a first pass over an old codebase does not have to be all or nothing. --all lists every pair suss made and every boundary it skipped, so you can tell "no findings" apart from "nothing got compared". --at src/articles.ts:14 narrows a run to one file, line, boundary or summary.
To see the whole project at once, run npx @suss/cli map. It writes .suss/map.html, a page of every route, table and env var, what check compared, and the calls suss could not follow. suss map has the details.
When a run turns up nothing, every command prints where it stopped. Fix a run that found nothing goes through each case.
Choose the packs yourself
-f picks a pack to read with, and you can repeat it. -p points at the tsconfig that covers the code you want read, which gives suss the same type resolution your compiler has, including your paths aliases and your lib set. Without it, an import that crosses a package boundary does not resolve, and most of the type information is lost.
# The service
npx @suss/cli extract -p tsconfig.json -f express -o summaries/api.json
# The web client that calls it, from its own tsconfig
npx @suss/cli extract -p apps/web/tsconfig.json -f fetch -o summaries/web.jsonsuss contract --from reads a document you already keep and produces summaries in the same format:
npx @suss/cli contract --from openapi openapi.yaml -o summaries/contract.json
npx @suss/cli contract --from cloudformation template.yaml -o summaries/infra.jsonThe pack catalog lists every -f name and every --from source with what each one reads.
A Python or Ruby project has no tsconfig, so point suss at the directory with --dir instead. Read Python or Ruby covers both.
Where the files go
summaries/ is derived, so leave it out of the repository and let CI regenerate it. The exception is a library that publishes its summaries for whoever consumes it, and Publish summaries covers that.
Commit both suss.json and .sussignore. suss.json records what the project contains. .sussignore lists the findings your team decided to accept.
Next
At a repository root with a workspace declaration, init asks which packages to set up. Work across services covers that, and what happens when two services serve the same path.
After the first run, Adopt it step by step goes from reading one service to gating pull requests.