# Connector source validators

The static validator suite for connector workspaces — the rules behind
`appmixer connector validate`. This is the **single source of truth** for
portable connector-source rules: workspaces (appmixer-connectors first) consume
the suite through the published CLI and must not copy rule files
(appmixer-connectors#1219 was closed exactly to prevent that).

`validate` proves a connector agrees with our conventions from the source alone
— offline, no credentials. Its live counterpart is
[`appmixer connector verify`](../connector-verify/README.md), which proves the
source tells the truth about the service's API.

## How the suite reaches consumers

- **Releases** (`npm install -g appmixer`) are the primary channel — validators
  ship with the regular prod version.
- **`appmixer@dev`** is an experimental dist-tag published automatically from
  every push to this repo's dev branch (`manual-release.yml`, `publish-dev`
  job, npm OIDC trusted publishing). The appmixer-connectors repo installs it
  in CI so its dev branch validates against this repo's dev — a rule merged
  here is enforced there on the next CI run, nothing to copy.

## Rule contract

Each rule is one file in `rules/` exporting:

```js
module.exports = {
    name: 'kebab-case-name',        // unique; on collision with a workspace
    description: 'one line',        // rule loaded via --rules-dir, built-in wins
    run(context) { ... }
};
```

`context` provides: `repoRoot`, `connectorsRoot` (`<workspace>/src`),
`bundleFiles`, `componentFiles` (pre-computed lists across every vendor dir),
`walkFiles(dir, matcher)`, `relativePath(filePath)`, and the two reporters:

- `addFailure(filePath, message)` — a hard finding; fails the run unless held
  under a workspace threshold (below).
- `addWarning(filePath, message)` — informational; never fails a run. Use it
  when the check is heuristic and false positives are structural, and name the
  known false-positive classes in the rule's header comment
  (`writable-fields-readable` is the model).

Conventions: `'use strict'`, a "What this validator checks / Why" header
comment, helpers from `./_shared` (`readJson`, …), and **vendor-generic** logic
— never assume `src/appmixer/`; walk what `componentFiles` gives you.

## Adding a rule

1. Create `rules/<name>.js` per the contract above.
2. Decide failure vs warning honestly: a rule whose false positives you cannot
   enumerate is a warning, not a gate. Warnings need no threshold anywhere.
3. Bump the expected rule counts in `test/api/connector-validate.test.js`
   (two assertions: base suite and the `--rules-dir` case).
4. Run it against the real appmixer-connectors workspace before merging and
   put the measured numbers in the commit message:
   `appmixer connector validate --connectors-dir <workspace> --show-suppressed <name>`.
5. Merge to dev → the dev dist-tag publish makes it enforceable downstream;
   it ships to everyone with the next release.

A new failing rule lands in consumer workspaces with **no threshold entry,
i.e. strict**. That is safe by construction — their whole-repo CI runs are
informational and the blocking gates only see the PR's diff — but if the rule
finds significant legacy debt, tell the workspace maintainers so they can add
a threshold cap in the same breath.

## Thresholds and ignore-lists — workspace data, engine here

The suite implements the semantics; the numbers and suppressions belong to the
consuming workspace and never live in this repo.

- **Thresholds (ratchet)** — whole-repo runs only. A rule listed in the
  workspace's thresholds file fails only when its count exceeds the cap;
  no entry = strict. `--update-thresholds` writes lowered caps back to the
  workspace file. Strict modes (`--changed`, `--connector <name>`) ignore
  thresholds entirely.
- **Ignore-list** — specific `{ validator, paths, messageIncludes, reason }`
  suppressions with mandatory reasons. Applies in **all** modes including
  strict ones, so a recorded false positive can never block a commit.
  Full-workspace runs report entries that no longer match anything.
- **Default file locations** resolved per workspace (first existing wins):
  `validators.thresholds.json` | `scripts/validators/.thresholds.json`, and
  `validators.ignore.js|.json` | `scripts/validators/_ignore-list.js`.

## Workspace-local rules

`--rules-dir <dir>` loads extra rules with the identical contract — for rules
that need the workspace's git history (appmixer-connectors keeps
`bundle-bump-on-change` and `oauth-scope-bump` this way) and for prototyping a
future built-in. On a name collision the built-in wins, so a stale local copy
of a shipped rule cannot shadow upstream fixes.
