Onus docs
Kinds of change · Onus docs

Kinds of change

Every kind and subkind of change Onus reports

Every row has a kind (one of seven), a subkind (the specific change) and a level (relationship, behavior or structure). The Kind column of the Markdown report shows a short label; the JSON has kind and subkind.

KindMeaning
security-sensitiveData leaves the system in a new way, a secret was committed, authentication or authorization code changed, a new HTTP route or public API operation appeared, or code in a sensitive component changed how it decides or writes. Always needs a person.
breakingCallers or consumers outside this change may break: a contract lost something or changed in a way Onus cannot prove compatible and some user was not updated, an API operation or route was removed, or a boundary rule was broken. Always needs a person.
dependencyThird-party code changed: a new, upgraded or removed npm package, a lockfile, or a patched package (which needs a person).
configConfiguration changed. CI, policies, onus.yaml and CODEOWNERS are also rules of the game. Migrations, root build config and infrastructure need a person.
additiveSomething new that existing callers do not notice: an optional field, a new export, a new event consumer, a new read; also a contract change whose users were all updated in the same change.
testTests were weakened (rules of the game), removed together with the code they tested, or edited to match behavior that changed (--behavior).
internalEverything else, collapsed into one row per component, plus renames, generated code and notable edits outside sensitive components.
"Sensitive component" means a component with a label of medium or high sensitivity in onus.yaml; payments, auth and pii are high unless onus.yaml says otherwise.

Relationships between components, data and the outside world

These are compared per component, so moving code around inside a component never looks like a new relationship.

SubkindKindWhen
new-external-servicesecurity-sensitiveA component starts calling an external service that is new to the repository, or that sends a kind of data (phone numbers, email addresses, payment details, ...) the component never sent before. Its novelty lists new-vendor:<vendor> and new-data-egress:<kind>. A new npm package that is the service's SDK is folded into this row (new-dependency:<package>, and new-package:<package> when no other component used it).
new-external-calladditiveA component starts calling a service another component already uses, sending nothing new.
external-service-removedinternalA component stops calling a service.
new-event-consumeradditiveA component starts subscribing to an event. The row says whether the publishers also changed; with no publisher in the map it is low confidence.
new-event-publisheradditiveA component starts publishing an event; the row names the components that consume it.
event-consumer-removedbreakingA component stops subscribing to an event.
event-publisher-removedbreakingA component stops publishing an event others still consume (internal when nobody consumes it).
new-data-writesecurity-sensitive or additiveA component starts writing a table (Prisma). Security-sensitive in a sensitive component.
new-data-readadditiveA component starts reading a table.
data-write-removed, data-read-removedinternalA component stops writing or reading a table.
new-cross-component-dependencyadditiveA component starts depending on another component's code. The row names what it uses, and says so when that is internal to the other component.
rule-violationbreakingA declared boundary rule is broken by a new reference (see onus help configuration). Rules of the game. Not reported when the same violation existed before.

Contracts

Contracts are the public symbols of a component: everything reachable from its entrypoint through exports and re-exports. Shapes come from the declared types; a type that is inferred rather than written is "unverified".

SubkindKindWhen
contract-field-added-optionaladditiveAn optional field or member is added (phoneVerified?: boolean).
contract-member-addedadditiveAn enum member or a class method or property is added.
contract-param-added-optionaladditiveAn optional or rest parameter is added.
contract-param-now-optionaladditiveA required parameter becomes optional.
contract-key-addedadditiveA public value without a type annotation gains a key: its initializer is an object literal, or a call such as Schema.struct({...}) or z.object({...}), and the object gains a key.
contract-type-widenedadditiveA field, method or result type changes only by gaining optional parameters or optional fields ((p: string) => R becomes (p: string, opts?: O) => R); callers and implementations keep compiling.
contract-union-widenedadditiveA union type accepts more members ('A' \'B' becomes 'A' \'B' \'C').
export-addedadditiveA symbol becomes public. Several new exports of one component are one row.
contract-field-added-requiredbreakingA required field or method is added to an interface or type, and an implementation outside this change was not updated.
contract-field-removedbreakingA field, member or method is removed.
contract-field-now-requiredbreakingAn optional field becomes required.
contract-field-now-optionalbreakingA required field becomes optional: readers may now get undefined.
contract-field-now-readonlybreakingA field becomes readonly.
contract-param-added-requiredbreakingA required parameter is added.
contract-param-removedbreakingA parameter is removed.
contract-param-now-requiredbreakingAn optional parameter becomes required.
export-removedbreakingA public symbol is removed or no longer exported.
contract-key-removedbreakingA public value without a type annotation loses a key of its initializer.
contract-union-narrowedbreakingA union type no longer accepts some members.
contract-changed-unverifiedbreakingA type changed in a way Onus cannot prove compatible: a parameter, return, field or key type, or type parameters. Types are compared with object members and union members sorted, so reordering (as generators do) is no change.
contract-inferred-changedinternalPublic symbols whose types are inferred, not written, changed their bodies, and Onus has nothing to compare. One row for all of them, low confidence.
A contract change is breaking only when something outside the change can break: a file that uses the symbol and was not changed, or, for a new required member, an implementation that was not changed (code that only reads the type is unaffected). Breaking rows name those files, test files first. Implementations are found by how they are written: implements X, satisfies X, : X = {...}, a function declared to return X, and Effect's Layer.succeed(X, ...) and X.of({...}). A cast (as X) is not an implementation: it compiles whatever members X gains.

When every user was updated in the same change, or nothing outside it uses the symbol, the row keeps its subkind but is additive ("Contract change, users updated" or "no users affected") and does not need a person. It stays breaking when Onus cannot see every user: the package is published (its package.json is not private and has files or publishConfig), or some imports of it could not be resolved.

Renaming a parameter is not a contract change. When a contract declared in onus.yaml has invariants, the row repeats them.

A contract declared in onus.yaml is a rule of the game. When its code changes (it is added or removed, or the body, shape or value of it or one of its members differs; formatting, comments and moves do not count), every row about it is marked rulesOfTheGame and needs a person. When no row is about it, for example a body-only edit, Onus adds one:

SubkindKindWhen
declared-contract-editedinternalThe code of a contract declared in onus.yaml changed and no other row is about it. Rules of the game.

Renames and moves

SubkindKindWhen
renameinternalA symbol is renamed with an identical body. The row counts the call sites and files updated.
rename-incompletebreakingThe same, but some references still use the old name.
moved-between-componentsinternal or breakingSymbols moved from one component to another: one row per pair of components ("17 symbols moved from orders-core to billing-shared"). Exported symbols edited while moving (same name and kind, most members in common) are included, and their contract changes are listed in the row; breaking when any of those changes is breaking. The row also says when the old component no longer exports them.
moved-incompletebreakingThe same, but some references still point at the old place.
A symbol or file that moved inside one component without other changes, and a file whose only changes are formatting, comments or import paths, is not a row: it is listed under "Structure only".

Dependencies

SubkindKindWhen
new-dependencydependencyA package.json gains a third-party package. Novelty new-package:<name> when no other component used it. A package new to the repository that several components add is one row, and new packages added to the same components share a row.
dependency-version-changeddependencyA declared version range changed.
dependency-removeddependencyA package is removed.
lockfile-changeddependencyA lockfile changed while no manifest dependency did. The row lists the installed versions that change (read from pnpm-lock.yaml, package-lock.json and yarn.lock), or says that none do when only patch hashes, checksums or resolution changed.
dependency-patcheddependencyA patch of a third-party package is added, changed or removed (patches/*.patch for pnpm and patch-package, .yarn/patches/). The row names the package and the files the patch edits. Needs a person: installed code changes without a version change, and the patch stops applying when the version moves.
Workspace packages (your own @scope/... packages) are not third-party: depending on one shows as a relationship between components.
SubkindKindWhen
external-api-first-useinternalProduction code uses an API of a package the repository already depends on (Effect.retry from effect) that no other code in the repository uses. The row names the version the repository pins, to check the API exists in it: code written from memory of another version often calls functions that are not there. Node's built-in modules and framework virtual modules ($app/…, $env/…) are left out, and an API counts as used when the repository already reaches it another way (RawData imported by name, WebSocket.RawData through the default import).

Risk classes

Some changes need a person whatever their size or shape, so they get their own row instead of disappearing into a component's changed lines. Onus finds them by fixed patterns in the changed files.

SubkindKindWhen
migration-changedconfigA database migration is added, edited or removed: versioned files in a migrations, migration or migrate folder, *.migration.*, Flyway V1__name.sql, Drizzle drizzle/0001_name.sql, Alembic versions, and versioned upgrade commands (upgrade…/…command…<timestamp>…). The row says what the migration does to stored data, read from its SQL and TypeORM calls (creates or drops tables, adds or drops columns, creates indexes, deletes or updates rows, grants), and mentions ON DELETE CASCADE. The down step is left out. Editing a migration that already ran is called out. Snapshots and journals the tool writes beside it (drizzle/meta/, Prisma's migration_lock.toml) are part of the migration, and other code in a migrations folder gets a row only when it touches stored data.
public-api-changedsecurity-sensitive, additive or breakingGraphQL queries, mutations or subscriptions are added, removed or change their signature, in schema files (.graphql, .gql), gql templates or code-first resolvers (@Query, @Mutation, @Subscription). Operations that skip authentication (@skipAuth, @Public() and similar) get their own security-sensitive row. Always needs a person.
route-addedsecurity-sensitiveNew HTTP routes: SvelteKit +server files, Next.js route files and pages/api, and controller decorators (@Controller('users') with @Get(':id')). The row names the methods and paths, and the cache-control header the route sets.
route-removedbreakingHTTP routes removed.
auth-code-changedsecurity-sensitiveCode changed in files whose names say they decide who may do what (auth, permission, guard, policy, role, acl, jwt, oauth, password, …, also in camelCase names). One row per component, even for a refactor.

Configuration and rules of the game

SubkindKindRules of the gameFiles
ci-changedconfigyes.github/workflows/*, .gitlab-ci.yml, .circleci/, .buildkite/, Jenkinsfile
onus-config-changedconfigyesonus.yaml (the report used the base version)
lanes-model-changedconfigyes*lanes-model.json, the learned lane model (the lane used the base version)
onus-pack-changedconfigyesa framework pack listed in onus.yaml (the report used the base version)
codeowners-changedconfigyesCODEOWNERS
policy-changedconfigyes*.rego, policy/, policies/
test-config-changedconfigyesjest, vitest, playwright, cypress, karma and mocha configs
repo-settings-changedconfigyesanything else under .github/
root-build-config-changedconfigneeds a personat the repository root: tsconfig*.json, babel config, nx.json, turbo.json, .npmrc, .nvmrc, .node-version, .tool-versions
infrastructure-changedconfigneeds a personTerraform (.tf, .tfvars, .hcl), Kubernetes manifests (YAML with apiVersion and kind, or YAML in k8s, helm, charts, deploy, manifests, infra folders), Chart.yaml, serverless, fly, vercel, netlify, render, wrangler, cdk, Pulumi and a root app.yaml
env-changedconfigno.env*
container-changedconfignoDockerfile, docker-compose, compose.*
data-schema-changedconfigno*.prisma
workspace-config-changedconfignopnpm-workspace.yaml, .yarnrc.yml, lerna.json
build-config-changedconfignotsconfig*.json, project.json, angular.json, ESLint, Prettier, Biome and SWC configs, and bundler, framework and codegen configs (vite, webpack, rollup, tsup, esbuild, svelte, next, nuxt, astro, babel, postcss, tailwind, codegen, …)
package-scripts-changedconfignopackage.json scripts
package-manifest-changedconfignoother package.json fields (dependencies are reported as packages)
config-changedconfignoother .yaml, .yml, .json, .toml, .ini and .properties files
Rows for a changed config file name what changed: the top-level keys of JSON and YAML (with compilerOptions broken down in tsconfig files), and the instructions of a Dockerfile (ENV BODY_SIZE_LIMIT, the base image).

Tests

SubkindKindWhen
test-weakenedtestOne row per component when any of these happen: a test case is removed; a case has fewer assertions; a case gains skip, only or todo; expected values in assertions are edited; a test file is deleted; or source code starts comparing against a string literal that only the tests use. Rules of the game.
tests-removed-with-codetestRemoved test cases or assertions that used code this change deletes. They covered nothing that still exists, so they do not weaken the tests and do not need a person.
A removed case that reappears among the change's new cases (the same body, or the same title in a file of the same name, as when tests move with their code) is neither. New test cases are not a row; the component's internal row counts them.

With onus report --behavior, the tests that reach the change also run at the base and at the head (onus help environments). What they showed is one row per component, at confidence traced:

SubkindKindWhen
behavior-newly-failinginternalA case passes at the base and fails at the head, and again when run once more; or a new case fails at the head. Needs a person, and blocks the change until one approves the row.
behavior-changedtestThe base version of a test the change edited fails against the head's code, while the head's version passes: the behavior it pinned changed, and the test was edited to match. Snapshot mismatches land here. Rules of the game.
behavior-newly-passinginternalA case fails at the base and passes at the head.
behavior-output-changedinternalA case fails at both, with a different message.

Notable edits inside functions

Found in functions and methods that exist on both sides. Security-sensitive in a sensitive component, internal otherwise. An await, guard or throw that the change adds to another function, as when a body is extracted into a helper, moved rather than disappeared, and is not reported.

SubkindWhen
boundary-condition-changedA comparison flips with the same operands: a > b becomes a >= b.
condition-constant-changedA comparison keeps its operator but compares against a different constant.
constant-changedA const or variable initialized with a literal changes value: LOYALTY_RATE from 0.1 to 0.15.
guard-removedA function has fewer early exits (if (...) return/throw) or fewer throws. An edited guard is not a removed guard.
await-removedA function awaits less, and a specific await disappeared. Awaiting calls together (await Promise.all([...])) is not a removal.
error-swallowedA new empty catch block.

Secrets

SubkindKindWhen
secret-committedsecurity-sensitiveAn added line matches a high-confidence pattern: AWS access key ID or secret access key, GitHub token, Stripe secret key, Slack token, Google API key, SendGrid API key, npm token, or a private key block. One row per file. The value is never printed, and it is redacted from the map too.

Everything else

SubkindKindWhen
internal-changesinternalThe changed lines of a component that no other row explains, with their count, labeled as code, tests or docs. In components with more than 300 files, the row also names the module folders that changed.
generated-code-changedinternalFiles written by a tool changed: found by folder (generated, __generated__, codegen), name (.generated., .gen., _pb.) or a marker in the first lines (@generated, DO NOT EDIT, "This file was generated"). One row per component that lists what the other rows would have said; review the schema or generator change it mirrors.

Grouped rows

To keep large changes readable, Onus merges rows that say the same thing:

  • Several config files of one kind in one component are one row per kind of edit ("Build config: 3 files in billing change module").
  • Packages already used elsewhere in the repository, dropped packages and version changes are one row per component ("2 npm packages already used in this repository added in c"). A package new to the repository always keeps its own row.
  • When the same kind of config or dependency change appears in more than 5 components, it is one row for all of them, and config rows stay apart by the keys they change ("Build config changes module in 262 components (445 files)").
  • When more than 10 components have internal changes, the 10 largest keep their rows and the rest are summarized in one ("1,490 changed lines inside 106 other components").
  • Several new exports of one component are one row.
Grouped rows keep every file and line in their evidence.

Hints on every row

  • labels: sensitivity labels of the component.
  • blastRadius: files in the new version (tests included) that depend on the subject, not counting the file that defines it.
  • novelty: new-vendor:<vendor>, new-data-egress:<kind>, new-dependency:<package>, new-package:<package>, secret.
  • confidence: declared (from onus.yaml), compiler (confirmed by a compiler, indexer or language server), static (read from the code), inferred (from conventions) or low (Onus could not resolve something).
  • rulesOfTheGame, intentMismatch, needsPerson.
  • dependentComponents: other components (not the row's own) that depend on what the row changed. For most rows that is the subject. For a component's internal row it is the functions and classes its changed lines are in, so a body-only edit of a widely used function counts its users. 0 for other rows about a whole component. dependents lists those components (absent when there are none).
  • tested: for rows that change code, whether tests reach the files it changes: tested (a test exercises every changed file), traced (each changed file is tested or seen in a runtime trace) or untested. Absent for rows that change no code. It is per file, not line coverage.
  • owners: the owners of the row's component, from onus.yaml or else CODEOWNERS; a row merged from several components has all of theirs. Absent when nobody owns it. Used to route a review (onus help ci), not to rank.
The lane classifier reads these hints (onus help lanes).