// SPDX-License-Identifier: CC-BY-SA-4.0
= Exemption mechanisms in the hyperpolymath estate
:toc:

Three concentric exemption layers exist in the estate. Each addresses a
different question and lives in a different file. Mixing them up is the
single most common cause of "I added an ignore file but CI still fails"
confusion.

== Quick decision tree

[source]
----
Are you trying to stop bots from WRITING to a path?
  -> use the estate-wide bot_exclusion_registry.a2ml

Are you trying to stop the governance gate from FAILING on a
pre-existing finding?
  -> use the per-repo .hypatia-baseline.json

Are you trying to make a finding GO AWAY for one specific PR only?
  -> currently unsupported. See "per-PR exemptions" below.

Are you trying to record that a shortfall EXISTS, is tolerated for now,
and must never grow?
  -> that is not an exemption. Use the per-repo .machine_readable/Debtfile.a2ml
     See docs/DEBTFILE-SPEC.adoc.
----

[NOTE]
====
*Exemption is not debt.* The layers below record findings you have decided
*not to act on* — the finding is excused, and the ledger's job is to keep the
excuse visible. A `Debtfile.a2ml` entry records a shortfall you *do* intend to
act on, carrying a probe that re-measures it and a ceiling that may only fall.

The two have their own ratchets, and they answer different questions:

* `check-exemption-ratchet.sh` — "did you quietly excuse more?"
* `check-debt-ratchet.sh` — "did the number go up?"

Layer 4b below (the `.claude/CLAUDE.md` exemption tables, which carry a *count*
and an *unblock condition*) is the case where the two got conflated. Those
tables are debt wearing an exemption's clothing, and they migrate to
`Debtfile.a2ml`.
====

== Layer 1: Estate-wide bot write denylist

**File:** `hyperpolymath/standards/.machine_readable/bot_exclusion_registry.a2ml`

**Scope:** All bots, all repos. Authoritative across the entire estate.

**Purpose:** Tells bots what they may not *write* to. Read-only scanning
is always allowed; only write actions (Write, Commit, CreatePr,
CreateBranch, etc.) are gated.

**Format:** A2ML (TOML-shaped). Three axes:
1. `external-repos` — exact `owner/repo` matches that bots must skip.
2. `vendored-directory-patterns` — globs that should never be edited.
3. `remote-origin-patterns` — bail-if-origin-matches patterns.

**Consumer:** Every bot via `shared-context::ExclusionRegistry::check()`.
The registry is fail-closed: if the file fails to load, all bot writes
are denied.

**Edit when:** A new repo should be off-limits to bots, or a new
vendored subtree should never be modified.

**Don't use for:** Suppressing CI findings. This layer doesn't touch
CI at all.

== Layer 2: Per-repo Hypatia finding baseline

**File:** `.hypatia-baseline.json` at the calling repo's root.

**Scope:** One repo. Acknowledged findings only — not a "ignore these
forever" file.

**Purpose:** Tells the governance gate which Hypatia findings have been
seen, triaged, and accepted as known debt. Findings matched by a baseline
entry don't block CI; they're surfaced in the run summary as
"acknowledged."

**Format:** JSON array of `{severity, rule_module, type, file | file_pattern, ...}`
objects. See `docs/HYPATIA-BASELINE-FORMAT.adoc` for the full schema.

**Consumer:** `apply-baseline.sh` invoked from
`hyperpolymath/standards/.github/workflows/governance-reusable.yml`.
Suppresses matching findings from the gate, downgrades severity if
`severity_override` is set, fails on expired entries.

**Edit when:**
* Pre-existing debt is tracked for migration (link a `tracking_issue`).
* Vendored / third-party code triggers a rule the team can't fix.
* Test fixtures intentionally violate a rule (with `note` explaining).

**Don't use for:**
* Newly-introduced findings on freshly-authored code. The rule exists
  for a reason; baselining the new file defeats it. Fix the code.
* One-off PR-scoped suppression. Baseline edits are merged to main; they
  affect all subsequent PRs. See Layer 3.

== Layer 3: Per-PR exemptions (NOT YET IMPLEMENTED)

There is currently no supported per-PR exemption mechanism. PR authors
attempting one (e.g. by inventing a `.hypatia-ignore` file) will find
nothing reads it, and the gate will still fail. **Do not invent new
file conventions.** If you need per-PR exemption, file an issue against
`hyperpolymath/standards` proposing a designed mechanism.

If a mechanism is added, it should be one of:

1. **PR-body marker**, e.g. a fenced block:
+
[source,markdown]
----
```hypatia-exempt
- rule: code_safety/obj_magic
  file: bots/sustainabot/bot-integration/src/Analysis.res
  reason: Pre-migration debt; tracked in #148
```
----
+
Pros: lives with the PR, vanishes when the PR closes, reviewable in
the same UI as the change. Cons: requires a PR-body parser.

2. **Label-driven**, e.g. apply `gate:exempt:code_safety` and require an
exemption-justification comment from a CODEOWNER.

3. **Sidecar file under `.github/pr-exemptions/`** — committed for one
PR's lifetime, deleted at merge by a workflow.

A formal proposal should pick one and document it explicitly.

== Anti-patterns to reject

These have been attempted and should be refused at review:

* `.hypatia-ignore` (any format) — never read by anything. Reject; point
  the author at `.hypatia-baseline.json`.
* Adding `pragma: ignore-rule` comments in source. Hypatia scans by AST
  and ignores comments; suppression has to be data-driven, not
  source-comment driven.
* Setting `continue-on-error: true` on the governance gate. This makes
  CI lie. Use a baseline entry with `severity_override: advisory`
  instead — keeps the finding visible while unblocking the gate.
* Adding the offending path to `.gitignore`. The file still exists in
  history; Hypatia and other estate scanners still see it.
* Forking `hyperpolymath/standards` to disable the rule for one repo.
  If a rule is wrong for one repo, the rule needs scoping in the
  upstream, not a fork.

== Layer 4: Governance language-policy TS-allowlist

A separate sub-system from Layers 1-3 (which gate the Hypatia
scanner). This layer gates the `governance / Language / package
anti-pattern policy` workflow step that enforces "no new TypeScript"
across the estate. Three sub-layers:

=== 4a: Built-in path / filename allowlist

Hard-coded in `scripts/check-ts-allowlist.affine` (source of truth;
compiled to `scripts/check-ts-allowlist.deno.js` which the workflow
invokes). Covers paths that are *always* exempt regardless of per-repo
configuration:

* Directory segments: `bindings`, `tests`, `test`, `scripts`,
  `mcp-adapter`, `cli`, `vendor`, `examples`, `ffi`, `node_modules`,
  `benchmarks`.
* Any segment containing `vscode` or starting with `deno-`.
* Filename patterns: `*.d.ts`, `mod.ts`, `lsp-server.ts`, `lsp.ts`,
  `*-lsp.ts`, `*.bench.ts`, `*_bench.ts`.

Changing this layer requires a standards-repo PR.

=== 4b: Per-repo `.claude/CLAUDE.md` heading-table

Per-repo exemptions documented as a markdown table under a heading
that contains both a language token and `Exemption[s]`. The
heading-match regex is:

----
^#{1,4}\s+.*(?:TypeScript|JavaScript|TS|JS|\.tsx?)\b[^#\n]*[Ee]xemption
----

Matches all of:

* `### TypeScript Exemptions (Approved)`
* `### TypeScript / JavaScript Exemptions (Approved)`
* `### TypeScript Exemption`
* `### .ts Exemptions (foo bar)`

Table rows have shape `| \`<glob>\` | <count> | <rationale> | <unblock-condition> |`.

Multi-section: every heading on the page is scanned, so a repo can
keep separate exemption tables (e.g. one for `.ts` bridge code, one
for vendored test fixtures).

Best for: exemptions whose *rationale* belongs in human-readable
documentation alongside the prose explaining why the repo exists.

=== 4c: `.governance-allowlist` (typed infrastructure file)

Optional plain-text file at the repo root. One glob per line. Lines
starting with `#` are comments. Blank lines are ignored.

----
# Telegram bot binding (no AffineScript binding planned)
avow-protocol/telegram-bot/avow-telegram-bot/**

# Bench harness fixtures
test/perf/fixtures/*.ts
----

Best for: exemptions that are *infrastructure config* (typed,
machine-validated) rather than human-readable rationale prose. Pairs
cleanly with the estate's "machine-readable everywhere" trajectory.
Decoupled from CLAUDE.md heading text so reformatting the docs
doesn't accidentally break the gate.

Both 4b and 4c are additive — declaring an exemption in either is
sufficient. Most repos will pick one or the other.

=== Picking between 4b and 4c

[cols="1,3,3"]
|===
| Layer | Use when | Avoid when

| 4b (CLAUDE.md table)
| The exemption needs human-readable rationale next to the glob.
  Reviewers reading CLAUDE.md should immediately understand *why*.
| The repo's CLAUDE.md is large and heading-text drift breaks the
  gate.

| 4c (`.governance-allowlist`)
| The exemption is structural and the rationale belongs elsewhere
  (tracked in an issue / ADR / commit message). Or the repo wants
  the gate-pass list to live in version-controlled infrastructure
  rather than prose.
| The exemption is one-off and CLAUDE.md prose is the right home.
|===

=== History

* Original implementation: inline Python heredoc in
  `governance-reusable.yml`, regex literal `TypeScript [Ee]xemptions`.
  Failed silently on every `affinescript`-style heading (the slash and
  `JavaScript` between keywords meant the regex never matched, and 3
  legitimate exemptions were dropped → gate red on every PR).
* Python eradication (standards#189): ported to
  `scripts/check-ts-allowlist.ts`. Brittle regex carried over.
* Layer 4b fix + Layer 4c introduction (2026-05-27): regex relaxation
  + new typed-infrastructure file. Owner picked the Layer-2.5 approach
  (standards#185) over the minimal regex-only fix (standards#183).
  This document seeds the doctrine.
* AffineScript port (standards#283 seed, #310 compile/runtime fixes,
  #311 workflow swap): `.ts` → `.affine` self-referential port under
  the TS→AffineScript campaign (#239 / #241 STEP 2). The `.ts`
  archetype is kept for the regression suite and parallel-validation;
  the workflow now runs the compiled `.deno.js`. Retirement of the
  `.ts` is a follow-up after the dual-target window.

== Cross-references

* `docs/HYPATIA-BASELINE-FORMAT.adoc` — the baseline file format.
* `.machine_readable/hypatia-baseline.schema.json` — machine schema.
* `scripts/check-ts-allowlist.affine` — the AffineScript source of
  truth for the Layer 4 detector (since standards#283 / #310 / #311).
* `scripts/check-ts-allowlist.deno.js` — the compiled artifact the
  governance workflow runs.
* `scripts/check-ts-allowlist.ts` — the Deno archetype, retained as the
  regression-suite target (`scripts/tests/check-ts-allowlist-test.sh`)
  and for parallel-validation during the TS→AS dual-target window.
* `hyperpolymath/standards#????` — proposal that landed this consumer.
* `hyperpolymath/hypatia` — the scanner that emits findings.
