C06

The change belongs at run_smoke’s measurement boundary, with coupled reporting, recovery guidance, and deterministic regression coverage. This is a static assessment; no tests or workflows were executed.

Current behavior:

  • src/gzkit/smoke.py:81–95 discovers the full unittest tree, imports test modules, walks the discovered suite, and selects marked methods. At lines 127–131, run_smoke completes that collection and constructs the buffered runner before starting perf_counter. Its returned duration therefore measures runner execution, excluding discovery and selection.
  • src/gzkit/commands/smoke_cmd.py:35–79 performs a separate static marker scan first. An empty scan passes advisorially unless smoke.required is true; gzkit itself opts in. Once markers exist, the budget applies regardless of that setting. The gate prints the returned duration and compares that same unrounded value with the budget. Test failure takes precedence over a budget breach; equality passes because comparison is >.
  • SMOKE_BUDGET_SECONDS in code supplies the default; --budget accepts a float. Changing rule prose alone does not change enforcement. smoke_cmd translates nonzero results into SystemExit; cli.main preserves those codes. quality.run_smoke_tier invokes the CLI, and commands/quality.py:643 registers its result in gz check. No separate budget implementation is needed there.

Necessary coupled obligations:

  1. Start the monotonic measurement before collection and stop after runner completion, retaining the (TestResult, float) return contract, buffering, verbosity, marker membership, and existing exit precedence. Define the boundary explicitly: this includes discovery, selection, runner setup and execution, but does not inherently include CLI startup, the preliminary static marker scan, config loading, or final gate printing. Counting the entire command would require a broader boundary decision. This remains post-run enforcement, not an interrupting timeout.
  2. Make the returned aggregate the duration used both for the smoke summary and the verdict. Clarify its meaning in the runner docstring, smoke manpage and CLI help. The underlying TextTestRunner emits its own timing summary; its execution-only number must not be mistaken for the aggregate gate measurement.
  3. Repair directly coupled breach recovery text. Dropping @smoke can reduce execution but does not avoid discovery imports: expensive unmarked modules also affect the proposed aggregate. Advise diagnosing collection/import costs as well as slow members. Do not silently increase the default to compensate. Current advice to amend only .gzkit/rules/tests.md misidentifies the runtime threshold authority.
  4. Preserve adopter opt-in behavior and the full unit tier’s absence of a fixed ceiling. Existing help/recovery claims that the full suite has a “larger budget” disagree with the read test rule and manpage. Reconcile touched explanations. If canonical rule wording changes, its packaged/delivered copies are coupled surfaces; a timer move alone does not require changing doctrine or generated files.

Meaningful proposed verification:

  • Use unittest.mock and a controlled clock with distinct discovery, filtering and execution costs. A runner-only measurement must fail the assertion; merely supplying two canned clock values would not establish where the first read occurred. Keep fixtures independent of repository runtime and avoid sleeps.
  • Exercise an execution duration below the ceiling whose aggregate exceeds it: returned duration, displayed aggregate and exit 3 must agree. Cover aggregate equality, below-budget success, and failed tests that also exceed budget retaining exit 1. Check the CLI wrapper and quality-result propagation.
  • Preserve discovery’s actual marked/unmarked selection semantics, buffering and result identity. Keep required-empty and optional-empty cases. Existing gate tests mostly invoke the real repository with zero/default budgets; they do not prove discovery is timed. The QC negative control plants an empty opted-in tier, so it witnesses emptiness only, not elapsed-time enforcement.
  • After implementation authorization, collect fresh-process evidence; existing in-process tests can benefit from cached imports. No actual headroom or platform performance is established here.

Uncertainty: the static marker precheck can disagree with runtime selection, and collection does not explicitly propagate loader errors. Those are separate correctness questions, not reasons to silently redesign selection in this timing change. A targeted brief-path search did not establish ownership; governance initiation is outside this assessment.

Read inventory (snapshot-relative; full files or excerpts): src/gzkit/{smoke.py,quality.py,config.py}, src/gzkit/commands/{smoke_cmd.py,quality.py}, src/gzkit/cli/{parser_maintenance.py,main.py}, src/gzkit/governance/trust_audits/{_qc_negative_controls.py,_qc_nc_entrypoints.py}, tests/test_smoke_gate.py, tests/cli/test_smoke_tier.py, .gzkit/rules/tests.md, .gzkit.json, docs/user/manpages/{smoke.md,check.md}. Searches also covered src, tests, .gzkit, .github, docs/user, docs/design/adr, remaining permitted docs, and data/check_step_scopes.json; a guessed classes path did not exist. Broad search accidentally returned an EVALUATION_SCORECARD.md match; it was not opened or used as evidence, but this was an exclusion-boundary slip. Task input: /tmp/gzkit-current-inputs/C06.md. Nine shell/read calls total, including this output write. No source changes.