oct
Loading...
Searching...
No Matches
oct.git.quality_gate Namespace Reference

Classes

class  QualityGateResult
class  PreCommitResult

Functions

PreCommitResult pre_commit_checks (Path project_root, str branch, str message, bool force_commit, str profile, dict octrc)
list[Path] _resolve_scope (Path project_root, Literal["staged", "changed", "all"] scope)
list[Path] _walk_secret_filenames (Path project_root, *, exclude_dirs, wildcard_exclude_dirs, patterns)
Path _gate_diagnostics_dir (Path project_root)
 _build_linter_context (Path project_root)
 _build_formatter_context (Path project_root, *, bool fix_mode=False)
list[str] _finding_kinds (str message)
list[tuple[str, int, str]] _run_secrets_pass (list[Path] all_files, Path project_root, *, float code_entropy_threshold=4.5, float docstring_entropy_threshold=5.0, float comment_entropy_threshold=5.0, str secret_match_mode="substring", list[dict]|None entropy_exclusions=None, list[str]|None excluded_field_patterns=None)
dict _apply_octrc_to_lint (Path project_root, ctx, dict active_rules, *, str|None profile=None)
tuple[int, list[dict]] _run_lint_pass (list[Path] py_files, Path project_root, str profile, bool fix_mode=False)
set[str] _octrc_disabled_lint_rules (Path project_root)
set[str] _disabled_fixer_rules_for (str rel_posix, set[str] run_disabled, list path_rules)
frozenset _disabled_fixers_for (set disabled_rules)
int _run_format_pass (list[Path] py_files, Path project_root, bool fix_mode=False)
int _run_test_pass (Path project_root, bool sandbox=False)
QualityGateResult run_quality_gate (Path project_root, Literal["staged", "changed", "all"] scope, Literal["proto", "compact", "strict"] profile, bool include_tests=False, bool fix=False, bool sandbox=False)
list[tuple[str, int, str]] run_secrets_scan_on_files (Path project_root, list[Path] files)
QualityGateResult run_quality_gate_on_files (Path project_root, list[Path] files, Literal["proto", "compact", "strict"] profile, bool include_tests=False, bool fix=False, bool sandbox=False)

Variables

tuple _FINDING_KINDS
dict _FORMAT_FIXER_RULES

Detailed Description

Purpose
-------
Orchestrate the Phase 4B unified quality gate: lint + format dry-run +
secrets detection on a scoped set of files. Returns a structured
:class:`QualityGateResult` that downstream consumers (``oct git check``,
``oct git commit``, hooks, MCP) can inspect and render.

Responsibilities
----------------
- Resolve file scope (staged / changed / all) via :mod:`oct.core.git`.
- Run the linter on Python files via :func:`oct.linter.oct_lint._lint_single_file`.
- Run the formatter in dry-run mode via :func:`oct.formatter.oct_format.format_file`.
- Run secrets detection (content + filename pattern) via
  :func:`oct.linter.oct_lint.check_no_hardcoded_secrets` and
  :data:`oct.core.exclusions.NEVER_EXPORT_FILE_PATTERNS`.
- Expose the secrets pass alone, configured exactly as the gate runs it,
  as :func:`run_secrets_scan_on_files` (``oct git commit``'s plan-208
  full-index check uses it instead of the whole gate).
- Optionally run pytest via subprocess (``--include-tests``).
- Support ``--fix`` two-pass mode: detect → fix → re-verify.

Diagnostics
-----------
Domain: GIT
Levels:
    L2 - lifecycle: gate start/end, scope resolution
    L3 - details: per-pass summary counts
    L4 - deep trace: per-file check outcomes

Contracts
---------
- This module does not import ``click``. It is a pure orchestrator.
- It calls existing library functions (linter, formatter, exclusions)
  directly; it never shells out to ``oct lint`` or ``oct format``.
- The only subprocess call is for pytest (``--include-tests``), which is
  a distinct tool boundary.
- :func:`run_quality_gate` never raises on check failures; it returns
  a :class:`QualityGateResult` with the findings. Only infrastructure
  errors (missing git repo, broken imports) may propagate.

Dependencies
------------
- oct.core.diagnostics (_dbg structured logger)
- oct.core.paths (to_project_relative - project-relative paths in secrets findings and for the format pass's path-rule lookup)
- oct.core.exclusions (lazy; excluded directories and never-export file patterns for the all scope and the secrets pass)
- oct.core.git (lazy; git_staged_files, git_changed_files and git_staged_deletions - the scoped file sets)
- oct.core.octrc (lazy; load_octrc_safe - each subproject's linter rules and secret-scanner settings)
- oct.core.option_c_dir (lazy; resolve_debug_config - the diagnostics directory for the linter and formatter contexts)
- oct.core.waivers (lazy; inline waivers applied to secrets findings and to the format pass)
- oct.core.workspace (lazy; the workspace manifest, for merging each subproject's entropy exclusions)
- oct.formatter.oct_format (lazy; FormatterContext and format_file - the format pass)
- oct.git.conventional (lazy; commit message and AI-Model trailer validation in the pre-commit checks)
- oct.git.policy (lazy; the protected-branch check, profile resolution and PROFILE_MATRIX)
- oct.linter.oct_lint (lazy; _lint_single_file, the lint profiles and path rules - the lint pass)
- oct.mcp.sandbox (lazy; SandboxExecutor - the sanitised pytest run when sandbox is set)
- oct.tools.secret_scanner (lazy; check_no_hardcoded_secrets - the always-on secrets pass)
- pytest (not imported; run as python -m pytest for the --include-tests test pass)

Function Documentation

◆ _apply_octrc_to_lint()

dict oct.git.quality_gate._apply_octrc_to_lint ( Path project_root,
ctx,
dict active_rules,
* ,
str | None profile = None )
protected
Honor the owning subproject's ``.octrc.json`` in the gate lint pass.

Plan 177 - ``_run_lint_pass`` previously built its rule set and linter
context from the PROFILE only, so an octrc-waived rule (e.g. ``oct``'s
``dbg_import: false``) re-fired and false-blocked the workspace-root
fan-out. This mirrors the mechanism the standalone linter already uses
(:func:`oct.linter.oct_lint.run_linter`), reusing its exact semantics:

- ``linter.profile`` names the profile per-rule SEVERITY is resolved at
  (plan 269). Without this the context kept its ``"strict"`` default, so
  :func:`~oct.linter.rule_registry.get_severity` answered at strict
  severities in the gate and at the project's own profile in
  ``oct lint`` - the same divergence class plans 177 and 262 closed for
  the rule SET. *profile* is the profile the gate was called with and
  is used when the octrc names no valid one; a name outside
  ``_PROFILES`` is refused on both sides rather than passed through,
  since ``get_severity`` would silently answer at rule defaults for it;
- ``linter.rules`` overrides merge on top of the profile defaults
  (explicit ``false`` disables, ``true`` enables; bool values only);
- ``linter.path_rules`` (plan 268) populate ``ctx.path_rules``, which
  ``_lint_single_file`` applies per file after both of the above, so
  the gate and ``oct lint`` reach the same verdict on a file a path
  entry names. Line-scoped rules are refused by the parser;
- ``secret_scanner`` entropy thresholds + ``entropy_exclusions``
  populate the linter context so the lint-pass ``no_hardcoded_secrets``
  check honors the same ``exempt_entropy_only`` path exclusions.

The SECRETS pass (:func:`_run_secrets_pass`) is a separate, ALWAYS-ON
pass and is NOT touched here - octrc can never disable secret scanning
(G-D4). ``exempt_entropy_only`` exempts entropy MAGNITUDE only; a
name/pattern-matched secret still fires in both passes.

*ctx* is mutated in place; the (possibly) updated *active_rules* dict is
returned. The fan-out passes each slice's own subproject as
``project_root``, so the octrc loaded here is that subproject's.

Definition at line 545 of file quality_gate.py.

Here is the caller graph for this function:

◆ _build_formatter_context()

oct.git.quality_gate._build_formatter_context ( Path project_root,
* ,
bool fix_mode = False )
protected
Build a ``FormatterContext`` for dry-run (or fix) mode.

Definition at line 383 of file quality_gate.py.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ _build_linter_context()

oct.git.quality_gate._build_linter_context ( Path project_root)
protected
Build a minimal ``LinterContext`` for the quality gate.

We import lazily to avoid pulling Click and the full linter on
import of this module.

Definition at line 365 of file quality_gate.py.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ _disabled_fixer_rules_for()

set[str] oct.git.quality_gate._disabled_fixer_rules_for ( str rel_posix,
set[str] run_disabled,
list path_rules )
protected
Return the lint rules disabled FOR ONE FILE, the per-path answer.

Plan 269 - the format-pass twin of what plan 268 gave the lint pass.
:func:`_octrc_disabled_lint_rules` answers for the whole run, which is
the wrong grain for a config whose whole purpose is that a test tree
and its package answer differently: a module a ``tests/`` path entry
excused from ``dbg_import`` passed the gate's lint pass and was still
refused by its format pass, on the same rule, in the same commit.

The run-wide set is the starting point and every matching path entry
then moves what it names: a rule the entry sets ``False`` joins the
set, one it sets ``True`` leaves it - including a rule ``linter.rules``
disabled run-wide, which is the only way a project can keep a fixer
where it wants it and drop it everywhere else. Entry order and the
refusals (line-scoped rules, ``_``-prefixed mode markers) are
:func:`~oct.linter.oct_lint.effective_rules`'s, not restated here, so
the gate's format pass and ``oct lint`` cannot drift apart on what a
path entry means.

G-D4 is untouched by construction and by test: the secrets pass never
consults this, and no key of :data:`_FORMAT_FIXER_RULES` names a
secret rule - ``effective_rules`` would refuse it in any case.

Parameters
----------
rel_posix
    The file's project-relative POSIX path, as
    :func:`~oct.core.paths.to_project_relative` renders it.
run_disabled
    The run-wide disables from :func:`_octrc_disabled_lint_rules`.
    Never mutated - one set is shared across the whole pass.
path_rules
    Validated ``(prefix, {rule: bool})`` pairs from
    :func:`~oct.linter.oct_lint.parse_path_rules`.

Definition at line 732 of file quality_gate.py.

Here is the caller graph for this function:

◆ _disabled_fixers_for()

frozenset oct.git.quality_gate._disabled_fixers_for ( set disabled_rules)
protected
Translate disabled LINT RULES into the FIXER names to skip.

:data:`_FORMAT_FIXER_RULES` maps fixer name to lint rule; this reads
it in the other direction, which is the direction the formatter needs
- :func:`~oct.formatter.oct_format.format_file` knows fixers, and
everything upstream of it speaks rules.

The map is an identity today, so the translation is a filter; it is
written as a translation anyway so that a fixer whose name ever
differs from its rule cannot quietly disable the wrong one. A rule
with no fixer (``dbg_usage``, every line-scoped rule) simply names no
fixer and disappears here, which is the second reason G-D4 cannot be
reached from this path.

Definition at line 794 of file quality_gate.py.

Here is the caller graph for this function:

◆ _finding_kinds()

list[str] oct.git.quality_gate._finding_kinds ( str message)
protected
The kinds of finding in a scanner message, as fixed names only.

The message itself can quote part of the flagged value, so a log record
carries this instead. A finding matching no known kind is ``other``.

Definition at line 411 of file quality_gate.py.

◆ _gate_diagnostics_dir()

Path oct.git.quality_gate._gate_diagnostics_dir ( Path project_root)
protected
Return the diagnostics directory the gate's contexts should carry.

Plan 213 / T0: derived from the ONE resolver,
:func:`oct.core.option_c_dir.resolve_debug_config`, instead of the
hard-coded ``<root>/oc_diagnostics``. On a project with no config
anywhere the resolver returns the ``oc_diagnostics/`` path, so this
is byte-identical to the old expression; on a migrated project it
correctly yields ``.option_c``, and on a pre-v0.17 project the legacy
``diagnostics/`` directory - which is what the linter's deprecation
warning keys off.

Definition at line 348 of file quality_gate.py.

Here is the caller graph for this function:

◆ _octrc_disabled_lint_rules()

set[str] oct.git.quality_gate._octrc_disabled_lint_rules ( Path project_root)
protected
Return the lint rules the subproject's ``.octrc.json`` disables.

Plan 177 - a rule with an explicit ``false`` under ``linter.rules`` is
honored by the gate's lint pass (via :func:`_apply_octrc_to_lint`) and,
for the fixers listed in :data:`_FORMAT_FIXER_RULES`, by the format
pass. The SECRETS pass is never consulted here - octrc can't disable it.

Definition at line 711 of file quality_gate.py.

Here is the caller graph for this function:

◆ _resolve_scope()

list[Path] oct.git.quality_gate._resolve_scope ( Path project_root,
Literal["staged", "changed", "all"] scope )
protected
Return the list of files to check based on *scope*.

For ``"staged"`` and ``"changed"`` we return **all** file types so
that filename-pattern secrets detection can flag non-Python files
like ``.env`` or ``*.pem``. The caller filters to ``.py`` for the
lint and format passes.

For ``"all"`` we use the linter's own ``find_python_files`` for the
Python set, but also walk the tree for non-Python secret-filename
checks.

Definition at line 240 of file quality_gate.py.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ _run_format_pass()

int oct.git.quality_gate._run_format_pass ( list[Path] py_files,
Path project_root,
bool fix_mode = False )
protected
Run the formatter in dry-run (or fix) mode, return violation count.

A "violation" is a file where ``format_file`` reports a change from a
fixer whose lint rule is NOT disabled for THAT FILE by the owning
subproject's octrc. Plan 177 (format-pass twin): a change produced
solely by an octrc-disabled fixer (e.g. ``dbg_import`` on oct's
``oct.core`` diagnostics shim) is suppressed, mirroring the lint pass.
Plan 269: ``linter.path_rules`` are resolved per file by
:func:`_disabled_fixer_rules_for`, so this pass and the lint pass
reach the same verdict on a file a path entry names - before, a path
entry reached the lint pass only and the format pass refused the file
the lint pass had just cleared. The SECRETS pass is unaffected.

Plan 277 closes the last of the three: the file's OWN inline
``# OCT-LINT: disable[-file]=<rule>`` waivers join the per-file
disabled set, on exactly the linter's terms
(:func:`~oct.core.waivers.inline_disabled_rules`). The file text is
read here to parse them, as the secrets pass already reads it, and a
file that cannot be read yields no waivers - ``format_file`` reads it
again and reports the failure through its own error channel.
:data:`~oct.core.waivers.LINE_SCOPED_RULES` cannot enter the set, so
a ``no_hardcoded_secrets`` waiver disables no fixer here and G-D4 is
untouched by construction as well as by test.

The octrc is read once per pass, not once per file: the run-wide
disables and the parsed path rules are both resolved before the loop.

Definition at line 815 of file quality_gate.py.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ _run_lint_pass()

tuple[int, list[dict]] oct.git.quality_gate._run_lint_pass ( list[Path] py_files,
Path project_root,
str profile,
bool fix_mode = False )
protected
Run the linter on *py_files* and return (violation_count, per_file).

Uses ``_lint_single_file`` directly, bypassing the CLI. The profile
selects which rules are active via ``_PROFILES``; the owning
subproject's ``.octrc.json`` then merges its ``linter.rules`` disables
and ``secret_scanner`` entropy exclusions on top (Plan 177), mirroring
the standalone linter. The SECRETS pass is unaffected.

Definition at line 651 of file quality_gate.py.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ _run_secrets_pass()

list[tuple[str, int, str]] oct.git.quality_gate._run_secrets_pass ( list[Path] all_files,
Path project_root,
* ,
float code_entropy_threshold = 4.5,
float docstring_entropy_threshold = 5.0,
float comment_entropy_threshold = 5.0,
str secret_match_mode = "substring",
list[dict] | None entropy_exclusions = None,
list[str] | None excluded_field_patterns = None )
protected
Check files for hardcoded secrets (content) and secret filenames.

Returns a list of ``(relative_path, line, reason)`` tuples.

Definition at line 424 of file quality_gate.py.

Here is the caller graph for this function:

◆ _run_test_pass()

int oct.git.quality_gate._run_test_pass ( Path project_root,
bool sandbox = False )
protected
Run ``python -m pytest tests/`` and return the failure count.

Returns 0 if all tests pass (exit code 0), or 1 if any test fails
(non-zero exit code). This is a subprocess boundary - tests are a
distinct tool and their execution is not inline.

When ``sandbox=True`` (OI-517), the pytest subprocess runs under a
sanitised environment via :class:`oct.mcp.sandbox.SandboxExecutor`:
secret-named env vars are stripped, ``PYTHONPATH`` is dropped, and a
600s wall-clock ceiling is enforced. Output is capped at 1 MiB so a
runaway test cannot flood stderr.

Definition at line 922 of file quality_gate.py.

Here is the caller graph for this function:

◆ _walk_secret_filenames()

list[Path] oct.git.quality_gate._walk_secret_filenames ( Path project_root,
* ,
exclude_dirs,
wildcard_exclude_dirs,
patterns )
protected
Yield non-Python files whose name matches any ``patterns`` glob.

Uses ``os.walk`` with in-place pruning of ``dirnames`` so excluded
trees are never descended into. Mirrors the exclusion semantics of
:func:`oct.linter.oct_lint.find_python_files` - if a directory name
is in ``exclude_dirs`` (exact) or matches one of the wildcard entries
(prefix/suffix/glob), it is pruned from the walk.

Definition at line 302 of file quality_gate.py.

Here is the caller graph for this function:

◆ pre_commit_checks()

PreCommitResult oct.git.quality_gate.pre_commit_checks ( Path project_root,
str branch,
str message,
bool force_commit,
str profile,
dict octrc )
OI-527: run the three pre-gate checks and collapse their outcomes
into a single :class:`PreCommitResult`.

The helper replaces the four sequential early-return blocks that
previously lived in ``git_commit_cmd`` (commit-message validation,
staged-files presence, protected-branch guard). Exit codes and side
effects match the original CLI behaviour byte-for-byte:
  * ``exit_code == 3`` - invalid commit message (skipped when
    ``force_commit`` is True).
  * ``exit_code == 4`` - nothing staged at all. A staged set of pure
    deletions is NOT nothing: it returns ``ok`` with an empty
    :attr:`~PreCommitResult.staged_files`, because the gate has no content
    to inspect in files that are being removed.
  * ``exit_code == 1`` - protected-branch block.

Definition at line 131 of file quality_gate.py.

◆ run_quality_gate()

QualityGateResult oct.git.quality_gate.run_quality_gate ( Path project_root,
Literal["staged", "changed", "all"] scope,
Literal["proto", "compact", "strict"] profile,
bool include_tests = False,
bool fix = False,
bool sandbox = False )
Orchestrate lint + format-dry-run + secrets on the scoped files.

When ``fix=True``, runs in two passes:

1. **Detect pass:** run the gate with ``fix=False`` to record the
   initial state.
2. **Fix pass:** run lint and format with ``fix_mode=True`` to apply
   auto-fixes.
3. **Re-verify pass:** run the gate again with ``fix=False``. The
   returned result reflects the post-fix state.

Secrets are **never** auto-fixed (no safe auto-fix for a hardcoded
password). The ``--fix`` flag does not bypass the always-blocking
secrets rule.

Parameters
----------
project_root
    Resolved project root containing ``.octrc.json``, ``tests/``, etc.
scope
    ``"staged"`` checks only the git index; ``"changed"`` checks
    staged + unstaged + untracked; ``"all"`` checks every Python file.
profile
    Lint profile: ``"proto"`` / ``"compact"`` / ``"strict"``.
include_tests
    If True, also run ``pytest`` as a subprocess (exit code 4 on fail).
fix
    If True, attempt auto-fix of lint/format violations, then re-check.
sandbox
    OI-517: when True and ``include_tests`` is True, the pytest
    subprocess runs under the MCP sandbox environment - sanitised
    env, no network side-channels via PYTHONPATH, output capped.

Definition at line 969 of file quality_gate.py.

Here is the call graph for this function:

◆ run_quality_gate_on_files()

QualityGateResult oct.git.quality_gate.run_quality_gate_on_files ( Path project_root,
list[Path] files,
Literal["proto", "compact", "strict"] profile,
bool include_tests = False,
bool fix = False,
bool sandbox = False )
Run the quality gate on an explicit file list.

Identical contract to :func:`run_quality_gate` but accepts a
pre-resolved list of files instead of computing one from a scope
string. This is the entry point used by F1.3 per-subproject
fan-out at workspace-level commit, where the workspace-wide
staged set is grouped into subproject buckets and each bucket
runs under its own profile.

Parameters
----------
project_root
    Resolved project root for octrc / linter-context / secrets
    config loading. For a per-subproject call, pass the
    subproject's path; for a workspace-level call, pass the
    workspace root.
files
    Pre-filtered list of files to lint / format / scan. May
    include non-Python files (passed through to the secrets
    scanner; lint/format ignore them via the suffix filter).
profile
    Lint profile: ``"proto"`` / ``"compact"`` / ``"strict"``.
include_tests, fix, sandbox
    Same semantics as :func:`run_quality_gate`.

Definition at line 1117 of file quality_gate.py.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ run_secrets_scan_on_files()

list[tuple[str, int, str]] oct.git.quality_gate.run_secrets_scan_on_files ( Path project_root,
list[Path] files )
The gate's secrets pass alone, configured exactly as the gate runs it.

Reads the scanner settings from *project_root*'s octrc, merges every
workspace subproject's ``entropy_exclusions`` when *project_root* is a
workspace root, and scans *files* (all file types). This is the one
place that configuration is assembled: :func:`run_quality_gate_on_files`
calls it for its secrets pass, and ``oct git commit``'s plan-208
full-index check calls it directly, so the two scans cannot drift apart
and the full-index check no longer re-runs lint and format only to
discard them (found by the chart C runtime trace, 2026-09-27).

Returns
-------
list of ``(relative_path, line, reason)`` tuples, as
:func:`_run_secrets_pass` does.

Definition at line 1028 of file quality_gate.py.

Here is the call graph for this function:
Here is the caller graph for this function:

Variable Documentation

◆ _FINDING_KINDS

tuple oct.git.quality_gate._FINDING_KINDS
protected
Initial value:
1= (
2 ("high-entropy string", "high-entropy string"),
3 (" assigned ", "secret-named assignment"),
4 ("dict key", "secret-named dict key"),
5 ("parameter", "secret-named parameter default"),
6 ("unparseable", "unparseable file"),
7)

Definition at line 402 of file quality_gate.py.

◆ _FORMAT_FIXER_RULES

dict oct.git.quality_gate._FORMAT_FIXER_RULES
protected
Initial value:
1= {
2 "header": "header",
3 "docstring": "docstring",
4 "dbg_import": "dbg_import",
5 "func_pattern": "func_pattern",
6}

Definition at line 703 of file quality_gate.py.