C04

The necessary implementation change is small, but its contract includes timing, reporting, policy exits, and operator recovery guidance. This is a static assessment; no tests or workflows were run.

Current behavior. In src/gzkit/smoke.py:120–131, run_smoke collects the suite and constructs TextTestRunner before calling time.perf_counter(). Consequently, the returned duration measures only runner.run(suite). Collection (:81–96) imports the full discovered unit suite, walks nested suites, and selects marked methods. Unmarked tests are imported but not executed.

src/gzkit/commands/smoke_cmd.py:59–79 uses the same returned duration for its summary and the elapsed > budget decision. Equality passes; failure takes precedence over excess duration (exit 1 versus 3). The default is the code constant SMOKE_BUDGET_SECONDS; --budget overrides it. Prose is not dynamically loaded as the threshold.

Before calling run_smoke, the gate performs a separate static marker scan. Empty static selection returns advisory success unless SmokeConfig.required is true; gzkit opts in. For populated tiers, budget enforcement applies regardless of that option (src/gzkit/config.py:137–154).

Necessary coupled obligations.

  • Start the monotonic measurement before collection and end it after execution. Preserve (TestResult, float), buffering, verbosity, full discovery, and marker selection. Measure actual work, rather than estimating or summing separately rounded phases.
  • Define the boundary explicitly: this change includes discovery, imports, selection, runner construction, and execution. It still excludes CLI startup and the earlier static marker scan. Including those would be a broader command-duration contract requiring a different boundary.
  • Keep the CLI summary and enforcement bound to that same total, with rounding confined to display. The framework runner can still print its own execution-only duration; distinguish the total in the CLI summary/documentation so two timings are intelligible.
  • Update the runner docstring, smoke manpage, relevant parser help, and breach guidance. Dropping a slow smoke marker does not remove that module from full discovery, so it cannot necessarily remedy a discovery-driven breach. Guidance must identify discovery/import/selection work as possible causes. Preserve the existing ceiling; moving the clock is not authorization to raise it. Any threshold amendment must change the executable authority, not only the rule prose.
  • Check propagation through quality.run_smoke_tier, the Smoke tier registration in commands/quality.py, and the test-isolation chore’s acceptance command. They consume the same CLI exit rather than independently timing smoke, so duplicate measurement is unnecessary. Preserve existing MX handling of policy-breach results; do not claim every exit 3 necessarily blocks every aggregate context.
  • Keep the full unit tier’s independent behavior. The health profiler reports full-suite wall time without a ceiling. Parser and breach text currently claim a “larger budget,” conflicting with the smoke manpage and canonical test rule; correct that coupled operator guidance when editing it.

Meaningful proposed verification. Use deterministic unittest fixtures with a fake monotonic clock advanced by discovery, selection, and execution. Assert the returned total equals all phases and retains the actual result. This must fail if timing starts after collection. Separately exercise collection with nested marked/unmarked cases, verifying only marked cases execute.

At the gate boundary, test execution below budget but total above it; assert policy exit and reported total agree. Cover below/equal/above boundary, failure plus overrun retaining exit 1, explicit/default budget, required-empty versus advisory-empty, and shell exit propagation. Use output-contract tests for rendered timing. Existing tests/test_smoke_gate.py mostly executes the real repository against default/zero budgets; those checks do not isolate discovery inclusion and can become host-dependent when discovery is counted.

Later verification should include a fresh-process smoke run, targeted regressions, and quality-wrapper propagation. Record observed timing and exit without treating warmed-import timing as cold-start evidence. None is claimed here.

Uncertainty. No runtime headroom was measured. Static markers can exist while runtime selection is empty; the inspected gate does not check testsRun == 0. Discovery error handling also deserves a separate assessment before promising fail-closed discovery behavior. These are recorded risks, not additional authorized changes. Brief ownership remains unestablished: targeted filename searches are not proof that no brief governs the work.

Read inventory and limits. Eight shell calls total, including task read and this output write. Fully read: /tmp/gzkit-current-inputs/C04.md; snapshot-relative src/gzkit/smoke.py, src/gzkit/commands/smoke_cmd.py, tests/test_smoke_gate.py, docs/user/manpages/smoke.md, .gzkit/rules/tests.md, tests/tools/test_health_profiler.py, .gzkit.json, src/gzkit/chores/test-isolation-compliance/acceptance.json. Excerpts: src/gzkit/quality.py, src/gzkit/commands/quality.py, src/gzkit/cli/parser_maintenance.py, src/gzkit/config.py. Searches covered src, tests, .github, docs/user, features, selected config files, and docs/design/adr. One broad design search inadvertently returned evaluation-scorecard snippets; they were not used as evidence. That search was truncated; a clean, fully restricted ownership assessment remains unfinished. All repository reads used /tmp/gzkit-current-snapshot; no checkout, history, network, source changes, or governance actions were used.