workspace-conformance 1.6.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,10 +7,10 @@ It complements the workspace architecture rules of [`@exadev/eslint-config`](htt
7
7
  ## Getting started
8
8
 
9
9
  ```sh
10
- pnpm add -D workspace-conformance @exadev/config cosmiconfig 'typescript@^6'
10
+ pnpm add -D workspace-conformance '@exadev/config@^2.1.0' cosmiconfig 'typescript@^6'
11
11
  ```
12
12
 
13
- `cosmiconfig` (`^9.0.0 || ^10.0.0`) is a peer dependency because `@exadev/config` loads config files through it. `typescript` (`^5.9.0 || ^6.0.0`) is a peer dependency because dependency-cruiser needs it to parse TypeScript and supports versions below 7 (the range is in the install command because an unpinned `typescript` installs a newer major, which pnpm only warns about); the import checks fail with a configuration error when dependency-cruiser cannot load it. ts-morph bundles its own compiler and does not use yours. `eslint` (`^9.0.0 || ^10.0.0`) is an optional peer dependency, needed only by the `eslint` check, which uses the copy installed in the repository it checks. The package supports the Node lines dependency-cruiser and its own dependencies support: 22.13 and later 22, 24, and 26 and later.
13
+ `@exadev/config` provides `withSections` and `layoutSection` for the config file, and the tool loads its sections with it, so install the major this package depends on (`^2.1.0`): a different major installs a second copy, whose sections and errors are not the ones the tool reads. `cosmiconfig` (`^9.0.0 || ^10.0.0`) is a peer dependency because `@exadev/config` loads config files through it. `typescript` (`^5.9.0 || ^6.0.0`) is a peer dependency because dependency-cruiser needs it to parse TypeScript and supports versions below 7 (the range is in the install command because an unpinned `typescript` installs a newer major, which pnpm only warns about); the import checks fail with a configuration error when dependency-cruiser cannot load it. ts-morph bundles its own compiler and does not use yours. `eslint` (`^9.0.0 || ^10.0.0`) is an optional peer dependency, needed only by the `eslint` check, which uses the copy installed in the repository it checks. The package supports the Node lines dependency-cruiser and its own dependencies support: 22.13 and later 22, 24, and 26 and later.
14
14
 
15
15
  Configure the checks in `exadev.config.ts`:
16
16
 
@@ -62,9 +62,9 @@ The tool reads two sections through `@exadev/config`, each from `exadev.config.t
62
62
 
63
63
  ### How the layout maps to packages
64
64
 
65
- Packages are the directories that the `packages` globs select and that hold a `package.json`. Each belongs to the group whose root is the longest prefix of its directory, and takes its rank from the first matching `nameRanks` pattern, else its group's `rank`, else `defaultRank`. A `segment` slice is the path segment below the group root; a `namePrefix` slice is the longest slice value produced by a `segment` group that prefixes the package's unscoped name. These are the same rules the ESLint architecture rules apply.
65
+ Packages are the directories that the `packages` globs select and that hold a `package.json`. The root package is one when a glob selects it (`.`), which is how a single-package repository is described: `packages: ['.']` with a group whose `path` is `.`, so `import-cycles` judges the files of the one package. Since every other package directory is inside the root, the root package cannot be a package beside others. Each belongs to the group whose root is the longest prefix of its directory, and takes its rank from the first matching `nameRanks` pattern, else its group's `rank`, else `defaultRank`. A `segment` slice is the path segment below the group root; a `namePrefix` slice is the longest slice value produced by a `segment` group that prefixes the package's unscoped name. These are the same rules the ESLint architecture rules apply.
66
66
 
67
- The tool fails loudly, with a configuration error, on what it cannot judge: a package directory no group owns, package directories nested inside one another, a rank check when some package resolves no rank, and a check that needs `rankSkip`, `isolatedGroups` or a slice when the layout has none.
67
+ The tool fails loudly, with a configuration error, on what it cannot judge: globs that select no package, a package directory no group owns, package directories nested inside one another, a rank check when some package resolves no rank, and a check that needs `rankSkip`, `isolatedGroups` or a slice when the layout has none.
68
68
 
69
69
  ## Checks
70
70
 
@@ -153,11 +153,13 @@ Code: `derived-types/not-derived`, reported at the type's declaration. Limits: a
153
153
 
154
154
  ### File tree and cross-config
155
155
 
156
- **`instruction-symlinks`** verifies that agent instruction files are symbolic links to the README beside them (written in any form that resolves there, such as `./README.md`, or through other tracked links, as `CLAUDE.md` to `AGENTS.md` to `README.md`), as git records them: mode `120000` in the index, not the working tree, so a checkout with `core.symlinks` false, which writes links out as plain files, is judged by what is committed. Options: `files` (`AGENTS.md` and `CLAUDE.md`), `directories` (globs, `.`), `target` (`README.md`). Codes: `instruction-symlinks/missing` (not tracked), `not-a-symlink`, `wrong-target` (the link does not resolve to the README, including a loop or an absolute target), `dangling` (the target is not tracked). It needs a git working tree and reads the index, so a link that is staged counts before it is committed.
156
+ **`instruction-symlinks`** verifies that agent instruction files are symbolic links to the README beside them (written in any form that resolves there, such as `./README.md`, or through other tracked links, as `CLAUDE.md` to `AGENTS.md` to `README.md`), as git records them: mode `120000` in the index, not the working tree, so a checkout with `core.symlinks` false, which writes links out as plain files, is judged by what is committed. Options: `files` (`AGENTS.md` and `CLAUDE.md`), `directories` (globs, `.`), `target` (`README.md`). Codes: `instruction-symlinks/missing` (not tracked, in a directory whose README is tracked; a directory without one has nothing for a link to point at and is not asked for one), `not-a-symlink`, `wrong-target` (the link does not resolve to the README, including a loop or an absolute target), `dangling` (the target is not tracked). It needs a git working tree and reads the index, so a link that is staged counts before it is committed.
157
157
 
158
158
  **`single-storybook`** verifies that at most one Storybook exists, in one permitted directory (the workspace root by default, or `location`), and none inside a package such as a UI package. A Storybook is a `.storybook` directory; one anywhere else is `single-storybook/misplaced`. `exclude` (globs) leaves paths out of the search, such as test fixtures; a directory name (`test/fixtures`) leaves out everything under it, as `test/fixtures/**` does. A Storybook that is started only from a `package.json` script or a dependency, without a `.storybook` directory, is not seen, and a fixture or template directory that holds one is reported unless `exclude` names it.
159
159
 
160
- **`commit-types`** verifies that the commit types commitlint accepts are accounted for in the semantic-release config. Options: `commitlint` (`commitlint.config.ts`) and `release` (`release.config.ts`). Both files are evaluated through the shared jiti loader, so a list derived from one shared constant compares as its values. The `alias` and `fsCache` of `runChecks`' `configFiles` apply to them as to the sections; `trust` and `merge` are for `extends` and do not. A commit type that no conventional preset knows (`PRESET_COMMIT_TYPES` lists the ones that do) must have a `releaseRules` entry in the `@semantic-release/commit-analyzer` options and, when `presetConfig.types` of `@semantic-release/release-notes-generator` is set, a changelog section. Every `releaseRules` type and every changelog type must be a commit type, or it can never apply. A preset type may be left out of either list, which is how a release rule list normally omits the types that do not release. Codes: `commit-types/no-release-rule`, `release-rule-not-a-commit-type`, `no-changelog-section`, `changelog-section-not-a-commit-type`, `no-type-enum` (a type no preset knows is listed but commitlint sets no `type-enum`, so the commit types cannot be read) and `missing-config`. Without `presetConfig.types` (or without a release notes generator) no changelog sections are listed, so none is required. Limits: `type-enum` is read as `[level, 'always', [types]]`, the only accepted types; `'never'` forbids the types it lists (a release rule or section for one is reported) and enumerates none, so nothing is required of any other type, and level 0 accepts every type; any other shape of the rule fails the check; `releaseRules` given as a module path (a string, which semantic-release supports) is not read and fails the check; types that come only from an extended preset are not read; rules that name no type (`{ breaking: true }`) are ignored; a changelog list that omits a preset type is not reported; the configs are executed, so they must be trusted.
160
+ **`commit-types`** verifies that the commit types commitlint accepts are accounted for in the semantic-release config. Options: `commitlint` (`commitlint.config.ts`) and `release` (`release.config.ts`). Both files are evaluated through the shared jiti loader, so a list derived from one shared constant compares as its values. The `alias` and `fsCache` of `runChecks`' `configFiles` apply to them as to the sections; `trust` and the per-shape `unified` and `standalone` layer options (`merge` and `presetSchema`) are for `extends` and do not. A commit type that no conventional preset knows (`PRESET_COMMIT_TYPES` lists the ones that do) must have a `releaseRules` entry in the `@semantic-release/commit-analyzer` options and, when `presetConfig.types` of `@semantic-release/release-notes-generator` is set, a changelog section. Every `releaseRules` type and every changelog type must be a commit type, or it can never apply. A preset type may be left out of either list, which is how a release rule list normally omits the types that do not release. Codes: `commit-types/no-release-rule`, `release-rule-not-a-commit-type`, `no-changelog-section`, `changelog-section-not-a-commit-type`, `no-type-enum` (a type no preset knows is listed but commitlint sets no `type-enum`, so the commit types cannot be read) and `missing-config`. Without `presetConfig.types` (or without a release notes generator) no changelog sections are listed, so none is required. Limits: `type-enum` is read as `[level, 'always', [types]]`, the only accepted types; `'never'` forbids the types it lists (a release rule or section for one is reported) and enumerates none, so nothing is required of any other type, and level 0 accepts every type; any other shape of the rule fails the check; `releaseRules` given as a module path (a string, which semantic-release supports) is not read and fails the check; types that come only from an extended preset are not read; rules that name no type (`{ breaking: true }`) are ignored; a changelog list that omits a preset type is not reported; the configs are executed, so they must be trusted.
161
+
162
+ **`engines-floor`** verifies that the `engines.node` range of each package is a subset of the `engines.node` range of each dependency and optional dependency it lists, and of each peer dependency whose range admits a single version, as installed, so the package admits no Node version that something it needs at run time rejects. A floor comparison is not enough: a package declaring `>=20` over a dependency requiring `^20.19.0 || ^22.13.0 || >=24` admits 20.0 to 20.18, 21 and 23, which the dependency rejects, so the test is `semver.subset(package, dependency)`. Option: `packages`, globs of the package directories in the form of the `packages` list of `pnpm-workspace.yaml` (a `!` pattern excludes, `.` is the root package); the root package and the `packages` of `pnpm-workspace.yaml`, when it exists, when omitted. Codes: `engines-floor/wider-than-dependency` (the range admits a version the dependency rejects) and `no-engines` (the package declares no range, so admits every version, while a dependency declares one), each reported at the package's `package.json` and naming the dependency and both ranges. A dependency that declares no range constrains nothing and is ignored. `devDependencies` are not judged: a consumer never installs them, and the Node the development toolchain needs is the repository's own concern, not a claim the published package makes. A dependency is found the way Node finds a package directory, in `node_modules` of the package's directory and then of each directory above it up to the working directory, following the symbolic links pnpm installs; it reads the dependency's `package.json` directly rather than resolving an entry point, since an `exports` map need not expose `package.json`. A peer dependency is judged only when its range is a single version (`1.2.3` or `=1.2.3`), by the installed copy, which must be that version or the run fails. The consumer, not the package, chooses which version of a peer to install, so the package's range holds when each Node version in it is accepted by some version the peer range admits. The copy installed in the workspace is one of those versions: if it rejects a Node version the package admits, another admitted version may accept it (`^9.0.0 || ^10.0.0` with cosmiconfig 10 requiring `^22.18 || >= 24` while cosmiconfig 9 accepts 22.13, and equally an earlier minor of a single major), and the versions that are not installed cannot be read without the registry, so judging the range by that copy reports packages that are correct. A range of one version leaves the consumer no choice, so its installed copy decides; a peer that is not judged need not be installed. A dependency that is not installed fails the run with a configuration error, because its range cannot be read; an optional dependency, or a peer dependency marked optional in `peerDependenciesMeta`, may be absent and is then skipped. A range semver cannot parse, in the package or a dependency, and options that select no package also fail the run. Limits: semver compares a range of several comparator sets set by set, so each set of the package's range must lie within one set of the dependency's, and a set that two adjacent dependency sets only cover together (`>=22` against `^22.0.0 || >=23`) is reported until it is split the same way; a peer dependency whose range admits more than one version is not judged at all, so a package can claim a Node version that no admitted version of a peer accepts; prerelease Node versions are not considered.
161
163
 
162
164
  **`dockerfile-package-manager`** verifies that a package manager version pinned in a Dockerfile is the one `packageManager` names, ignoring a `+sha` suffix on either side. Options: `dockerfiles` (`Dockerfile`, `Dockerfile.*` and `*.Dockerfile` at any depth), `exclude` (globs left out of the search) and `packageJson` (`package.json`). Only pins of the package manager `packageManager` names are compared: `pnpm@12.4.1` on a `RUN` line is compared, `npm@latest` is not. Codes: `dockerfile-package-manager/version-mismatch` (with line and column) and `no-package-manager`. A version that refers to a variable (`pnpm@${PNPM_VERSION}` or `pnpm@$PNPM_VERSION`) is compared with the value an `ARG` or `ENV` earlier in the same file gives it. Limits: it reads `<manager>@<version>` as written on a line that is not a comment; a variable the file gives no value (an `ARG` without a default, whose value the build supplies) or whose value refers to another variable is not compared; the value in effect is the latest one set before the pin, whichever build stage set it; and an unpinned `pnpm@latest` is a mismatch.
163
165
 
@@ -200,7 +202,11 @@ ESLint (`^9.0.0 || ^10.0.0`) is an optional peer dependency, needed only by this
200
202
 
201
203
  The default level lints nothing. For each sample it calls `calculateConfigForFile`, the same call the CLI's `--print-config` makes, which applies `files` and `ignores`. A file that is ignored, or that no configuration block matches, resolves to no configuration (`isPathIgnored` is that call returning nothing), and that is `eslint/not-linted`. Otherwise each required rule must be configured at the required severity: `eslint/rule-missing`, `eslint/rule-off` or `eslint/rule-too-weak`, each naming the rule and the file.
202
204
 
203
- With `lint` the check also lints the workspace through `lintFiles` and maps every message to a violation at its position. The code names the rule, `eslint/lint/<rule id>`, so `eslint/lint/no-console` and `eslint/lint/@scope/plugin/rule` are stable; a parsing error is `eslint/fatal` and a message with no rule, such as an unused disable directive, is `eslint/lint-message`. A pattern that matches no file, or only ignored files, is `eslint/nothing-linted`. Warnings are reported as well as errors, since a repository that fails its lint script on warnings (`--max-warnings 0`) is judged by what that script reports.
205
+ With `lint` the check also lints the workspace through `lintFiles` and maps every message to a violation at its position, or at the file alone when ESLint gives none, as for a parsing error that typescript-eslint's project service raises for a file outside every tsconfig. The code names the rule, `eslint/lint/<rule id>`, so `eslint/lint/no-console` and `eslint/lint/@scope/plugin/rule` are stable; a parsing error is `eslint/fatal` and a message with no rule, such as an unused disable directive, is `eslint/lint-message`. A pattern that matches no file, or only ignored files, is `eslint/nothing-linted`. Warnings are reported as well as errors, since a repository that fails its lint script on warnings (`--max-warnings 0`) is judged by what that script reports.
206
+
207
+ A clean lint says little without how much was linted, so with `lint` the check adds a note to its result (`notes` of its entry in `runChecks`' `results`, a line on standard output on the command line): how many files it linted and, when `lintPatterns` leave some out, how many other files below the working directory ESLint has a configuration for (not ignored, and matched by a configuration block) that the patterns did not reach. That second number is ESLint's own answer for each file, not a threshold. It does not see files that the config itself leaves out: a root config that ignores `packages/**` because each package lints itself with its own config lints only the root's files, and the note then counts only those.
208
+
209
+ Type-aware rules read the types of imports, so lint a workspace that is installed and built, as its own lint script does: an import of a workspace package whose declarations are not built resolves to nothing, its types are `any`, and every use of it is a `no-unsafe-*` finding that the built workspace does not have.
204
210
 
205
211
  Limits:
206
212
 
@@ -269,18 +275,28 @@ All three take `repository` (`owner/name`; the `origin` remote of the working di
269
275
  | Check | Verifies | Codes |
270
276
  |---|---|---|
271
277
  | `settings-merge-methods` | Only one merge method (`allowed`, `rebase` by default) is available: the repository allows it and no `pull_request` rule of the branch leaves it out. | `method-enabled`, `method-unavailable` |
272
- | `settings-required-checks` | A `required_status_checks` rule requires the check of each junction job (`junctionJobs`, found in the workflows selected by `workflows` and `exclude`; the check name is the job's `name`, else its id) and some rule sets strict mode. | `no-junction-job`, `no-required-checks`, `required-check-missing`, `not-strict` |
273
- | `settings-review-thread-resolution` | A `pull_request` rule requires review conversations to be resolved. | `not-required` |
278
+ | `settings-required-checks` | A `required_status_checks` rule requires the check of each junction job (`junctionJobs`, found in the workflows selected by `workflows` and `exclude`; the check name is the job's `name`, else its id) and some rule sets strict mode. | `no-junction-job`, `no-required-checks`, `required-check-missing`, `not-strict`, `rules-unavailable` |
279
+ | `settings-review-thread-resolution` | A `pull_request` rule requires review conversations to be resolved. | `not-required`, `rules-unavailable` |
280
+
281
+ A request GitHub refuses fails the run with its status and GitHub's own message (`GET /repos/o/r failed: 403 Forbidden: Resource not accessible by personal access token`), so a token without the permission is a configuration error and never a pass. One refusal is a finding instead: GitHub answers a request for a branch's rules with 403 and `Upgrade to GitHub Pro or make this repository public to enable this feature.` when the repository's plan has no rulesets, as for a private repository on a plan without them ([about rulesets](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/about-rulesets), [archived](https://web.archive.org/web/20261002054719/https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/about-rulesets)). Nothing can then require a check or resolved conversations, so `settings-required-checks` and `settings-review-thread-resolution` report `rules-unavailable` with GitHub's message, and `settings-merge-methods`, which no rule then narrows, judges the repository settings alone. A 403 is also how GitHub reports a missing permission and a rate limit ([troubleshooting the REST API](https://docs.github.com/en/rest/using-the-rest-api/troubleshooting-the-rest-api), [archived](https://web.archive.org/web/20260917193919/https://docs.github.com/en/rest/using-the-rest-api/troubleshooting-the-rest-api)), and the endpoint's documentation lists no error status, so the plan limit is told apart by the start of GitHub's message (`Upgrade to GitHub `), and every other 403 stays an error. A `GitHubClient` of your own signals the plan limit by rejecting `branchRules` with `RulesetsUnavailableError`.
274
282
 
275
283
  Limits: classic branch protection and a merge queue's own merge method are not read, so a repository that uses classic protection is reported as having no required checks; a junction job in a reusable workflow, or a name that is an expression, is not resolved.
276
284
 
277
285
  ## Command line
278
286
 
279
287
  ```sh
280
- workspace-conformance check [--cwd <directory>] [--check <name>]... [--list] [--settings]
288
+ workspace-conformance check [--cwd <directory>] [--check <name>]... [--list] [--settings] [--format <text|json|github>]
281
289
  ```
282
290
 
283
- `--cwd` is the directory that holds the config files (default: the current directory). `--check` runs only that check, which must be enabled, and may be repeated. `--list` prints every check and stops without reading any configuration. `--settings` also runs the enabled `settings-*` checks, which read the repository through the GitHub API with the token in `GITHUB_TOKEN` or `GH_TOKEN`; without it they are left out and the run is offline. Violations are printed to standard error as `file:line:column: code: message`.
291
+ `--cwd` is the directory that holds the config files (default: the current directory). `--check` runs only that check, which must be enabled, and may be repeated. `--list` prints every check and stops without reading any configuration. `--settings` also runs the enabled `settings-*` checks, which read the repository through the GitHub API with the token in `GITHUB_TOKEN` or `GH_TOKEN`; without it they are left out and the run is offline. `--format` chooses how the result is printed, and never changes the exit status:
292
+
293
+ | Format | Output |
294
+ |---|---|
295
+ | `text` | The default. Violations on standard error as `file:line:column: code: message` (`file: code: message` for one without a position), a check's notes on standard output as `check: note`, and on a clean run a line naming the checks that ran. |
296
+ | `json` | One JSON document on standard output: the `RunResult` of `runChecks`, so `results` (each `{ check, violations, notes }`), `violations` (the library's `{ code, message, file, location? }` objects, with `file` relative to `--cwd`) and `exitCode`. |
297
+ | `github` | A GitHub Actions error annotation per violation on standard output, `::error file=<file>,line=<line>,col=<column>,title=<code>::<message>` (no `line` and `col` without a position), and a `::notice` per note. The runner turns each into an annotation on the file ([workflow commands](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands#setting-an-error-message), [archived](https://web.archive.org/web/20260918001346/https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands)). `file` is relative to the directory the command runs in, the repository root in a workflow step unless it sets `working-directory`, so `--cwd` may name a subdirectory. Reserved characters are percent-encoded as `@actions/core` encodes them (`%`, carriage return and line feed everywhere, and `:` and `,` in properties; [`command.ts`](https://github.com/actions/toolkit/blob/main/packages/core/src/command.ts), [archived](https://web.archive.org/web/20250901085906/https://github.com/actions/toolkit/blob/main/packages/core/src/command.ts)). A settings violation names the repository rather than a file, so its annotation is attached to no file. |
298
+
299
+ When the checks cannot run (exit status 2), the reason is printed to standard error as text in every format.
284
300
 
285
301
  | Exit status | Meaning |
286
302
  |---|---|
@@ -294,11 +310,11 @@ workspace-conformance check [--cwd <directory>] [--check <name>]... [--list] [--
294
310
  import { runChecks } from 'workspace-conformance';
295
311
 
296
312
  const result = await runChecks({ cwd: process.cwd() });
297
- // result.results: one { check, violations } per check that ran
313
+ // result.results: one { check, violations, notes } per check that ran
298
314
  // result.violations: all of them; result.exitCode: 0 or 1
299
315
  ```
300
316
 
301
- `runChecks` loads both sections from `cwd` unless you pass `config` (the `conformance` section) or `layout`, which are validated with the same schemas as the ones it loads, and takes `checks` to run a subset, `github` (a `GitHubClient`, which switches the settings checks on) and `configFiles` (`alias`, `fsCache`, `trust`, `merge`) for how config files load. Violations never throw. It throws `ConformanceError` when the checks cannot run and `ConfigValidationError` (from `@exadev/config`) when a section is invalid. Use it in a vitest test so the conformance suite runs with the rest:
317
+ `cwd` may be absolute or relative to the working directory of the process; `runChecks` resolves it once, so every check, and every exported check function, accepts either. `runChecks` loads both sections from `cwd` unless you pass `config` (the `conformance` section) or `layout`, which are validated with the same schemas as the ones it loads, and takes `checks` to run a subset, `github` (a `GitHubClient`, which switches the settings checks on) and `configFiles` (`alias`, `fsCache`, `trust`, and the `unified` and `standalone` layer options, each a `{ merge, presetSchema }` pair) for how config files load, with the same meaning as in `loadSection` of `@exadev/config`. Violations never throw. It throws `ConformanceError` when the checks cannot run and `ConfigValidationError` (from `@exadev/config`) when a section is invalid. Use it in a vitest test so the conformance suite runs with the rest:
302
318
 
303
319
  ```ts
304
320
  import { runChecks } from 'workspace-conformance';
@@ -311,7 +327,7 @@ it('conforms to the workspace layout', async () => {
311
327
  });
312
328
  ```
313
329
 
314
- Every check is also exported as a function taking `{ cwd, options }` (and `layout` for the import checks): `aggregateMappers`, `codecPairs`, `commandTypes`, `derivedTypes`, `importUphill`, `importRankSkip`, `importCrossSlice`, `importIsolatedGroups`, `importCycles`, `instructionSymlinks`, `singleStorybook`, `commitTypes`, `dockerfilePackageManager`, `migrationsDirectory`, `eslint`, the workflow checks (`workflowJobOrdering`, `workflowSkippableJobs`, `workflowRunnerResolution`, `workflowVersionSingleSource`, `workflowCredentials`, `workflowReleaseJob`, `workflowRepositoryDispatch`, `workflowUpdateBotCooldown`, `workflowMergeGroup` and `workflowActionPinning`) and, taking `{ cwd, options, github }`, the settings checks (`settingsMergeMethods`, `settingsRequiredChecks` and `settingsReviewThreadResolution`). These take options and a layout as already validated and do not check them again; go through `runChecks` for validation. `parseWorkflow` and `loadWorkflows` expose the workflow model the workflow checks read. A check is a function that returns `Promise<readonly Violation[]>`, where a `Violation` is `{ code, message, file, location? }`.
330
+ Every check is also exported as a function taking `{ cwd, options }` (and `layout` for the import checks): `aggregateMappers`, `codecPairs`, `commandTypes`, `derivedTypes`, `importUphill`, `importRankSkip`, `importCrossSlice`, `importIsolatedGroups`, `importCycles`, `instructionSymlinks`, `singleStorybook`, `commitTypes`, `enginesFloor`, `dockerfilePackageManager`, `migrationsDirectory`, `eslint`, the workflow checks (`workflowJobOrdering`, `workflowSkippableJobs`, `workflowRunnerResolution`, `workflowVersionSingleSource`, `workflowCredentials`, `workflowReleaseJob`, `workflowRepositoryDispatch`, `workflowUpdateBotCooldown`, `workflowMergeGroup` and `workflowActionPinning`) and, taking `{ cwd, options, github }`, the settings checks (`settingsMergeMethods`, `settingsRequiredChecks` and `settingsReviewThreadResolution`). These take options and a layout as already validated and do not check them again; go through `runChecks` for validation. `parseWorkflow` and `loadWorkflows` expose the workflow model the workflow checks read. A check is a function that returns `Promise<readonly Violation[]>`, where a `Violation` is `{ code, message, file, location? }`.
315
331
 
316
332
  ### Proving a handler map is exhaustive
317
333