|
| 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) |
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)
| 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.
| 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.
| 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.
| 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.
| 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.
| 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.
| 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.
| 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.
| 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.