Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6e87e6a
feat(cf13): add gate CLI execution module
TheHalfMoon Aug 26, 2026
e5f7f0d
feat(cf13): wire commandf gate CLI
TheHalfMoon Aug 26, 2026
75ba99e
test(cf13): add gate parse exit contract
TheHalfMoon Aug 26, 2026
a4370d6
test(cf13): add gate end-to-end regressions
TheHalfMoon Aug 26, 2026
c1f2fc9
test(cf13): add deterministic gate proof
TheHalfMoon Aug 26, 2026
e832a40
ci(cf13): add deterministic quality-gate proof
TheHalfMoon Aug 26, 2026
7fbd2fd
style(cf13): apply rustfmt to gate behavior tests
TheHalfMoon Aug 26, 2026
64aa2f9
style(cf13): apply rustfmt to gate proof
TheHalfMoon Aug 26, 2026
701f2fe
test(cf13): prove exact suppression membership
TheHalfMoon Aug 26, 2026
cf477ec
test(cf13): replace incomplete suppression regression
TheHalfMoon Aug 26, 2026
4069bc0
style(cf13): apply rustfmt to gate regression
TheHalfMoon Aug 26, 2026
a620d3e
ci(cf13): run renamed gate behavior target
TheHalfMoon Aug 26, 2026
1683f63
fix(cf13): bound verified cache reads
TheHalfMoon Aug 26, 2026
1373074
fix(cf13): bound and bind primary gate inputs
TheHalfMoon Aug 26, 2026
8ff2cad
fix(cf13): bind proof to exact source head
TheHalfMoon Aug 26, 2026
d928f97
test(cf13): prove bounded gate inputs
TheHalfMoon Aug 26, 2026
e2647dd
style(cf13): apply rustfmt to bounded gate loader
TheHalfMoon Aug 26, 2026
f82612c
style(cf13): apply rustfmt to gate bounds regression
TheHalfMoon Aug 26, 2026
c7d7332
ci(cf13): use safe-directory for proof provenance
TheHalfMoon Aug 26, 2026
08c5072
test(cf13): cover oversized optional gate inputs
TheHalfMoon Aug 26, 2026
49c78bd
fix(cf13): correct oversized-input test fixture
TheHalfMoon Aug 26, 2026
bdf34d7
test(cf13): cover oversized suppression input
TheHalfMoon Aug 26, 2026
06da4f3
test(cf13): consolidate optional input bounds coverage
TheHalfMoon Aug 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions .github/workflows/cf13-quality-gate-proof.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
name: cf13-quality-gate-proof

on:
pull_request:
paths:
- .github/workflows/cf13-quality-gate-proof.yml
- Cargo.toml
- Cargo.lock
- crates/commandf-pkg/**
- crates/commandf-cli/**
- specs/014-cf-13-baselines-suppression-quality-gates/**
- .specify/memory/constitution.md
- AGENTS.md
push:
branches:
- feat/cf13-quality-gate-cli
paths:
- .github/workflows/cf13-quality-gate-proof.yml
- Cargo.toml
- Cargo.lock
- crates/commandf-pkg/**
- crates/commandf-cli/**
- specs/014-cf-13-baselines-suppression-quality-gates/**
- .specify/memory/constitution.md
- AGENTS.md
workflow_dispatch:

permissions:
contents: read

env:
CF13_PROOF_CONTAINER: docker.io/library/rust@sha256:9146b0f62e1939989aa96fc8d89699a43c5635bf212819235a773e1a9e71a98f
CF13_SOURCE_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
Comment thread
qodo-code-review[bot] marked this conversation as resolved.

jobs:
deterministic-quality-gate:
runs-on: ubuntu-24.04
container:
image: docker.io/library/rust@sha256:9146b0f62e1939989aa96fc8d89699a43c5635bf212819235a773e1a9e71a98f
timeout-minutes: 15
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09
with:
ref: ${{ env.CF13_SOURCE_SHA }}
persist-credentials: false

- name: Assert exact source checkout
run: |
set -euo pipefail
test "$(git -c safe.directory="$GITHUB_WORKSPACE" rev-parse HEAD)" = "$CF13_SOURCE_SHA"

- name: Assert pinned execution toolchain
run: |
set -euo pipefail
rustc --version --verbose
cargo --version
test "$(rustc --version | awk '{print $2}')" = "1.97.1"

- name: Prove CF-13 CLI contract and deterministic evidence
run: |
set -euo pipefail
cargo test --locked -p commandf --test gate_behavior_v1 --test gate_bounds --test gate_exit_contract
cargo test --locked -p commandf --test gate_determinism_proof -- --nocapture --test-threads=1 \
| tee /tmp/cf13-quality-gate-proof.log
grep -oE 'CF13_(GATE|BASELINE_CANONICAL|SUPPRESSION_CANONICAL|BEFORE_ARCHIVE|AFTER_ARCHIVE)_SHA256=(sha256:)?[0-9a-f]{64}' \
/tmp/cf13-quality-gate-proof.log \
| sort \
| tee /tmp/cf13-quality-gate-proof.env
grep -q '^CF13_GATE_SHA256=[0-9a-f]\{64\}$' /tmp/cf13-quality-gate-proof.env
grep -q '^CF13_BASELINE_CANONICAL_SHA256=sha256:[0-9a-f]\{64\}$' /tmp/cf13-quality-gate-proof.env
grep -q '^CF13_SUPPRESSION_CANONICAL_SHA256=sha256:[0-9a-f]\{64\}$' /tmp/cf13-quality-gate-proof.env

- name: Record immutable repository evidence
run: |
set -euo pipefail
git_safe() {
git -c safe.directory="$GITHUB_WORKSPACE" "$@"
}
{
echo "CF13_SOURCE_SHA=$CF13_SOURCE_SHA"
echo "CF13_SOURCE_TREE=$(git_safe rev-parse 'HEAD^{tree}')"
echo "CF13_WORKFLOW_BLOB=$(git_safe rev-parse "$CF13_SOURCE_SHA:.github/workflows/cf13-quality-gate-proof.yml")"
echo "CF13_AGENTS_BLOB=$(git_safe rev-parse "$CF13_SOURCE_SHA:AGENTS.md")"
echo "CF13_CONSTITUTION_BLOB=$(git_safe rev-parse "$CF13_SOURCE_SHA:.specify/memory/constitution.md")"
echo "CF13_SPEC_BLOB=$(git_safe rev-parse "$CF13_SOURCE_SHA:specs/014-cf-13-baselines-suppression-quality-gates/spec.md")"
echo "CF13_PLAN_BLOB=$(git_safe rev-parse "$CF13_SOURCE_SHA:specs/014-cf-13-baselines-suppression-quality-gates/plan.md")"
echo "CF13_TASKS_BLOB=$(git_safe rev-parse "$CF13_SOURCE_SHA:specs/014-cf-13-baselines-suppression-quality-gates/tasks.md")"
echo "CF13_CARGO_LOCK_BLOB=$(git_safe rev-parse "$CF13_SOURCE_SHA:Cargo.lock")"
echo "CF13_CF04_COMPATIBILITY_BLOB=$(git_safe rev-parse "$CF13_SOURCE_SHA:crates/commandf-pkg/src/compatibility.rs")"
echo "CF13_CF05_CHECK_BLOB=$(git_safe rev-parse "$CF13_SOURCE_SHA:crates/commandf-pkg/src/check.rs")"
echo "CF13_GATE_LIBRARY_BLOB=$(git_safe rev-parse "$CF13_SOURCE_SHA:crates/commandf-pkg/src/gate.rs")"
echo "CF13_GATE_CLI_BLOB=$(git_safe rev-parse "$CF13_SOURCE_SHA:crates/commandf-cli/src/gate.rs")"
echo "CF13_PROOF_TEST_BLOB=$(git_safe rev-parse "$CF13_SOURCE_SHA:crates/commandf-cli/tests/gate_determinism_proof.rs")"
echo "CF13_WORKFLOW_SHA256=$(sha256sum .github/workflows/cf13-quality-gate-proof.yml | awk '{print $1}')"
echo "CF13_CARGO_LOCK_SHA256=$(sha256sum Cargo.lock | awk '{print $1}')"
echo "CF13_SPEC_SHA256=$(sha256sum specs/014-cf-13-baselines-suppression-quality-gates/spec.md | awk '{print $1}')"
echo "CF13_PLAN_SHA256=$(sha256sum specs/014-cf-13-baselines-suppression-quality-gates/plan.md | awk '{print $1}')"
echo "CF13_TASKS_SHA256=$(sha256sum specs/014-cf-13-baselines-suppression-quality-gates/tasks.md | awk '{print $1}')"
echo "CF13_PROOF_TEST_SHA256=$(sha256sum crates/commandf-cli/tests/gate_determinism_proof.rs | awk '{print $1}')"
echo "CF13_RUSTC=$(rustc --version)"
echo "CF13_CONTAINER=$CF13_PROOF_CONTAINER"
echo "CF13_CHECKOUT_ACTION=fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09"
echo "CF13_UPLOAD_ACTION=ea165f8d65b6e75b540449e92b4886f43607fa02"
} >> /tmp/cf13-quality-gate-proof.env
sort -o /tmp/cf13-quality-gate-proof.env /tmp/cf13-quality-gate-proof.env

- name: Assert repository remains clean
run: |
set -euo pipefail
status="$(git -c safe.directory="$GITHUB_WORKSPACE" status --porcelain)"
test -z "$status"

- name: Upload CF-13 deterministic evidence
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: cf13-quality-gate-proof
path: /tmp/cf13-quality-gate-proof.env
if-no-files-found: error
retention-days: 3
164 changes: 164 additions & 0 deletions crates/commandf-cli/src/gate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
use std::path::PathBuf;
use std::process::ExitCode;

use clap::{Args, ValueEnum};
use commandf_pkg::{
classify_structural_diff, diff_package_archives, evaluate_compatibility_policy,
evaluate_quality_gate, CheckDirection, CheckFailOn, CheckPolicy, CheckReport, GateSuppressions,
Lockfile, PackageCache, PackageName, StructuralDiffReport,
};

use super::{read_bounded_file, select_locked_package, write_check_output};

const MAX_GATE_LOCKFILE_INPUT_BYTES: u64 = 16 * 1024 * 1024;
const MAX_GATE_ARCHIVE_INPUT_BYTES: u64 = 128 * 1024 * 1024;
const MAX_GATE_BASELINE_INPUT_BYTES: u64 = 64 * 1024 * 1024;
const MAX_GATE_SUPPRESSIONS_INPUT_BYTES: u64 = 64 * 1024 * 1024;

#[derive(Args)]
pub(crate) struct GateArgs {
package: String,
#[arg(long)]
before_lock: PathBuf,
#[arg(long)]
before_cache: PathBuf,
#[arg(long)]
after_lock: PathBuf,
#[arg(long)]
after_cache: PathBuf,
#[arg(long, value_enum, default_value = "both")]
direction: GateDirectionArg,
#[arg(long, value_enum, default_value = "breaking")]
fail_on: GateFailOnArg,
#[arg(long)]
baseline: Option<PathBuf>,
#[arg(long)]
suppressions: Option<PathBuf>,
#[arg(long, value_enum, default_value = "json")]
format: GateOutputFormat,
#[arg(long)]
output: Option<PathBuf>,
}

#[derive(Clone, Copy, ValueEnum)]
enum GateOutputFormat {
Json,
}

#[derive(Clone, Copy, ValueEnum)]
enum GateDirectionArg {
Both,
Producer,
Consumer,
}

#[derive(Clone, Copy, ValueEnum)]
enum GateFailOnArg {
Breaking,
Risky,
None,
}

impl From<GateDirectionArg> for CheckDirection {
fn from(value: GateDirectionArg) -> Self {
match value {
GateDirectionArg::Both => Self::Both,
GateDirectionArg::Producer => Self::Producer,
GateDirectionArg::Consumer => Self::Consumer,
}
}
}

impl From<GateFailOnArg> for CheckFailOn {
fn from(value: GateFailOnArg) -> Self {
match value {
GateFailOnArg::Breaking => Self::Breaking,
GateFailOnArg::Risky => Self::Risky,
GateFailOnArg::None => Self::None,
}
}
}

pub(crate) fn run(args: GateArgs) -> Result<ExitCode, Box<dyn std::error::Error>> {
let diff = build_gate_diff_report(
args.package,
args.before_lock,
args.before_cache,
args.after_lock,
args.after_cache,
)?;
let compatibility = classify_structural_diff(&diff)?;
let current = evaluate_compatibility_policy(
&compatibility,
CheckPolicy {
direction: args.direction.into(),
fail_on: args.fail_on.into(),
},
)?;

let baseline = args
.baseline
.as_deref()
.map(|path| {
let bytes = read_bounded_file(path, MAX_GATE_BASELINE_INPUT_BYTES)?;
Comment thread
TheHalfMoon marked this conversation as resolved.
Ok::<_, Box<dyn std::error::Error>>(CheckReport::from_json_slice(&bytes)?)
})
.transpose()?;
let suppressions = args
.suppressions
.as_deref()
.map(|path| {
let bytes = read_bounded_file(path, MAX_GATE_SUPPRESSIONS_INPUT_BYTES)?;
Ok::<_, Box<dyn std::error::Error>>(GateSuppressions::from_json_slice(&bytes)?)
})
.transpose()?;

let report = evaluate_quality_gate(&current, baseline.as_ref(), suppressions.as_ref())?;
let bytes = match args.format {
GateOutputFormat::Json => report.to_json_bytes()?,
};
write_check_output(&bytes, args.output.as_deref())?;

if report.decision.passed {
Ok(ExitCode::SUCCESS)
} else {
Ok(ExitCode::from(2))
}
}

fn build_gate_diff_report(
package: String,
before_lock: PathBuf,
before_cache: PathBuf,
after_lock: PathBuf,
after_cache: PathBuf,
) -> Result<StructuralDiffReport, Box<dyn std::error::Error>> {
let package_name = PackageName::parse(package)?;
let before_lockfile = Lockfile::from_slice(&read_bounded_file(
&before_lock,
MAX_GATE_LOCKFILE_INPUT_BYTES,
)?)?;
let after_lockfile = Lockfile::from_slice(&read_bounded_file(
&after_lock,
MAX_GATE_LOCKFILE_INPUT_BYTES,
)?)?;
let before_locked = select_locked_package(&before_lockfile, package_name.as_str())?;
let after_locked = select_locked_package(&after_lockfile, package_name.as_str())?;

let before_cache = PackageCache::new(before_cache);
let after_cache = PackageCache::new(after_cache);
let before_bytes =
before_cache.read_verified_bounded(&before_locked.sha256, MAX_GATE_ARCHIVE_INPUT_BYTES)?;
let after_bytes =
after_cache.read_verified_bounded(&after_locked.sha256, MAX_GATE_ARCHIVE_INPUT_BYTES)?;

Ok(diff_package_archives(
package_name.to_string(),
&before_locked.version,
&before_locked.sha256,
&before_bytes,
&after_locked.version,
&after_locked.sha256,
&after_bytes,
)?)
}
11 changes: 9 additions & 2 deletions crates/commandf-cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod gate;
mod impact;
mod oracle;

Expand Down Expand Up @@ -115,6 +116,7 @@ enum Command {
#[arg(long)]
output: Option<PathBuf>,
},
Gate(gate::GateArgs),
Terminology {
package: String,
#[arg(long)]
Expand Down Expand Up @@ -237,7 +239,11 @@ enum PkgCommand {
}

fn main() -> ExitCode {
let is_check = std::env::args_os().nth(1).as_deref() == Some(OsStr::new("check"));
let command = std::env::args_os().nth(1);
let normalize_usage_exit = matches!(
command.as_deref(),
Some(value) if value == OsStr::new("check") || value == OsStr::new("gate")
);
let cli = match Cli::try_parse() {
Ok(cli) => cli,
Err(error) => {
Expand All @@ -246,7 +252,7 @@ fn main() -> ExitCode {
if clap_exit == 0 {
return ExitCode::SUCCESS;
}
if is_check {
if normalize_usage_exit {
return ExitCode::from(1);
}
return ExitCode::from(clap_exit as u8);
Expand Down Expand Up @@ -426,6 +432,7 @@ fn run(cli: Cli) -> Result<ExitCode, Box<dyn std::error::Error>> {
}
return Ok(ExitCode::from(2));
}
Command::Gate(args) => return gate::run(args),
Command::Terminology {
package,
before_lock,
Expand Down
Loading
Loading