oct.git.policy module#

Purpose#

Encode the Phase 4B profile-aware enforcement rules for the oct git quality gate. Maps a combination of profile (proto / compact / strict), branch context, and raw check findings to a semantic exit code in the range 0..5 defined by blueprint §5.3.

Responsibilities#

  • Resolve the effective lint profile from CLI / octrc / branch context, honouring the protected-branch override (blueprint §7.2).

  • Apply the profile enforcement matrix (blueprint §5.3 / §7.1) to a QualityGateResult and return the matching exit code.

  • Enforce the invariant “secrets always blocks” regardless of profile (blueprint §5.3 final clause).

  • Manage the OCT-managed block in .gitignore — add/remove/list patterns inside the fenced block, leaving user-managed lines alone.

Diagnostics#

Domain: GIT Levels:

L3 — profile resolution and policy decisions L4 — per-field raw counts during exit-code derivation

Contracts#

  • This module is pure: no filesystem, no subprocess, no Click.

  • resolve_effective_profile never raises; unknown values collapse to the documented default.

  • apply_profile_policy depends only on the QualityGateResult dataclass defined in oct.git.quality_gate.

  • Precedence for the exit code is strictly: 5 > 4 > 3 > 2 > 1 > 0.

oct.git.policy.DEFAULT_BRANCH_PATTERNS: tuple[str, ...] = ('^(main|master|develop)$', '^(release|hotfix|feature|bugfix|docs|chore)/.+$')#

Default branch naming patterns (Phase 4E — G-E3, blueprint §7.1). Matches main/master/develop as exact names and common prefix-based patterns. Projects may override via git.branch_naming.patterns.

oct.git.policy.DEFAULT_PROFILE: Literal['proto', 'compact', 'strict'] = 'compact'#

Fallback profile used when no other source (CLI, config, branch override) selects one. Blueprint §7 names “compact” as the project default.

oct.git.policy.DEFAULT_PROTECTED_BRANCHES: tuple[str, ...] = ('main', 'master')#

Default protected branches written into .octrc.json when a project does not override them (blueprint §7.2). Branches listed here force the profile to strict regardless of CLI or config.

oct.git.policy.OC_GITIGNORE_MARKER_START: str = '# --- Option C defaults ---'#

Marker comments that delimit the OCT-managed block in .gitignore. The literals are kept stable for backwards compatibility with files created by older versions of oct git init.

oct.git.policy.PROFILE_MATRIX: dict[Literal['proto', 'compact', 'strict'], dict[str, str]] = {'compact': {'format': 'blocking', 'lint': 'blocking', 'secrets': 'blocking', 'tests': 'optional'}, 'proto': {'format': 'skip', 'lint': 'warn', 'secrets': 'blocking', 'tests': 'skip'}, 'strict': {'format': 'blocking', 'lint': 'blocking', 'secrets': 'blocking', 'tests': 'blocking'}}#

Profile enforcement matrix (blueprint §5.3 / §7.1, verbatim).

lint / format / tests values are one of:
  • "blocking": a violation produces a non-zero exit code.

  • "optional": the check runs and reports, but failures are advisory — exit code is unaffected (OI-531).

  • "warn": violations are reported but do not affect exit code.

  • "skip": the check is not run at all.

secrets is always "blocking" — see SECRETS_ALWAYS_BLOCKS.

oct.git.policy.SECRETS_ALWAYS_BLOCKS: bool = True#

secrets findings are always blocking. This is asserted as a unit test so any accidental weakening of PROFILE_MATRIX produces a CI failure rather than a silent security regression.

Type:

Invariant

oct.git.policy.VALID_PROFILES: tuple[Literal['proto', 'compact', 'strict'], ...] = ('proto', 'compact', 'strict')#

The three canonical profile names. Anything else resolves to DEFAULT_PROFILE.

oct.git.policy.add_ignore_patterns(repo_root: Path, patterns: list[str]) list[str][source]#

Append patterns to the OCT-managed block of .gitignore.

Creates the block (with markers) if it does not exist. Patterns already present anywhere in the file are skipped. Returns the list of patterns actually added (in input order, after dedup).

Caller is responsible for validating each pattern via validate_ignore_pattern() first.

oct.git.policy.apply_profile_policy(profile: str, result: QualityGateResult) int[source]#

Derive the semantic exit code for a quality-gate result.

Exit code table (blueprint §5.3, verbatim):

Code

Meaning

0

all checks pass

1

lint violations (blocking under this profile)

2

format violations (blocking under this profile)

3

both lint and format violations

4

test failures (when --include-tests)

5

secrets detected — always blocking

Precedence: 5 > 4 > 3 > 2 > 1 > 0. The first rule that matches wins. Because secrets takes precedence, a profile that would otherwise return 0 (e.g. proto with warnings-only lint) still returns 5 if secrets are present. This is the “secrets always blocks” invariant.

For profiles where a check is not blocking (proto lint is warn; proto format is skip), the corresponding violation counter never contributes to the exit code even if non-zero.

Parameters:
  • profile – One of "proto", "compact", "strict". Unknown values degrade to DEFAULT_PROFILE.

  • result – A QualityGateResult with the raw counts from a completed gate run.

oct.git.policy.check_protected_branch_commit(branch: str | None, octrc: dict | None, profile: str) tuple[bool, str][source]#

Check if committing directly to a protected branch.

Returns (blocked, message):

  • On a non-protected branch: (False, "").

  • On a protected branch in strict profile: (True, error_msg).

  • On a protected branch in other profiles: (False, warning_msg).

The caller is responsible for printing the message and aborting if blocked is True.

oct.git.policy.generate_branch_name_from_message(message: str) str | None[source]#

Derive a valid branch name from a Conventional Commits message.

Returns a branch name like feature/auth-add-jwt-tokens or None when the message cannot be parsed.

The generated name is guaranteed to pass validate_branch_name() against the default patterns.

oct.git.policy.is_protected_branch(branch: str | None, octrc: dict | None) bool[source]#

Return True if branch is listed in git.protected_branches.

A missing octrc or a missing git.protected_branches key falls back to DEFAULT_PROTECTED_BRANCHES. Detached HEAD (branch is None) is never considered protected — detached workflows are per-definition outside branch policy.

oct.git.policy.list_ignore_patterns(repo_root: Path) tuple[list[str], list[str]][source]#

Return (oct_managed, other) patterns from .gitignore.

Patterns inside the OCT-managed block are returned in the first list; everything else (user-managed) is returned in the second. Comment lines and blank lines are excluded from both.

oct.git.policy.remove_ignore_pattern(repo_root: Path, pattern: str) tuple[bool, str][source]#

Remove pattern from the OCT-managed block.

Returns (removed, message). Refuses to touch patterns located outside the OCT block (those are user-managed); the caller should surface the message to the user.

oct.git.policy.resolve_effective_profile(branch: str | None, octrc: dict | None, cli_override: str | None) Literal['proto', 'compact', 'strict'][source]#

Return the profile that should be applied for the current run.

Precedence (highest wins):

  1. Protected-branch override — if branch is listed in the project’s protected-branches set, the result is always "strict". This cannot be overridden by CLI or config.

  2. CLI ``–profile`` — explicit user override from the command line. Must be one of VALID_PROFILES.

  3. ``.octrc.json → git.pre_commit_profile`` — the persistent per-project default.

  4. ``.octrc.json → linter.profile`` — if the git-specific key is absent, fall back to the linter-wide profile.

  5. :data:`DEFAULT_PROFILE` — final fallback if nothing above is set.

Unknown profile strings (from any source) are silently discarded in favour of the next lower level of precedence. Malformed octrc values are also ignored — never raise.

oct.git.policy.validate_branch_name(branch: str | None, octrc: dict | None) tuple[bool, str][source]#

Validate branch against the configured naming patterns.

Returns (valid, message):

  • git.branch_naming.enabled is False(True, "")

  • branch is None (detached HEAD) → (True, "")

  • Branch matches at least one pattern → (True, "")

  • No pattern matches → (False, "Branch name '...' does not ...")

Patterns default to DEFAULT_BRANCH_PATTERNS when not overridden in .octrc.json.

oct.git.policy.validate_ignore_pattern(pattern: str) tuple[bool, str][source]#

Return (valid, message) for a candidate gitignore pattern.

Rejects empty strings, patterns containing newlines, leading ! (negation — too easy to weaponise via oct git ignore), absolute paths (leading / is interpreted as anchored to repo root which is allowed by gitignore but our policy is to require relative patterns), and any character outside _VALID_PATTERN_RE.