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)
body.warningsnames the guards that could not run for the registry inputs and says nothing about the diff inputs. Omitdiffand every content-mode checkpoint is silently inactive; omitname_statusand every checkpoint of both modes is silently inactive. Either way the envelope isstatus: "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 mountsghcr.io/barnett-studios/commitward:0.3.0(digestsha256:1f737085474e), stdin only. Fixtures hand-authored; expectations derived fromCONTRACT.md(paths= regex over changed paths,content= regex over added lines) before the first run.Registry supplied in every row:
The change is one added line,
rm -rf /var/lib/data, inscripts/deploy.sh.firedexit_classwarningsname_status+diff(control)["destructive-shell"]name_statusonly[]diffonly[][]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: 0with one warning, and that warning is about the base registry. Nothing was evaluated; the envelope reports nothing fired.A second registry with one
pathsand onecontentcheckpoint gives the same shape — withname_statusalone 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-CCONTRACT.mdshipped inside the crate, "The fail-open guarantee":README.md, thegatesection:That sentence names this exact failure and the implementation does not cover the case.
warningshas exactly two producers (main.rs,gate_envelope):no_registry_suppliedandbase_names.is_none(). There is no third for an emptyfilesor an emptyadded_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 omitsdiff: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.yamlcarriesdestructive-opsin content mode (line 47), so the default policy is inside the blast radius: a consumer that inlines the default registry and omitsdiffgets a gate withdestructive-opsswitched 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:Mode::Contentcheckpoint andadded_linesis empty → warn, naming those checkpointsMode::Pathcheckpoint andfilesis empty → warn, naming those checkpointsfiles.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_classstays 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 mountsBarnett-Studios/.github#3)