Skip to content

gate envelope reports a clean pass for a change it never saw: warnings covers the registry inputs, not the diff inputs #20

Description

@lyubomir-bozhinov

body.warnings names the guards that could not run for the registry inputs and says nothing about the diff inputs. Omit diff and every content-mode checkpoint is silently inactive; omit name_status and every checkpoint of both modes is silently inactive. Either way the envelope is status: "ok", fired: [], exit_class: 0 — a clean pass for a change the gate never saw.

This is the same class as #4 (base registry) and #7/NF3 (parse error), on the one input dimension those fixes did not cover.

Measured — published commitward 0.3.0, image, --network none, no mounts

ghcr.io/barnett-studios/commitward:0.3.0 (digest sha256:1f737085474e), stdin only. Fixtures hand-authored; expectations derived from CONTRACT.md (paths = regex over changed paths, content = regex over added lines) before the first run.

Registry supplied in every row:

version: "1"
checkpoints:
  - name: destructive-shell
    summary: destructive shell command added
    content:
      - "rm -rf"

The change is one added line, rm -rf /var/lib/data, in scripts/deploy.sh.

request fields supplied fired exit_class warnings
name_status + diff (control) ["destructive-shell"] 2 1 — base registry
name_status only [] 0 1 — base registry
diff only [] 0 1 — base registry
neither [] 0 1 — base registry

The control is the mutation check: with both inputs the guard fires, so nothing is wrong with the registry or the pattern. The only thing that changed in rows 2–4 is which inputs the caller handed over, and the response does not distinguish those rows from row 1's clean sibling.

The last row is the sharpest: a request carrying a registry, a commit message and no change at all returns exit_class: 0 with one warning, and that warning is about the base registry. Nothing was evaluated; the envelope reports nothing fired.

A second registry with one paths and one content checkpoint gives the same shape — with name_status alone the path guard fires and the content guard does not, still unwarned, so the response looks like a partial evaluation that succeeded.

Why this is class-C

CONTRACT.md shipped inside the crate, "The fail-open guarantee":

Fail-open is not fail-silent. A gate that cannot evaluate must say so; a check that did not run must never be reported as a check that passed.

  • Every ok envelope carries body.warnings, naming the guards that could not run.

README.md, the gate section:

body.warnings is a (possibly empty) list of checks that could not be performed. exit_class: 0 means "nothing fired", which is indistinguishable from "nothing was checked" unless the response says so.

That sentence names this exact failure and the implementation does not cover the case. warnings has exactly two producers (main.rs, gate_envelope): no_registry_supplied and base_names.is_none(). There is no third for an empty files or an empty added_lines.

Same on main (9212058) — the warnings block is byte-identical there, so this is not one of the four unreleased fixes waiting on a release.

Why a consumer hits it

Every request field is #[serde(default)], all are documented optional, and the README's own example request omits diff:

$ echo '{"name_status":"M\tCLAUDE.md","commit_msg":"docs: x", "global_registry_yaml":"..."}' \
    | docker run --rm -i --network none ghcr.io/barnett-studios/commitward gate

That example is correct for the path checkpoint it uses. Copied as the shape of a gate request — which is what a documented example is for — it disables every content-mode checkpoint in whatever registry the consumer inlines, with no signal. The shipped checkpoints.yaml carries destructive-ops in content mode (line 47), so the default policy is inside the blast radius: a consumer that inlines the default registry and omits diff gets a gate with destructive-ops switched off and nothing saying so.

Suggested direction

The absent/empty distinction is not recoverable from the request (String + serde(default)), and it does not need to be. The actionable condition is observable after compile:

  • compiled set contains a Mode::Content checkpoint and added_lines is empty → warn, naming those checkpoints
  • compiled set contains a Mode::Path checkpoint and files is empty → warn, naming those checkpoints

files.is_empty() && added_lines.is_empty() is the vacuous-request case and deserves its own line, since "nothing to evaluate" is a different statement from "guard X could not run". Behaviour need not change — exit_class stays what it is, the gate stays fail-open. Only the reporting.

Not measured

Whether any real consumer omits a field in practice. I drove the published artifact from outside; what the callers do is not visible from here, and the issue does not claim it.

Environment

  • ghcr.io/barnett-studios/commitward:0.3.0, docker run --rm -i --network none, no mounts
  • request built by hand; no git, no network
  • Filed by the family QA rotation (Barnett-Studios/.github#3)

Metadata

Metadata

Assignees

No one assigned

    Labels

    class-EEnablement: blocks shipping safely, incl. the machinery that proves we canp0Preempts everything

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions