---
id: OBPI-0.0.20-01-validator-and-allowlist
parent: ADR-0.0.20-agent-rule-placement-invariant
item: 1
lane: Lite
status: Completed
---

# OBPI-0.0.20-01-validator-and-allowlist: Validator and Allow-list Foundation

## ADR Item

- **Source ADR:** `docs/design/adr/foundation/ADR-0.0.20-agent-rule-placement-invariant/ADR-0.0.20-agent-rule-placement-invariant.md`
- **Checklist Item:** #1 — Validator + allow-list foundation. Pydantic models (`UnscopedAllowlistEntry`, `Violation`, `UnscopedRulesResult`); `gz validate --unscoped-rules` flag; manifest schema fragment; initial allow-list with 3 entries (doomed files, `tracking_ref: ADR-0.0.20`); table-driven TDD tests; scorecard row addition; command-doc update.

**Status:** Draft

## Objective

Land `uv run gz validate --unscoped-rules` as a passing (non-violating) check against the current repo, via an initial manifest allow-list carrying the three files scheduled for OBPI-02/03/04 consolidation. This OBPI delivers the validator substrate (Pydantic models, CLI flag, manifest schema, classification logic, tests, scorecard row, command doc) WITHOUT migrating any rule file content — that is OBPI-02/03/04. After this OBPI lands, the anti-regression invariant is mechanically enforced: any new `paths: "**"` rule added to `.gzkit/rules/` without an allow-list entry fails `gz check` at Gate 2.

## Lane

**Lite** — New `--unscoped-rules` flag is additive on the existing `gz validate` subcommand per `.gzkit/rules/cli.md` § New Flag. The manifest schema extension is additive (new optional `rules.unscoped_allowlist` key). No subcommand added, no breaking schema change, no runtime contract change, no external consumer impact.

> Heavy is reserved for command/API/schema/runtime-contract changes. Process,
> documentation, and template-only work stays Lite unless it changes one of
> those external surfaces.

## Allowed Paths

- `src/gzkit/validators/unscoped_rules.py` — new; Pydantic models + classification logic
- `src/gzkit/validators/__init__.py` — touched only if package needs initialization
- `src/gzkit/cli/parser_validate.py` — register `--unscoped-rules` flag
- `src/gzkit/schemas/manifest.schema.json` — extend with `rules.unscoped_allowlist` schema fragment
- `.gzkit/manifest.json` — add initial allow-list entries for the three doomed files
- `tests/validators/__init__.py` — new, empty test package init
- `tests/validators/test_unscoped_rules.py` — new; table-driven unittest tests
- `docs/governance/advisory-rules-audit.md` — add scorecard row for the new Mechanical rule
- `docs/user/commands/validate.md` — document `--unscoped-rules` flag with example
- `docs/design/adr/foundation/ADR-0.0.20-agent-rule-placement-invariant/ADR-0.0.20-agent-rule-placement-invariant.md` — parent ADR (read-only; not modified)

## Denied Paths

- `.gzkit/rules/agent-contract.md` / `attestation-enrichment.md` / `defect-fix-routing.md` — NOT deleted in this OBPI (allow-list only; deletion is OBPI-02/03/04)
- `AGENTS.md` — content migration is OBPI-02/03/04
- `CLAUDE.md` — addendum addition is OBPI-02
- `docs/governance/agent-contract-rationale.md` / `arb-middleware.md` / `defect-fix-routing.md` — created in OBPI-02/03/04
- Any content migration from the three rule files
- New third-party dependencies (uses stdlib + existing Pydantic + existing YAML parser)
- Mirror files under `.claude/rules/` / `.github/instructions/` (auto-regenerated by sync; not hand-edited)
- Any file mutation outside the Allowed Paths above

## Requirements (FAIL-CLOSED)

1. REQUIREMENT: Pydantic models `UnscopedAllowlistEntry`, `Violation`, `UnscopedRulesResult` are defined in `src/gzkit/validators/unscoped_rules.py` as `BaseModel` subclasses with `model_config = ConfigDict(frozen=True, extra="forbid")`. Stdlib `dataclass` is NEVER used.
2. REQUIREMENT: `UnscopedAllowlistEntry` fields: `file: str`, `rationale: str` (with `min_length=20`), `tracking_ref: str` (with `pattern=r"^(GHI-\d+|ADR-[\d.]+[-\w]*)$"`), `added_date: date`. `Violation` fields: `file: str`, `reason: str` (value in `{"missing-paths", "universal-glob"}`), `allowlisted: bool`, `detected_value: str | None = None`. `UnscopedRulesResult` fields: `scope: str = "unscoped-rules"`, `result: str` (value in `{"pass", "fail"}`), `violations: list[Violation]`, `allowlist_entries: list[UnscopedAllowlistEntry]`, `canonical_root: str`, `files_checked: int`, `exit_code: int`.
3. REQUIREMENT: `--unscoped-rules` flag is registered in `src/gzkit/cli/parser_validate.py`. Help text describes the scope, includes one example invocation, and keeps the line length ≤ 80 chars per `.gzkit/rules/cli.md` § Help Text Requirements.
4. REQUIREMENT: The validator enumerates canonical `.gzkit/rules/*.md` via `pathlib.Path` globbing. Mirrors under `.claude/rules/`, `.github/instructions/`, `.agents/rules/` are NEVER enumerated — the sync contract (GHI #210) guarantees mirror fidelity.
5. REQUIREMENT: YAML frontmatter is parsed via an existing project YAML helper (no new `yaml` library imports). The validator inspects only the `paths:` key; other fields are ignored.
6. REQUIREMENT: Classification rules: `paths` absent OR `paths: null` → `reason="missing-paths"` VIOLATION. `paths: "**"` (string) OR `paths: ["**"]` (single-item list) → `reason="universal-glob"` VIOLATION. `paths:` with any other concrete glob → PASS. A file in `rules.unscoped_allowlist` is marked `allowlisted=True` in its `Violation` record but does NOT fail the check.
7. REQUIREMENT: Exit codes per `.gzkit/rules/cli.md` 4-code map — 0 (all PASS or ALLOWLISTED), 2 (I/O error: missing manifest, malformed YAML, unreadable rule file), 3 (policy breach: one or more non-allowlisted VIOLATIONs).
8. REQUIREMENT: `--json` flag emits `UnscopedRulesResult` as JSON to stdout. Log messages go to stderr. JSON output is parseable via `json.loads` and roundtrips through the Pydantic model.
9. REQUIREMENT: `--allowlist-only` flag lists the current allow-list entries with rationale and tracking_ref in human-readable form; exits 0.
10. REQUIREMENT: `--fix` flag is NOT implemented in v1. Autofix would guess wrong (fold vs. narrow vs. allow-list is judgment).
11. REQUIREMENT: `src/gzkit/schemas/manifest.schema.json` is extended with a schema fragment under `properties.rules.properties.unscoped_allowlist` that matches the Pydantic `UnscopedAllowlistEntry` contract (`type: array`, `items.required: [file, rationale, tracking_ref, added_date]`, `items.additionalProperties: false`, per-field type + constraint). The fragment is additive — no existing schema property is removed or changed.
12. REQUIREMENT: `.gzkit/manifest.json` gains `rules.unscoped_allowlist` populated with exactly three entries for the doomed files — each `rationale="Pending consolidation per OBPI-0X"` (X=2, 3, 4 respectively), `tracking_ref="ADR-0.0.20"`, `added_date="2026-04-21"`.
13. REQUIREMENT: `tests/validators/test_unscoped_rules.py` contains table-driven `unittest.TestCase` tests covering — (a) `paths:` absent → VIOLATION missing-paths; (b) `paths: null` → VIOLATION missing-paths; (c) `paths: "**"` string form → VIOLATION universal-glob; (d) `paths: ["**"]` list form → VIOLATION universal-glob; (e) `paths: "src/**"` broad-but-concrete → PASS; (f) `paths: ["tests/**"]` concrete list → PASS; (g) file in allow-list with universal-glob → ALLOWLISTED (not gate fail); (h) malformed YAML → exit 2; (i) missing manifest → exit 2; (j) `--json` output roundtrips through `UnscopedRulesResult`; (k) exit 0 when all rules clean; (l) exit 3 when any non-allowlisted VIOLATION present.
14. REQUIREMENT: Tests use `tempfile.TemporaryDirectory` for fixture filesystem state; NEVER touch the live repo root or the live manifest. Tests use stdlib `unittest` per `.gzkit/rules/tests.md` (no pytest).
15. REQUIREMENT: Coverage for `src/gzkit/validators/unscoped_rules.py` is ≥ 40.00% per `.gzkit/rules/tests.md` Coverage Floor.
16. REQUIREMENT: `docs/governance/advisory-rules-audit.md` gains a new section header "Agent Rule Placement (`ADR-0.0.20-agent-rule-placement-invariant`)" with one row table entry scoring the invariant as `Mechanical`, citing `gz validate --unscoped-rules` as the enforcement. The scorecard summary counts at the top of the file are updated to reflect the new Mechanical rule.
17. REQUIREMENT: `docs/user/commands/validate.md` gains a section for the `--unscoped-rules` scope with — one example invocation, the exit-code table (0/2/3), the allow-list recovery guidance, and a cross-reference link to the parent ADR.
18. REQUIREMENT: `uv run gz validate --unscoped-rules` exits 0 against the repo state AFTER the allow-list entries are added. AFTER this OBPI lands, the three doomed files are ALLOWLISTED (expected); no other rule files under `.gzkit/rules/` have `paths: "**"` or missing `paths:`.
19. REQUIREMENT: `--unscoped-rules` is included in the `gz validate --all` aggregate flag expansion. `gz check` invokes the validator as part of its aggregate quality pass.
20. REQUIREMENT: The validator NEVER writes files, NEVER modifies the manifest, NEVER edits rule files, NEVER calls an LLM, NEVER uses `shell=True`, and NEVER reads files outside `.gzkit/rules/*.md` and `.gzkit/manifest.json`.

> STOP-on-BLOCKERS: if prerequisites are missing, print a BLOCKERS list and halt.

## Discovery Checklist

**Governance (read once, cache):**

- [ ] `.github/discovery-index.json` — repo structure
- [ ] `AGENTS.md` — agent operating contract
- [ ] Parent ADR — understand full context

**Context:**

- [ ] Parent ADR: `docs/design/adr/foundation/ADR-0.0.20-agent-rule-placement-invariant/ADR-0.0.20-agent-rule-placement-invariant.md`
- [ ] `.gzkit/rules/skill-surface-sync.md` — understand sync contract (why mirrors aren't checked)
- [ ] `docs/governance/advisory-rules-audit.md` — understand scorecard layout for the new row
- [ ] `docs/governance/trust-doctrine.md` § T2 — allow-list must be explicit, not silent
- [ ] `.gzkit/rules/cli.md` — new-flag discipline and 4-code exit map
- [ ] `src/gzkit/validators/` — existing validators for pattern reference
- [ ] `src/gzkit/cli/parser_validate.py` — existing `gz validate` flag registrations

**Prerequisites (check existence, STOP if missing):**

- [ ] `src/gzkit/cli/parser_validate.py` exists
- [ ] `src/gzkit/schemas/manifest.schema.json` exists
- [ ] `.gzkit/manifest.json` exists
- [ ] `docs/governance/advisory-rules-audit.md` exists
- [ ] `docs/user/commands/validate.md` exists
- [ ] Existing `gz validate --<scope>` pattern reference (e.g., `--taxonomy`, `--pydantic-models`, `--type-ignores`) available for consistency

**Existing Code (understand current state):**

- [ ] Review an existing `gz validate --<scope>` handler for output formatting and exit-code patterns
- [ ] Confirm YAML parsing helper location (`src/gzkit/utils/` or similar) — avoid introducing a new `yaml` dependency
- [ ] Review `tests/validators/` fixtures if any exist; follow existing test conventions

## Quality Gates

### Gate 1: ADR

- [ ] Intent and scope recorded in this OBPI brief
- [ ] Parent ADR checklist item quoted

### Gate 2: TDD (Red-Green-Refactor)

- [ ] Tests derived from the Requirements above, not from implementation
- [ ] Red-Green-Refactor cycle followed per behavior increment (observe failing test, minimum code to green, refactor)
- [ ] Tests pass: `uv run gz test`
- [ ] Validation commands recorded in Evidence with real outputs

### Code Quality

- [ ] Lint clean: `uv run gz lint`
- [ ] Type check clean: `uv run gz typecheck`
- [ ] Coverage ≥ 40% for new module

## Verification

```bash
# Validator functionality
uv run gz validate --unscoped-rules
uv run gz validate --unscoped-rules --json
uv run gz validate --unscoped-rules --allowlist-only

# Aggregate integration
uv run gz validate --all
uv run gz check

# Tests
uv run -m unittest tests/validators/test_unscoped_rules -v

# Quality
uv run gz lint
uv run gz typecheck
uv run gz arb coverage run -m unittest discover -s tests/validators -t .
```

## Acceptance Criteria

- [ ] REQ-0.0.20-01-01: Three Pydantic models defined with `ConfigDict(frozen=True, extra="forbid")`
- [ ] REQ-0.0.20-01-02: Model fields match the contract (file, rationale with min_length=20, tracking_ref regex, added_date; Violation reason enum; UnscopedRulesResult structure)
- [ ] REQ-0.0.20-01-03: `--unscoped-rules` flag registered with example in help text, line length ≤ 80
- [ ] REQ-0.0.20-01-04: Validator enumerates canonical only (mirrors untouched)
- [ ] REQ-0.0.20-01-05: YAML parsing via existing helper; no new `yaml` import
- [ ] REQ-0.0.20-01-06: Classification rules as specified (missing/universal/concrete, allow-list short-circuit)
- [ ] REQ-0.0.20-01-07: Exit codes 0/2/3 per cli.md 4-code map
- [ ] REQ-0.0.20-01-08: `--json` emits parseable JSON roundtripping through Pydantic
- [ ] REQ-0.0.20-01-09: `--allowlist-only` lists entries cleanly and exits 0
- [ ] REQ-0.0.20-01-10: No `--fix` flag (v1 deliberately excludes)
- [ ] REQ-0.0.20-01-11: `manifest.schema.json` fragment additive and matches Pydantic contract
- [ ] REQ-0.0.20-01-12: `.gzkit/manifest.json` has 3 initial allow-list entries (agent-contract.md / attestation-enrichment.md / defect-fix-routing.md) with proper rationale and tracking_ref
- [ ] REQ-0.0.20-01-13: Table-driven tests cover all 12 classification + error scenarios listed
- [ ] REQ-0.0.20-01-14: Tests use `tempfile.TemporaryDirectory` and `unittest`
- [ ] REQ-0.0.20-01-15: Coverage ≥ 40.00% for `src/gzkit/validators/unscoped_rules.py`
- [ ] REQ-0.0.20-01-16: Scorecard row added to `advisory-rules-audit.md` with Mechanical score, summary counts updated
- [ ] REQ-0.0.20-01-17: Command doc `validate.md` documents `--unscoped-rules` with example, exit codes, recovery guidance, parent-ADR link
- [ ] REQ-0.0.20-01-18: `uv run gz validate --unscoped-rules` exits 0 against final repo state (3 allow-listed, 0 violations)
- [ ] REQ-0.0.20-01-19: Flag included in `gz validate --all` and `gz check` aggregates
- [ ] REQ-0.0.20-01-20: Validator is READ-ONLY — never writes files, never calls LLMs, never uses `shell=True`

## Completion Checklist

- [ ] **Gate 1 (ADR):** Intent recorded in brief
- [ ] **Gate 2 (TDD):** RGR cycle followed, tests derived from REQs, coverage maintained
- [ ] **Code Quality:** Lint, typecheck, coverage clean
- [ ] **Value Narrative:** Problem-before vs capability-now is documented
- [ ] **Key Proof:** One concrete usage example is included
- [ ] **OBPI Acceptance:** Evidence recorded below

> For ceremony steps and lane-inheritance attestation rules, see `AGENTS.md` section `OBPI Acceptance Protocol`.

## Evidence

### Gate 1 (ADR)

- [ ] Intent and scope recorded

### Gate 2 (TDD — Red-Green-Refactor)

```text
# Paste test output here
```

### Code Quality

```text
# Paste lint/typecheck/coverage output here
```

### Value Narrative

<!-- What problem existed before this OBPI, and what capability exists now? -->

### Key Proof


$ uv run gz validate --unscoped-rules
Validated: unscoped-rules
✓ 16 rule file(s) checked (3 allowlisted).
$ echo $?
0

ARB receipts anchoring this evidence: lint arb-ruff-db63c70cdd70473bb28572498af2c14b; typecheck arb-step-typecheck-823db51d9d6948859d48daa71cfb1826; tests arb-step-unittest-4414c4fd30b9432188bd911bd9d006ad (3498/3498 green); coverage arb-step-coverage-5eac4a61004c4dd0a3ea881806716bd2 (93% on src/gzkit/validators/unscoped_rules.py; floor 40%). Also verified: uv run gz validate --audits composes unscoped_rules across 5 scopes (exit 0), uv run gz validate --advisory-scorecard accepts row 47 (exit 0).

### Implementation Summary


- Files created: src/gzkit/validators/__init__.py, src/gzkit/validators/unscoped_rules.py (137 LOC, 93% cov), tests/validators/__init__.py, tests/validators/test_unscoped_rules.py (30 tests).
- Files modified: src/gzkit/cli/parser_maintenance.py (flag registration), src/gzkit/commands/validate_cmd.py (dedicated handler + integrated runner), src/gzkit/commands/quality.py + src/gzkit/quality.py (gz check step), src/gzkit/sync_surfaces.py (preserve rules block across manifest regen), src/gzkit/schemas/manifest.json (schema fragment), .gzkit/manifest.json (3 seed allowlist entries), docs/governance/advisory-rules-audit.md (row 47; mechanical 33->34), docs/user/commands/validate.md (new section), tests/commands/test_skills.py (mock added).
- Tests added: 30 REQ-derived unittests with @covers decoration across all 20 REQs.
- Date completed: 2026-04-22.
- Attestation status: Operator-attested Stage 4 Normal-mode ceremony.
- Defects noted: Four brief Allowed-Paths drifts fixed in-scope per Prime Directive #2. Pre-existing trust_audits.py size-ceiling overrun (1520 lines) observed but out of scope.

## Tracked Defects

_No defects tracked._

## Human Attestation

- Attestor: `Jeffry Babb`
- Attestation: attest completed — Normal-mode ceremony: validator substrate lands with 93% coverage on src/gzkit/validators/unscoped_rules.py, 3 allowlist entries seeded pending OBPI-02/03/04 consolidation, gz validate --unscoped-rules exits 0 against live repo, --audits composition and gz check integration verified. Four brief Allowed-Paths drifts flagged and fixed in-scope (parser_validate.py→parser_maintenance.py, validate_cmd.py missing, quality.py missing, sync_surfaces.py manifest-preservation gap). Receipts: lint arb-ruff-db63c70c; types arb-step-typecheck-823db51d; tests arb-step-unittest-4414c4fd; coverage arb-step-coverage-5eac4a61.
- Date: 2026-04-22

---

**Brief Status:** Completed

**Date Completed:** 2026-04-22

**Evidence Hash:** -
