# Local/CI Parity Invariants

This document is the authoritative reference for CI/local check parity in ado-git-repo-insights. Every CI quality gate has a verified local equivalent that runs automatically via git hooks, ensuring developers cannot push code that fails CI. The local PR preflight is authoritative by default and fails closed if a CI-hard gate cannot run.

This document records **invariants and contracts**, not point-in-time state. It does not track drift-prone numbers (test counts, historical failure tallies, line offsets, `Last verified` dates, or gate counts). Authoritative values live in the source files the document references — [scripts/run_pr_preflight.py](scripts/run_pr_preflight.py), [scripts/run_repo_hook.py](scripts/run_repo_hook.py), [.github/workflows/ci.yml](.github/workflows/ci.yml), [.pre-commit-config.yaml](.pre-commit-config.yaml), and the hook scripts under [.husky/](.husky/). If a number would need manual updating to stay truthful, it belongs in code, not here.

## How It Works

```
git commit  -->  .husky/pre-commit  -->  run_repo_hook.py pre-commit  -->  Tier 1 checks
git push    -->  .husky/pre-push    -->  run_repo_hook.py pre-push    -->  Tier 2 checks (includes full preflight)
```

The Tier 1 / Tier 2 labels describe *when* a given run fires, not strict hook
exclusivity. A handful of framework-provided hooks in
`.pre-commit-config.yaml` register for both stages (e.g. `detect-private-key`,
`check-added-large-files`, `check-merge-conflict`, `env-guard`,
`tool-version-parity`) and therefore fire on both commit and push — the YAML
is authoritative for the current set.

**Only bypass**: `--no-verify` flag, which is forbidden by project policy.

## Tier 1: Pre-Commit (automatic on `git commit`)

| #   | CI Check                         | Local Equivalent                      | Parity  | Prevention Proof                                                                | How It Can Never Happen Again                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| --- | -------------------------------- | ------------------------------------- | ------- | ------------------------------------------------------------------------------- |   |
| 1   | ci-guards                        | pre-commit framework                  | Partial | [run_repo_hook.py:run_pre_commit_stage()](scripts/run_repo_hook.py)             | CI-config validation (composite actions, packageManager) cannot be replicated locally.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| 2   | pnpm-lockfile-guard              | `run_pnpm_lockfile_guard()`           | Match   | [run_repo_hook.py:run_pnpm_lockfile_guard()](scripts/run_repo_hook.py)          | Pre-commit hook blocks `package-lock.json` on staged files. Same logic as CI.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| 3   | npm-command-guard                | `run_npm_command_guard()`             | Match   | [run_repo_hook.py:run_npm_command_guard()](scripts/run_repo_hook.py)            | Pre-commit hook scans staged files for `npm ci`/`npm install` with identical allowlist (tfx-cli).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| 4   | pagination-token-guard           | `run_pagination_token_guard()`        | Match   | [run_repo_hook.py:run_pagination_token_guard()](scripts/run_repo_hook.py)       | Pre-commit hook scans staged files for direct `continuationToken` usage. Same allowlist as CI.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| 5   | ui-bundle-sync                   | `run_managed_artifacts("sync")`       | Match   | [run_repo_hook.py:run_managed_artifacts()](scripts/run_repo_hook.py)            | Pre-commit triggers full artifact sync (SDK + UI + docs) when TypeScript files are staged. Introduced alongside the managed-artifact pipeline in [3247874](https://github.com/oddessentials/ado-git-repo-insights/commit/3247874); before that, UI builds were manual and could be forgotten before push.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| 6   | extension-tests (tsc)            | `run_extension_typecheck()`           | Match   | [run_repo_hook.py:run_extension_typecheck()](scripts/run_repo_hook.py)          | `tsc --noEmit` runs in pre-commit when TS files are staged. Added in [5d18b31](https://github.com/oddessentials/ado-git-repo-insights/commit/5d18b31) after repeated type-error escapes to CI (prior escapes at [88ed3b7](https://github.com/oddessentials/ado-git-repo-insights/commit/88ed3b7), [7264576](https://github.com/oddessentials/ado-git-repo-insights/commit/7264576), [3247874](https://github.com/oddessentials/ado-git-repo-insights/commit/3247874), PR #207).                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| 7   | extension-tests (ESLint)         | `run_extension_lint()`                | Match   | [run_repo_hook.py:run_extension_lint()](scripts/run_repo_hook.py)               | ESLint runs in pre-commit when TS files are staged. Added in [7264576](https://github.com/oddessentials/ado-git-repo-insights/commit/7264576). Pre-push re-run: [run_pr_preflight.py](scripts/run_pr_preflight.py) CommandSpec "Extension lint".                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| 7a  | extension-tests (test tsc)       | `run_extension_test_typecheck()`      | Match   | [run_repo_hook.py:run_extension_test_typecheck()](scripts/run_repo_hook.py)     | `tsc --noEmit -p tsconfig.test.json` runs in pre-commit when any file in the test tsconfig's compilation scope is staged (tests/\*\*/\*.ts, ui/\*\*/\*.ts, types/\*.d.ts, tsconfig\*.json). Guarded by `require_clean_test_compilation_scope()` — blocks commit if any file in the compilation scope has unstaged changes, ensuring tsc validates the staged snapshot. Trigger scope regression tests: [test_hook_triggers.py](tests/unit/test_hook_triggers.py). Resolution regression tests: [config-parity-resolution.test.ts](extension/tests/meta/config-parity-resolution.test.ts). Qualifying UI changes trigger both production tsc (#6) and test tsc (#7a) — intentional, as they protect different contracts. Also in `test:ci` entry point. Pre-push: [run_pr_preflight.py](scripts/run_pr_preflight.py) CommandSpec "Extension test type check". CI: [ci.yml](.github/workflows/ci.yml) `extension-tests` job.                                                        |
| 7c  | extension-tests (test ESLint)    | `run_extension_test_lint()`           | Match   | [run_repo_hook.py:run_extension_test_lint()](scripts/run_repo_hook.py)          | `pnpm run lint:tests` (eslint tests/ --max-warnings=0) runs in pre-commit when test triggers fire (extension/tests/\*\*/\*.ts, extension/eslint.config.mjs). Enforces zero warnings — test rules stay at warn severity but cannot accumulate. Authoritative command is `lint:tests` in extension/package.json, reused by all entry points. Pre-push: [run_pr_preflight.py](scripts/run_pr_preflight.py) CommandSpec "Extension test lint". CI: [ci.yml](.github/workflows/ci.yml) `extension-tests` job. test:ci: included in extension test:ci chain. Parity-drift regression test: [test_ci_parity_drift.py](tests/unit/test_ci_parity_drift.py).                                                                                                                                                                                                                                                                                                                          |
| 7b  | test config parity               | `run_extension_config_parity()`       | Match   | [run_repo_hook.py:run_extension_config_parity()](scripts/run_repo_hook.py)      | In-process TypeScript API comparison (no child processes) between tsconfig.json and both test configs (tsconfig.test.json, tsconfig.type-tests.json). Compares explicit PARITY_FLAGS set — resilient to TS API shape changes. Guarded by `require_clean_tsconfigs()` — blocks commit if any extension/tsconfig\*.json has unstaged changes. Missing configs fail hard. Also in `test:ci` entry point. Pre-push: [run_pr_preflight.py](scripts/run_pr_preflight.py) CommandSpec "Extension test config parity". CI: [ci.yml](.github/workflows/ci.yml) `extension-tests` job.                                                                                                                                                                                                                                                                                                                                                                                                          |
| 7d  | scope-coverage (staged)          | `run_scope_coverage_guard()`          | Match   | [run_repo_hook.py:run_scope_coverage_guard()](scripts/run_repo_hook.py)         | Pre-commit verifies each staged `.py`/`.ts` file belongs to a known audit scope. Proves "no staged file escapes audit." Full-tree coverage is Tier 2 only.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| 7e  | rule-disable-invariants (staged) | `run_rule_disable_invariants_guard()` | Match   | [run_repo_hook.py:run_rule_disable_invariants_guard()](scripts/run_repo_hook.py) | Pre-commit checks staged Python files for unsafe subprocess, random, and sys.path patterns. Compensating guardrails for globally disabled S603/S311. Same detection functions as CI.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |

## Commit Message Gate (automatic on `git commit`)

| #   | CI Check   | Local Equivalent              | Parity   | Prevention Proof                                                                            | How It Can Never Happen Again                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| --- | ---------- | ----------------------------- | -------- | ------------------------------------------------------------------------------------------- |   |
| 7f  | commitlint | `pnpm exec commitlint --edit` | Weaker\* | [.husky/commit-msg](.husky/commit-msg), [ci.yml](.github/workflows/ci.yml) `commitlint` job | **CI is authoritative.** CI validates all PR commits via `commitlint --from origin/main --to HEAD`. Locally, the `.husky/commit-msg` hook runs `pnpm exec commitlint --edit` — but see Known Inconsistency below. Config: [commitlint.config.cjs](commitlint.config.cjs) extends `@commitlint/config-conventional` with `ratchet` added to `type-enum` for internal ratchet floor-bump commits, ignores merge commits. Semantic-release commits use `HUSKY=0` in CI to bypass hooks — the release commit format (`chore(release): X.Y.Z [skip ci]`) passes commitlint regardless. Pytest guard: [test_hook_guards.py](tests/unit/test_hook_guards.py) `TestCommitlintInfrastructure`.                                                                                                                                                                                                                                                                                                                                  |

**Known Inconsistency (Weaker\*):** The local commit-msg hook depends on husky's internal dispatcher (`.husky/_/commit-msg`) being intact. External tools that manage git hooks (e.g. `entire`) can overwrite this dispatcher, silently breaking the chain to commitlint. A pre-commit health check warns when this is detected and prints the fix command (`pnpm exec husky`), but does **not** block — because the external tool re-injects on every commit, creating an unwinnable loop if the check were fatal. **CI is the authoritative gate.** Malformed commit messages will always be caught on PR, even if the local hook is bypassed. This is an accepted asymmetry: local enforcement is best-effort developer UX; CI enforcement is mandatory.

## Tier 2: Pre-Push Preflight (automatic on `git push`)

| #   | CI Check                            | Local Equivalent                                                                                        | Parity  | Prevention Proof                                                                                                                                                                                                                                        | How It Can Never Happen Again                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| --- | ----------------------------------- | ------------------------------------------------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |   |
| 8   | line-ending-guard                   | `run_crlf_guard()`                                                                                      | Match   | [run_repo_hook.py:run_crlf_guard()](scripts/run_repo_hook.py)                                                                                                                                                                                           | Pre-push scans all source directories for CRLF. Same targets as CI.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| 9   | baseline-integrity                  | `check-baseline-integrity.js`                                                                           | Match   | [run_repo_hook.py:run_pre_push_hook()](scripts/run_repo_hook.py)                                                                                                                                                                                        | Pre-push runs the identical Node script via `run_pre_push_hook()`. Baseline format is self-healing on drift.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| 10  | mypy                                | `mypy src/ tests/ scripts/ .github/scripts/`                                                            | Match   | [run_pr_preflight.py](scripts/run_pr_preflight.py) CommandSpec "Python type check"                                                                                                                                                                      | Pre-push preflight runs `mypy src/ tests/ scripts/ .github/scripts/` with identical strict config from `pyproject.toml` (relaxed overrides for tests/ and scripts/, strict defaults for CI-owned Python under `.github/scripts/`). Extended from `src/`-only to cover all repo-owned Python code.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| 11  | suppression-audit                   | `audit-suppressions.py --diff` (strict)                                                                 | Match\* | [run_pr_preflight.py](scripts/run_pr_preflight.py) CommandSpecs "Suppression baseline sync gate", "Suppression main-baseline gate", and "Suppression justifications"; [run_repo_hook.py:run_pre_commit_hook()](scripts/run_repo_hook.py)                 | Pre-commit and pre-push both run `--diff` against the committed `.suppression-baseline.json`. Scope coverage spans every tracked Python and TypeScript subdirectory (expanded in #232 to cover `scripts/`, `tests/`, `.github/scripts/`). **Gate reordering**: suppression audit runs FIRST in pre-commit, pre-push, and `test:ci`. **Baseline immutable at zero**: all scopes are blocking at zero. **Remaining asymmetry (Match\*)**: Local compares branch baseline; CI fetches `origin/main`'s baseline.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| 11a | scope-coverage (full-tree)          | `audit-suppressions.py --check-coverage`                                                                | Match   | [run_pr_preflight.py](scripts/run_pr_preflight.py) CommandSpec "Suppression scope coverage (FR-026)"                                                                                                                                                    | Pre-push and CI verify every tracked `.py`/`.ts` file belongs to exactly one audit scope via `git ls-files`. Pre-commit runs staged-subset check only (Tier 1 #7d). Added in #232.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| 11b | baseline-staleness                  | `audit-suppressions.py --check-staleness`                                                               | Match   | [run_pr_preflight.py](scripts/run_pr_preflight.py) CommandSpec "Baseline staleness (FR-025)"                                                                                                                                                            | Pre-push and CI regenerate baseline into temp and JSON-compare (ignoring `generated_at`). Catches hand-edited or stale baselines. Resolution: `--update-baseline`. Added in #232.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| 11c | rule-disable-invariants (full-tree) | `check_rule_disable_invariants.py --check-subprocess --check-random --check-syspath --verify-artifacts` | Match   | [run_pr_preflight.py](scripts/run_pr_preflight.py) CommandSpec "Rule-disable invariants (FR-014)"                                                                                                                                                       | Pre-push and CI run full-tree subprocess/random/syspath guardrails + artifact verification. Pre-commit runs staged-subset check (Tier 1 #7e). Same detection functions across all entry points. Added in #232.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| 12  | test (Python, CI matrix)            | `pytest tests/` (baseline Python)                                                                       | Weaker  | [run_pr_preflight.py](scripts/run_pr_preflight.py) CommandSpec "Full Python test suite with coverage"                                                                                                                                                   | Pre-push runs the full pytest suite on the baseline Python version. CI runs the OS × Python matrix declared in [.github/workflows/ci.yml](.github/workflows/ci.yml). Cross-platform issues (Windows path handling, macOS timing) are CI-only by nature; local baseline catches logic errors.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| 13  | extension-tests (Jest)              | `jest --ci --coverage`                                                                                  | Match   | [run_pr_preflight.py](scripts/run_pr_preflight.py) CommandSpec "Extension Jest CI"                                                                                                                                                                      | Pre-push preflight runs identical Jest command with `--ci --runInBand --coverage`. Same reporters and path ignores.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| 14  | extension-tests (type-tests)        | `pnpm run test:types`                                                                                   | Match   | [run_pr_preflight.py](scripts/run_pr_preflight.py) CommandSpec "Extension type tests"                                                                                                                                                                   | Pre-push preflight runs `tsc --noEmit --project tsconfig.type-tests.json`. Identical to CI.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| 15  | extension-tests (smoke)             | `pnpm run test:smoke`                                                                                   | Match   | [run_pr_preflight.py](scripts/run_pr_preflight.py) CommandSpec "Extension smoke tests"                                                                                                                                                                  | Pre-push preflight runs Playwright smoke tests. Historical PR #207 regression where smoke tests used `locator("option")` after `<select>` was replaced with typeahead `<div>` — fixed in [db5b04b](https://github.com/oddessentials/ado-git-repo-insights/commit/db5b04b) with `[role="option"]` + `data-testid` selectors that survive DOM refactors.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| 16  | build (Python)                      | `python -m build --sdist`                                                                               | Weaker  | [run_pr_preflight.py](scripts/run_pr_preflight.py) CommandSpec "Python package build check"                                                                                                                                                             | Pre-push builds sdist; CI additionally builds wheel. sdist catches the majority of packaging issues. Wheel-only failures have not been observed in practice.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| 17  | build-extension (tsc+esbuild)       | `build:check` + `build:ui`                                                                              | Partial | [run_pr_preflight.py](scripts/run_pr_preflight.py) CommandSpecs "Extension build check" and "Extension UI bundle"                                                                                                                                       | Pre-push runs tsc (type check) + esbuild (bundle) separately. CI runs them linked via `pnpm run build` (which uses `tsc -p tsconfig.build.json` for CommonJS emission + esbuild for IIFE bundles). The split tsconfig architecture uses `tsconfig.json` (ES2022/bundler) for type checking and `tsconfig.build.json` (CommonJS/bundler) for `dist/` emission — Node-executed scripts require CJS runtime semantics. Guard: [build-output-format-guard.test.ts](extension/tests/meta/build-output-format-guard.test.ts) pins module + moduleResolution for each config plus the build script entry point and `ui/` exclusion. tsc + esbuild parity catches code errors; VSIX packaging is CI-only (requires tfx-cli).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| 17a | extension-tests (prettier)          | `pnpm --dir extension run format:check`                                                                 | Match   | [run_pr_preflight.py](scripts/run_pr_preflight.py) CommandSpec "Extension format check"; [extension/package.json](extension/package.json) `test:ci`; [.github/workflows/ci.yml](.github/workflows/ci.yml) `extension-tests` step "Prettier format check" | **Contract**: call the `format:check` script by name; never invoke Prettier directly. **Invocation form follows cwd**: when cwd is `extension/` (via `cwd=EXTENSION_ROOT` in preflight, `working-directory: extension` in CI, or script location in `test:ci`), use `pnpm run format:check`; when cwd is the repo root, use `pnpm --dir extension run format:check`. Both forms resolve to the same script in `extension/package.json`, which owns all flags: the `--check` mode, the `--ignore-path ../.prettierignore` pointer to the repo-root ignore file, and the `**/*.{ts,js,json,md}` glob. Config: [extension/.prettierrc.json](extension/.prettierrc.json) (explicit, `endOfLine: lf` to preserve CRLF-guard parity on Windows). Ignore list at repo-root [.prettierignore](.prettierignore) holds only files with proven competing owners (version-stamped JSON files whose indentation is owned by [scripts/stamp-extension-version.cjs](scripts/stamp-extension-version.cjs), and regenerated local artifact directories); see `.prettierignore` for the current entries and rationale comments. Regression coverage: [test_ci_parity_drift.py](tests/unit/test_ci_parity_drift.py) `TestFormatCheckParity` locks the script flags, the per-entry-point invocation form, a negative assertion forbidding any direct Prettier invocation outside the authoritative script (allowlist: `extension/package.json` exactly once), and failure-case symmetry via a seeded-violation pytest that proves the authoritative command catches violations. **No pre-commit hook by design** — pre-commit auto-fix was considered and rejected to avoid format-loop risk; read-only pre-commit was considered and rejected to avoid trigger-scope and clean-worktree churn. Enforcement is Tier 2 (preflight) and CI only. |
| 18  | build-extension (VSIX)              | `package:vsix` + `test:vsix` (VSIX_REQUIRED=true)                                                       | Match   | [run_pr_preflight.py](scripts/run_pr_preflight.py) CommandSpecs "Extension VSIX package" and "Extension VSIX artifact inspection"; [ci.yml](.github/workflows/ci.yml) `build-extension` steps "Package VSIX" and "Run VSIX Inspection Tests (Tier B)"   | Pre-push and CI both package a fresh VSIX from HEAD via `pnpm run package:vsix` (build + stage:tasks + `tfx extension create`), then invoke `pnpm run test:vsix` with `VSIX_REQUIRED=true` so a missing VSIX is a hard failure instead of a silent pass. The package step runs immediately before the inspection step in both entry points; regression-locked by [test_ci_parity_drift.py](tests/unit/test_ci_parity_drift.py) `TestVsixGateParity` which asserts (a) preflight has the package CommandSpec using `pnpm run package:vsix`, (b) preflight inspection sets `extra_env={"VSIX_REQUIRED": "true"}` exactly, (c) package immediately precedes inspection in preflight, (d) CI `build-extension` packages VSIX immediately before inspection, (e) CI inspection step sets `env.VSIX_REQUIRED='true'`. `tfx-cli` is an `extension/` devDependency — no global install is needed locally. Prior asymmetry (`Partial`): preflight ran the inspection against whatever stale `*.vsix` happened to be on disk and silently skipped if none was present; both holes closed by the package-first + VSIX_REQUIRED pair. |
| 19  | build-extension (task tests)        | `node index.test.js`                                                                                    | Match   | [run_pr_preflight.py](scripts/run_pr_preflight.py) CommandSpec "Extension task unit tests"                                                                                                                                                              | Identical command in pre-push and CI.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| 20  | build-extension (task input)        | `ts-node validate-task-inputs.ts`                                                                       | Match   | [run_pr_preflight.py](scripts/run_pr_preflight.py) CommandSpec "Task input validation"                                                                                                                                                                  | Identical command in pre-push and CI.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| 21  | Run Demo Tests                      | `pytest tests/demo/`                                                                                    | Match   | [run_pr_preflight.py](scripts/run_pr_preflight.py) CommandSpec "Demo dashboard validation"                                                                                                                                                              | Pre-push runs identical `pytest tests/demo/ -v --no-cov`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| 22  | Check Directory Size                | Directory-size limit on `docs/`                                                                         | Match   | [run_pr_preflight.py](scripts/run_pr_preflight.py) CommandSpec "Demo directory size check"                                                                                                                                                              | Pre-push checks `docs/` directory against the size limit declared in [scripts/run_pr_preflight.py](scripts/run_pr_preflight.py) (search for `SIZE_LIMIT_MB` / the `limit = ...` literal). Identical logic to CI.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| 23  | Verify Byte-Identical Output        | `manage_generated_artifacts.py verify`                                                                  | Partial | [run_pr_preflight.py](scripts/run_pr_preflight.py) CommandSpec "Generated artifact parity"                                                                                                                                                              | Pre-push verifies all managed artifacts (SDK, UI bundle, docs) are in sync. CI does a narrower `git diff docs/` check after regeneration.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| 24  | Regenerate Demo Data                | `validate_demo_generation_contract.py`                                                                  | Partial | [run_pr_preflight.py](scripts/run_pr_preflight.py) CommandSpec "Demo generation contract"                                                                                                                                                               | Pre-push validates the demo generation contract. CI also runs the full `build-demo-dataset.py`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| 25  | threshold-change-guard              | `check_threshold_changes.py`                                                                            | Match   | [run_pr_preflight.py](scripts/run_pr_preflight.py) CommandSpec "Threshold change guard"                                                                                                                                                                 | Pre-push compares coverage thresholds against main branch. **Bypass**: `[threshold-update]` placed in a **commit subject line** within `origin/main..HEAD`. Scanned via `git log --oneline` (subjects only); markers placed in commit bodies are NOT honored. Same convention as version-guard (Row 31), ratchet-bump guard (Row 27a), and coverage-delta (Row 32).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| 26  | test count (Python)                 | `validate-test-results.py`                                                                              | Match   | [run_pr_preflight.py](scripts/run_pr_preflight.py) CommandSpec "Python test count validation"; [ci.yml](.github/workflows/ci.yml); [.test-floor-contract.json](.test-floor-contract.json)                                                               | Pre-push and CI both run `validate-test-results.py --min-collected-artifact .test-floor-contract.json --suite python --max-skips=0`. The floor lives in [.test-floor-contract.json](.test-floor-contract.json); both entry points read it by path — no hardcoded integers in either file.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| 27  | test count (Extension)              | `validate-test-results.py`                                                                              | Match   | [run_pr_preflight.py](scripts/run_pr_preflight.py) CommandSpec "Extension test count validation"; [ci.yml](.github/workflows/ci.yml); [.test-floor-contract.json](.test-floor-contract.json)                                                            | Pre-push and CI both run `validate-test-results.py --min-collected-artifact .test-floor-contract.json --suite extension --max-skips=0`. Same committed artifact as Python (Row 26); both suites share one file to prevent partial updates.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| 27a | ratchet-bump guard                  | `check_ratchet_bump.py`                                                                                 | Match   | [run_pr_preflight.py](scripts/run_pr_preflight.py) CommandSpec "Ratchet bump guard"; [ci.yml](.github/workflows/ci.yml) job `ratchet-bump-guard`                                                                                                        | Self-contained per-commit ratchet-bump discipline gate (#280). Reads floors from the committed [.test-floor-contract.json](.test-floor-contract.json) artifact (consumed by both [run_pr_preflight.py](scripts/run_pr_preflight.py) and [ci.yml](.github/workflows/ci.yml) via `--min-collected-artifact`), with defensive YAML navigation (typed `CiYamlParseError`, never `AttributeError`), measures Python count via subprocess-isolated pytest collector (`PYTEST_DISABLE_PLUGIN_AUTOLOAD=1`, `-o addopts=`, scrubbed `PYTEST_ADDOPTS`/`PYTEST_PLUGINS`, committed `scripts/_pytest_count_collector.py` plugin — zero stdout parsing), measures Extension count via JUnit XML parse. Asserts strict equality `actual == floor` on both dimensions AND inter-file parity between the two authoritative sites. `run_gate` splits drift into a `DriftReport(parity, equality)` pair so bypass markers can waive the equality bucket only — parity failures exit `EXIT_DRIFT` regardless of marker, and the marker is printed as "ignored for inter-file parity" so good-faith users see why it was refused. **Base-ref determinism**: `ensure_base_ref_reachable` always issues `git fetch --no-tags origin <ref>` before validation (no stale-ref short-circuit), and `--base-ref` input is normalized to `origin/<branch>` up front (bare names accepted; HEAD, full ref paths, non-origin remotes, bare SHAs, and empty strings rejected with explicit `EXIT_SETUP` messages). `scripts/run_pr_preflight.py::resolve_pr_base_ref()` defaults to `origin/main` when neither `BASE_REF` nor `GITHUB_BASE_REF` is set (non-strict mode, which is the default for all hook-invoked preflight runs). Developers targeting a non-main base branch export `BASE_REF=<target-branch>` (matching CI's `github.base_ref` env). Strict mode (`--strict` flag) fails closed instead of defaulting, but the pre-push hook does not force strict on any branch. Shallow-clone handling uses deterministic `git fetch --unshallow` (never `--depth=N`) with an explicit cross-check that `git log {base}..HEAD` and `git rev-list --count {base}..HEAD` agree, so marker scans cannot silently miss bypass commits. CI wiring is a dedicated top-level `ratchet-bump-guard` job with `needs: [test, extension-tests, python-collection-parity]` and `if: always() && !cancelled()`, a liveness assert on all sibling `needs.*.result` values (including `python-collection-parity` which proves the Python floor is cross-OS-identical before the ratchet gate trusts it), and an artifact-present assert ONLY for the Extension JUnit (Python is measured in-job via the same code path as local preflight — asserting an unused Python artifact would create false failures unrelated to the gate's logic). **Bypass**: `[ratchet-realignment]` (floor catching up on historical drift) or `[ratchet-test-removal]` (intentional reduction) placed in a **commit subject line** within `{base}..HEAD`. Both markers are scanned via `git log --oneline` (subjects only), matching the convention used by `check_threshold_changes.py` and `check-version-unchanged.py`; markers placed in commit bodies are NOT honored, preventing feature-documentation prose (including this gate's own commit messages) from accidentally disarming the gate. **Per-commit accounting**: Python marker waivers are commit-local and first-parent based — the gate walks the first-parent range, snapshots each commit and its parent, compares `floor_delta` vs `actual_delta`, and only honors a marker on the commit that introduced that delta. Extension markers no longer waive equality drift at all because `extension/test-results.xml` is not historically committed — the gate fails explicitly rather than pretending per-commit accounting can work. Parent and child snapshot authority mismatches (preflight vs ci.yml floor disagreement at any commit in the range) are non-waivable. **Scope**: markers waive actual-vs-floor equality only; inter-file parity and parse validation are never exempted.                         |
| 28  | patch coverage parity               | `check_patch_coverage.py`                                                                               | Match   | [run_pr_preflight.py](scripts/run_pr_preflight.py) CommandSpec "Local patch coverage parity"                                                                                                                                                            | Pre-push compares Python + TypeScript coverage against main branch baseline.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| 29  | pandas version policy               | runtime version check                                                                                   | Match   | [run_pr_preflight.py](scripts/run_pr_preflight.py) CommandSpec "Pandas version policy"                                                                                                                                                                  | Pre-push verifies pandas major version matches Python version expectation.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| 30  | demo generation contract            | `validate_demo_generation_contract.py`                                                                  | Match   | [run_pr_preflight.py](scripts/run_pr_preflight.py) CommandSpec "Demo generation contract"                                                                                                                                                               | Identical validation script in pre-push and CI.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| 31  | version-guard                       | `check-version-unchanged.py`                                                                            | Match   | [run_repo_hook.py:run_version_guard()](scripts/run_repo_hook.py)                                                                                                                                                                                        | Pre-push runs `check-version-unchanged.py` against `origin/main` as the FIRST check — fails fast before any expensive gates. Identical script and bypass mechanism in both local and CI. **Bypass**: Add `[version-override-acknowledged]` to a **commit subject line** on the branch (same subject-line marker pattern as `[threshold-update]` and the ratchet-bump markers). Scanned via `git log --oneline {base}..HEAD` (subjects only, two-dot range for branch-local commits); markers placed in commit bodies are NOT honored. Direct pushes to main are NEVER bypassed. No PR-body mechanism — local and CI are fully symmetric. Promoted from Tier 3 (CI-Only) after the 99.0.x marketplace incident demonstrated the need for local fail-fast.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| 32  | coverage-delta                      | `check_coverage_delta.py`                                                                               | Match   | [run_pr_preflight.py](scripts/run_pr_preflight.py) CommandSpec "Coverage delta parity (Codecov project status)"                                                                                                                                         | Pre-push compares current Python + TypeScript coverage totals against `.coverage-baseline.json` and fails if any metric drops more than 2% — matching Codecov's `project` status (`target: auto`, `threshold: 2%`). Runs after pytest and jest generate coverage files. Baseline updated via `--update` flag. Baseline-change acknowledgement shares the `[threshold-update]` subject-line marker with the threshold-change-guard (Row 25); `check_coverage_delta.py` does not scan the marker itself — it only references the convention in its remediation output — so placement rules follow Row 25: subject line only, scanned via `git log --oneline`, markers in commit bodies are NOT honored.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| 33  | secret-scan (gitleaks)              | `gitleaks detect --config=.gitleaks.toml`                                                               | Match   | [run_pr_preflight.py](scripts/run_pr_preflight.py) CommandSpec "Secret scan (gitleaks)"                                                                                                                                                                 | Pre-push requires the same gitleaks scan CI enforces. If `gitleaks` is unavailable locally, authoritative preflight fails rather than silently skipping. `detect-private-key` still provides an earlier staged-file guard in pre-commit, but it is no longer the only local secret check.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| 34  | cli-reference-drift                 | `generate_cli_reference.py --check`                                                                     | Match\* | [run_pr_preflight.py](scripts/run_pr_preflight.py) CommandSpec "CLI reference drift"; [ci.yml](.github/workflows/ci.yml) job `cli-reference-drift`; [.pre-commit-config.yaml](.pre-commit-config.yaml) hook `cli-reference-drift`                      | Pre-commit (`stages: [pre-push]`), preflight, and CI all call the single authoritative command `python scripts/generate_cli_reference.py --check`. The generator imports `create_parser()` from [src/ado_git_repo_insights/cli.py](src/ado_git_repo_insights/cli.py) and emits the per-subcommand flag tables; on the CI-canonical Python version (`_CANONICAL_PYTHON = (3, 12)` in [scripts/generate_cli_reference.py](scripts/generate_cli_reference.py)), `--check` regenerates in memory and fails non-zero with a unified diff if the committed `docs/reference/cli-reference.md` generated regions, the golden SHA fixture at `tests/unit/fixtures/cli_reference_generated_sections.sha256`, or the per-subcommand `--help` snapshots at `tests/unit/fixtures/help_snapshots/` have drifted. Regenerate with `python scripts/generate_cli_reference.py --write` after any `create_parser()` edit; the write path refuses non-canonical Python with an actionable error, so committed snapshots are canonical by construction. The generator refuses at emission time to copy rate / throughput / latency language into generated tables, so the corpus-wide hedge sweep in [tests/unit/test_backfill_doc_parity.py](tests/unit/test_backfill_doc_parity.py) stays load-bearing. Symmetry between preflight, CI, and pre-commit invocation is locked by [tests/unit/test_ci_parity_drift.py](tests/unit/test_ci_parity_drift.py) `TestCliReferenceGateParity`. **Canonical-Python asymmetry (Match\*)**: argparse help-text rendering is not byte-stable across Python versions (Python 3.14 ignores `HelpFormatter(width=100)` for the subparser-choices line where 3.12 honors it). To prevent false-positive drift on non-canonical interpreters, `mode_check()` emits a `[SKIP]` marker and returns exit 0 on any Python version other than 3.12 — preflight and pre-commit continue without blocking the dev, and CI (pinned to Python 3.12) remains the authoritative verdict. The write-path guard (`_require_canonical_python_for_write`) still refuses regeneration on non-canonical Python, so the SKIP cannot hide actual drift: any snapshot on disk was either written under Python 3.12 or not at all. The `[SKIP]` marker is distinct from a test-runner skip (the two tests in [tests/unit/test_cli_reference_generator.py](tests/unit/test_cli_reference_generator.py) that exercise the argparse-render path branch on `_ON_CANONICAL_PYTHON` rather than using `@pytest.mark.skipif`, preserving the `--max-skips=0` gate). |
| 35  | typing-any-guard (QG-40)            | `check_no_any_types.py`                                                                                 | Match   | [run_pr_preflight.py](scripts/run_pr_preflight.py) CommandSpec "No typing.Any in src/, tests/, scripts/ (QG-40)"; [ci.yml](.github/workflows/ci.yml) step "No typing.Any in src/, tests/, scripts/ (QG-40)"                                             | Pre-push and CI run the identical `python scripts/check_no_any_types.py` invocation against `src/`, `tests/`, and `scripts/`. Enforces QG-40: no `typing.Any` leakage into repo-owned Python surfaces. Detection is AST-based; no allowlist. Same script across both entry points — parity follows by construction.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| 36  | partial-branch ratchet              | `pnpm --dir extension run test:partial-branches`                                                        | Match   | [run_pr_preflight.py](scripts/run_pr_preflight.py) CommandSpec "Partial-branch ratchet"; [ci.yml](.github/workflows/ci.yml) step "Partial-branch ratchet"                                                                                               | Pre-push and CI both invoke the `test:partial-branches` script defined in `extension/package.json`. Prevents TypeScript partial-branch coverage from regressing against its committed floor. Authoritative command lives in `extension/package.json`; both entry points call it by name — identical flags and measurement source.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| 37  | test floor contract validation      | `check_test_floor_contract.py`                                                                          | Match   | [run_pr_preflight.py](scripts/run_pr_preflight.py) CommandSpec "Test floor contract validation"; [ci.yml](.github/workflows/ci.yml) step "Validate test floor contract"; [.test-floor-contract.json](.test-floor-contract.json)                        | Pre-push and CI both run `python scripts/check_test_floor_contract.py --contract .test-floor-contract.json --extension-junit <path>`. Validates the structural integrity of the committed floor artifact (schema version, required suite keys, authority strings) and that the Extension JUnit it is about to be compared against is well-formed. Complements Row 26 / Row 27 (which enforce the floor value) and Row 27a (per-commit ratchet-bump discipline) by asserting the artifact itself is parseable before any downstream gate consumes it. Same script path in both entry points.                                                                                                                                                                                                                                                                                                                                              |
| 38  | PR-record schema parity (Feature 310) | `check_pr_record_schema_parity.py`                                                                    | Match   | [run_pr_preflight.py](scripts/run_pr_preflight.py) CommandSpec "PR-record schema parity"; [ci.yml](.github/workflows/ci.yml) step "PR-record schema parity (Feature 310, QG-49)"; [run_repo_hook.py](scripts/run_repo_hook.py) `is_pr_record_parity_trigger` / `require_clean_pr_record_parity_scope` / `run_pr_record_schema_parity_check`; [extension/package.json](extension/package.json) `test:schema-parity` chained into `test:ci`; [specs/310-comments-visualization/contracts/schema-parity-gate.md](specs/310-comments-visualization/contracts/schema-parity-gate.md) | Cross-surface schema parity gate for the extended PrRecord (Feature 310, QG-49). One canonical command — `python scripts/check_pr_record_schema_parity.py` — invoked identically from all four entry points: pre-commit (via `run_repo_hook.py` predicate + runner dispatch placed **before** the existing early-return in `run_pre_commit_hook` so single-file edits to `types.py` or the 310 §1 contract cannot silently skip the gate), pre-push preflight (`CommandSpec "PR-record schema parity"`), CI (`test:` job Python matrix step co-located with `Verify Ruff Version Consistency`), and `pnpm test:ci` (via `test:schema-parity` pnpm script wrapper chained immediately before `test:smoke`, mirroring the `test:partial-branches` pattern). Worktree-clean guard `require_clean_pr_record_parity_scope` covers exactly the three read paths — `src/ado_git_repo_insights/types.py`, `extension/ui/schemas/rollup.schema.ts`, and `specs/310-comments-visualization/contracts/pr-record-comments-fields.md` — matching `is_pr_record_parity_trigger` one-for-one (the 060 contract is a human-continuity pointer and is NOT a trigger, verified by near-miss regression in [test_hook_triggers.py](tests/unit/test_hook_triggers.py) `TestIsPrRecordParityTrigger::test_near_miss_paths_are_not_triggers`). Python-only parser (ast for the Python TypedDict; regex over a tightly-locked closed type set for the TypeScript interface and the `PR_RECORD_REQUIRED_FIELDS` array; markdown parse for the 310 §1 Canonical field declaration table) so the gate stays green under `pre-commit run --all-files` in the Python test matrix where `extension/node_modules` is absent. Fails closed on any construct outside the accepted shape, printing a field-level drift diagnostic. Coverage-backed by [tests/unit/test_pr_record_schema_parity.py](tests/unit/test_pr_record_schema_parity.py) which imports the script module and asserts `main([])` returns zero, plus per-surface parse smokes and fail-closed negative cases. |
| 39  | ephemeral-cleaner-smoke (#327) | `pnpm clean:dry` (preflight CommandSpec; pre-push runs it via `pnpm` wrapper) — `pnpm clean` is CI-only | Partial | [run_pr_preflight.py](scripts/run_pr_preflight.py) CommandSpec "Ephemeral cleaner dry-run (pnpm wrapper + schema lock)" with `allowed_exit_codes=(0, 3)` (`EXIT_SETUP` (2) intentionally excluded); [ci.yml](.github/workflows/ci.yml) job `ephemeral-cleaner-smoke`; [package.json](package.json) `clean` / `clean:dry`; [extension/package.json](extension/package.json) `clean` / `clean:ui`; [scripts/clean_ephemeral.py](scripts/clean_ephemeral.py); [scripts/ephemeral_registry.json](scripts/ephemeral_registry.json); [tests/unit/test_ci_parity_drift.py](tests/unit/test_ci_parity_drift.py) `TestCleanEphemeralParity` + `TestEphemeralCleanerSmokeJob` | **Tier 2 blocking, Partial parity by design.** Local pre-push preflight runs `pnpm clean:dry` via the wrapper to validate discoverability, planning, the wrapper-to-script chain, and the JSON schema contract — **non-destructive**, never runs `pnpm clean`, never deletes developer state. Exit 3 (`EXIT_DRY_RUN_PENDING` — plan has work pending) is the expected steady-state on a live workspace and is whitelisted via `allowed_exit_codes=(0, 3)` so legitimate scratch never blocks a push. **Exit 2 (`EXIT_SETUP`) is intentionally NOT whitelisted** — real cleaner setup failures (registry missing, not in repo, registry unparseable) surface as preflight failures rather than being masked. Exit 1 (validation error) and other unexpected codes likewise fail the gate. The CI-only half is the destructive `pnpm clean` apply, the cross-OS sweep on `windows-latest` / `macos-latest`, the >260-character Windows long-path seed, the post-clean idempotency assertion (`pnpm clean:dry` → exit 0 + empty plan), and the tracked-sentinel refusal (`git add -f` + `pnpm clean` non-zero exit + sentinel intact). Step order (review-directed): seed → `pnpm clean:dry` with work pending → `pnpm clean` → verify swept → final `pnpm clean:dry` idempotency lock → tracked-sentinel refusal. **The CI job invokes the cleaner ONLY via pnpm wrappers — direct `python scripts/clean_ephemeral.py` invocation is forbidden in the job and parity-test enforced.** Seven structural parity tests (`TestEphemeralCleanerSmokeJob`) lock the matrix cell set (`{ubuntu-latest, windows-latest, macos-latest}`), pnpm-only invocation rule, schema-based JSON assertions, step order, long-path seed, idempotency, and refusal contract. Five wrapper parity tests (`TestCleanEphemeralParity`) lock each pnpm wrapper's exact string, the preflight CommandSpec invocation form, and a negative scan that rejects inline `rmtree`/`rmSync`/`rmdir`/`shutil`/`recursive:true` in any `clean*` script (except `extension/package.json` `clean:tasks`, which is staging-teardown, not ephemeral sweep). The dual cleanup contract (module-level atexit in `tests/demo/test_demo_parity_pipeline.py` + session-scope fixture in `tests/demo/conftest.py`) is regression-locked by the `TestScratchRootIsProcessPrivate` pair and by `test_two_concurrent_peers_clean_only_their_own`. |

## Tier 3: CI-Only by Design

Row identifiers use a `T3-` prefix to avoid ambiguity with the numeric IDs
in Tier 2 above.

| #     | CI Check                   | Why CI-Only                                                                            |
| ----- | -------------------------- | -------------------------------------------------------------------------------------- |
| T3-1  | fresh-clone-verify         | Tests deterministic install from scratch; cannot simulate locally                      |
| T3-2  | scalability-tests          | Expensive synthetic data generation; run on demand locally via `pnpm run test:scalability` |
| T3-3  | test-base-no-ml            | ML-optional isolation; requires separate venv without `[ml]` extras                    |
| T3-4  | task-major-guard           | Requires GitHub PR context (webhook data)                                              |
| T3-5  | badge-publish              | Main-only post-merge artifact publish                                                  |
| T3-6  | Verify Asset Accessibility | Requires HTTP server + curl integration test                                           |

## External Checks (not in our control)

| Check                      | Provider |
| -------------------------- | -------- |
| codecov/patch              | Codecov  |
| ai-review / AI Code Review | GitHub   |

## Key Incidents and Fixes

Append-only historical log. Commit SHAs are immutable; dates anchor when the incident was diagnosed.

| Date       | Incident                                          | Fix                                                    | Commit                                                                           |
| ---------- | ------------------------------------------------- | ------------------------------------------------------ | -------------------------------------------------------------------------------- |
| 2026-03-25 | CI parity gaps discovered across multiple gates   | Added preflight gates                                  | [3247874](https://github.com/oddessentials/ado-git-repo-insights/commit/3247874) |
| 2026-03-25 | Type errors escaped to CI (non-null assertions)   | Added ESLint to pre-commit; type casts                 | [7264576](https://github.com/oddessentials/ado-git-repo-insights/commit/7264576) |
| 2026-03-25 | Type errors escaped to CI (ES2020 compat)         | Added VSIX test to preflight                           | [88ed3b7](https://github.com/oddessentials/ado-git-repo-insights/commit/88ed3b7) |
| 2026-03-27 | CI noise floor flakiness                          | Raised NOISE_FLOOR_MS to 5ms                           | [8a9857d](https://github.com/oddessentials/ado-git-repo-insights/commit/8a9857d) |
| 2026-03-27 | tsc errors escaped to CI (PR #207)                | Added `tsc --noEmit` to pre-commit hook                | [5d18b31](https://github.com/oddessentials/ado-git-repo-insights/commit/5d18b31) |
| 2026-03-27 | Suppression audit failed CI (PR #207)             | Made suppression audit strict for all branches         | [db5b04b](https://github.com/oddessentials/ado-git-repo-insights/commit/db5b04b) |
| 2026-03-27 | Smoke tests broke on filter HTML change (PR #207) | Updated selectors to `[role="option"]` + `data-testid` | [db5b04b](https://github.com/oddessentials/ado-git-repo-insights/commit/db5b04b) |

## Known Tradeoffs (Documented, Accepted)

| Tradeoff                                                     | Rationale                                                                                                                                                                                                                            |
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Python tests run on baseline Python only (not 3×3 matrix)    | Full matrix is slow; baseline catches the majority of issues. Cross-platform bugs (Windows paths, macOS timing) are CI-discovered.                                                                                                   |
| Python build is sdist-only (CI also builds wheel)            | sdist catches the majority of packaging issues. Wheel-only failures have not been observed in practice.                                                                                                                              |
| Scalability tests are CI-only                                | Synthetic data generation is expensive; run on demand locally via `pnpm run test:scalability`.                                                                                                                                       |
| Fresh-clone-verify is CI-only                                | Cannot simulate fresh clone without re-cloning. Verifies lockfile determinism.                                                                                                                                                       |
| mypy is Tier 2 (pre-push), not Tier 1 (pre-commit)           | Requires `[ml]` optional deps for full coverage; not every commit-stage environment has them. Pre-push catches errors before code leaves the machine. Parity is Match at Tier 2.                                                     |

## Governance

- **Adding a new CI check**: MUST add corresponding local equivalent in `run_repo_hook.py` or `run_pr_preflight.py` before merging. Update this document.
- **Trigger scope alignment**: Pre-commit trigger scope MUST match or exceed the effective compilation scope of the gate it guards. Any file included in a gate's tsconfig (or equivalent config) MUST have a corresponding trigger in `is_test_trigger()` or `is_ui_trigger()`. If a gate compiles files from multiple directories (e.g., `tsconfig.test.json` includes `tests/`, `ui/`, and `types/`), the trigger must fire for changes to any of those directories. Define triggers by what the compiler reads, not by what the developer intends to change. If a tsconfig gains a new include path, add a trigger and a regression test. Regression tests: [tests/unit/test_hook_triggers.py](tests/unit/test_hook_triggers.py).
- **Weakening a local check**: MUST document rationale in this file. CI-hard-gate checks must never exist in a weaker local mode. `python scripts/run_pr_preflight.py --allow-local-degraded` is diagnostic-only and never counts as parity.
- **Pre-commit selectivity (Tier 1 asymmetry)**: Pre-commit gates are intentionally selective — they run only when relevant files are staged, not unconditionally. This is a performance tradeoff: pre-push (Tier 2) runs unconditionally and catches anything pre-commit missed. The asymmetry is between Tier 1 and Tier 2, not between local and CI. This is by design, not a defect.
- **Clean-worktree guards**: Any pre-commit gate that reads the worktree (tsc, parity script, etc.) MUST have a corresponding clean-worktree guard covering its full input scope. The guard blocks commit if unstaged changes exist in any path the gate reads. Current guards: `require_clean_ui_sources()` (extension/ui/), `require_clean_test_compilation_scope()` (extension/tests/, extension/ui/, types/), `require_clean_tsconfigs()` (extension/tsconfig*.json).
- **Entry point alignment**: When adding a new gate to CI or pre-commit, also add it to `pnpm test:ci` in the root `package.json`. The root `test:ci` script is the documented local equivalent of the CI gate chain — if they diverge, `pnpm test:ci` can pass while CI fails. The `test:ci` script runs the suppression audit first, then all CI gates (ruff, mypy, pytest, tsc, eslint, jest, smoke).
- **Verify before commit**: Every new gate, guard, trigger, pathspec, or config change MUST be mechanically verified against real execution before committing. If you cannot point to terminal output proving the change works, do not commit it. Verify the success path AND the failure path.
- **Test count ratcheting**: The authoritative floor lives in [.test-floor-contract.json](.test-floor-contract.json). Both [run_pr_preflight.py](scripts/run_pr_preflight.py) and [ci.yml](.github/workflows/ci.yml) read it via `--min-collected-artifact`; no hardcoded integer exists in either file. The Python floor MUST use the **cross-platform minimum** collected count — see "Platform-Conditional Test Collection" below. Cross-OS equality of the Python floor is enforced by the `python-collection-parity` CI job which compares exact node_id sets between Ubuntu and Windows. The Extension floor uses the exact actual count (single CI environment, no matrix). Zero-tolerance on discovery count; `--max-skips` handles execution variance separately. When adding tests, bump `.test-floor-contract.json` by exactly the delta in the same commit. When removing tests, update with PR justification. Per-commit enforcement is done by **Tier 2 Row 27a** (`check_ratchet_bump.py`), which walks the first-parent range and compares per-commit `floor_delta` vs `actual_delta`. Bypass: `[ratchet-realignment]` or `[ratchet-test-removal]` in the **subject line of the specific commit** that introduced the delta (scanned via `git log --oneline`; markers in bodies are NOT honored; markers on other commits in the range do NOT cover unrelated commits). **Collection-time stability rule**: shared-floor Python tests must be defined unconditionally across the canonical interpreter lane. Python-version branching may change execution (for example, `pytest.skip(...)` inside the test body) but must not hide or create `def test_*` definitions at import time, or the collected floor will drift by interpreter version.
- **`--no-verify`**: Forbidden by project policy. Never bypass git hooks.
- **Reviewing this document**: After any CI pipeline change, verify parity by running the full preflight locally: `python scripts/run_pr_preflight.py`.
- **Split tsconfig architecture**: The extension uses separate configs for type checking (`tsconfig.json`: `module: "ES2022"`, `moduleResolution: "bundler"`) and build emission (`tsconfig.build.json`: `module: "CommonJS"`, `moduleResolution: "bundler"`). The build config MUST emit CommonJS — Node-executed scripts in `dist/` require CJS runtime semantics (`__dirname`, `require()`). The `build:tsc` script in `package.json` MUST reference `tsconfig.build.json` explicitly. **`dist/ui/` is owned exclusively by esbuild (`build:ui`)**; `tsconfig.build.json` MUST NOT include `ui/` — otherwise `build:tsc` silently overwrites IIFE bundles with CJS, breaking browser runtime. Guard: [build-output-format-guard.test.ts](extension/tests/meta/build-output-format-guard.test.ts) pins module + moduleResolution for each config, the build script entry point, and ui/ exclusion from build scope. When upgrading TypeScript, verify both configs resolve to their expected values.

## Platform-Conditional Test Collection

Python CI runs the OS × Python matrix declared in [.github/workflows/ci.yml](.github/workflows/ci.yml). Some tests are platform-specific (e.g., Windows console handler tests). These must **never** use `pytest.mark.skip`, `pytest.mark.skipIf`, or runtime `pytest.skip()` calls because CI enforces `--max-skips=0`. Any form of skip — declarative or runtime — counts against the zero-tolerance gate.

**Pattern**: Platform-conditional tests live in files named `test_*_windows.py` (or `_linux.py`, `_macos.py` when added). The glob patterns that match those files are declared once in [scripts/_platform_test_filters.py](scripts/_platform_test_filters.py) as `PLATFORM_CONDITIONAL_IGNORE_GLOBS` and imported by both [tests/conftest.py](tests/conftest.py) (`collect_ignore_glob` on non-Windows) and [scripts/check_ratchet_bump.py](scripts/check_ratchet_bump.py) (`--ignore-glob` CLI flags for its hermetic subprocess). A parity test in [tests/unit/test_platform_conditional_collection.py](tests/unit/test_platform_conditional_collection.py) asserts at the AST level that **both** call sites import the shared constant — either side dropping the import fails the test immediately, so the filter sets cannot drift.

This means:

- On Windows: all tests are collected (including `test_*_windows.py`)
- On Linux/macOS: files matching `PLATFORM_CONDITIONAL_IGNORE_GLOBS` are never discovered — they don't count as collected or skipped
- The `--min-collected` threshold MUST use the **cross-platform minimum** — the count pytest produces when the shared globs are applied. This is what Linux/macOS cells see and what Windows cells see after the filter is applied.

**Cross-platform minimum vs. Windows hermetic full count**: On a Windows machine, a hermetic `pytest --collect-only` without the platform-conditional `--ignore-glob` filter reports a number **larger** than the floor because it sees the platform-conditional files. The ratchet-bump gate prints both numbers — the floor-comparison number is labeled `cross-platform (Windows-filtered)` and the un-filtered number is labeled `Windows hermetic full count (no platform filter)` — so the Windows-machine developer can reconcile the two. Both numbers come from the same hermetic code path (plugins disabled, `PYTEST_ADDOPTS`/`PYTEST_PLUGINS` scrubbed, `-o addopts=` cleared); the Windows hermetic full count is NOT what a bare `pytest --collect-only` in an arbitrarily-configured developer shell would return — it is what Windows CI cells see, which is the relevant comparison point for ratchet-bump drift. The delta between the two numbers is derived at runtime from `scripts._platform_test_filters.count_platform_conditional_test_functions`, which AST-walks every file matched by `PLATFORM_CONDITIONAL_IGNORE_GLOBS` — never hardcoded, and extensible to Linux/macOS conventions without any helper edit. A Windows-only measured drift test at [tests/unit/test_platform_conditional_collection_windows.py](tests/unit/test_platform_conditional_collection_windows.py) fails if the AST-derived count diverges from the real pytest-measured count (e.g. when a `test_*_windows.py` test is parametrized without updating the helper).

**Adding new platform-conditional tests**:

1. Create a file whose name matches one of the patterns in `PLATFORM_CONDITIONAL_IGNORE_GLOBS` (currently `test_*_windows.py`). If you are adding a Linux- or macOS-only test, extend `PLATFORM_CONDITIONAL_IGNORE_GLOBS` with the new pattern — both conftest and the gate pick it up automatically.
2. Do NOT use `pytest.mark.skip` — use the collection exclusion pattern.
3. Re-measure the cross-platform minimum. The authoritative method is to run the ratchet-bump guard: `python scripts/check_ratchet_bump.py --base-ref origin/main --junit-extension extension/test-results.xml`. Its output line `actual=N (cross-platform (Windows-filtered))` is the correct floor regardless of the developer's OS. (Bare `pytest --collect-only` on a Windows machine over-reports by the platform delta.)
4. Update `python.min_collected` in [.test-floor-contract.json](.test-floor-contract.json) to the reported number in the same commit. Both [run_pr_preflight.py](scripts/run_pr_preflight.py) and [ci.yml](.github/workflows/ci.yml) read from this artifact — no other file needs editing. Cross-OS parity of the new floor is proven by the `python-collection-parity` CI job on each PR.

**Interpreter-version parity rule**: the shared Python floor is measured on the canonical Python lane (`3.12` in CI today), but collection must stay stable across supported interpreters for any test that contributes to that floor. Do not wrap `def test_*` definitions in module- or class-level `if _IS_BASELINE_PYTHON:` branches. If a behavior is baseline-only, define the test unconditionally and call `pytest.skip(...)` inside the body on non-baseline interpreters. That keeps collection stable while preserving runtime semantics.
