1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
//! Contract coverage for the continuous-integration side of the policy.
//!
//! `tests/env_access_policy.rs` proves the Makefile's `lint` target enforces
//! the environment-access policy, and `tests/env_access_enforcement.rs` proves
//! the lint fires. Neither says anything about whether CI runs any of it. A
//! workflow can keep a perfectly good command and never execute it: `if: false`
//! on the step, `if: false` on the job, a condition that is simply never true
//! on a pull request, or a removed `pull_request` trigger all leave the `run`
//! value untouched while the gate stops running. A green tick would then mean
//! nothing.
//!
//! So this file asserts the path from the trigger to the command: the
//! `pull_request` trigger exists, and the job and the steps that run the lint
//! target and the test suite are each reachable and blocking.
//!
//! Two keys make a step non-blocking and they are not interchangeable. `if`
//! decides whether it runs at all; `continue-on-error` lets it run, fail, and
//! report success anyway. A contract that rejected only the first would accept
//! a gate that runs, finds the policy broken, and goes green.
//!
//! Three details matter. The lint step's *whole* `run` value must be the
//! command, not merely contain it, because `if false; then make lint; fi`
//! contains it while running nothing. The command and the condition are checked
//! on the *same* step: looked up separately, an unconditional step named `Lint`
//! that runs something else would vouch for a second step carrying the real
//! command behind `if: false`. And conditions are judged by presence rather
//! than by value: enumerating the spellings of a false condition is the same
//! losing game as enumerating the spellings of a disabled command, and a
//! condition that looks true can still be false on a pull request. Any
//! condition on these steps is a decision that deserves a fresh look at this
//! contract.
//!
//! Mutation proof (2026-09-07). Each mutation was applied to
//! `.github/workflows/ci.yml` alone, the suite run, and the mutation reverted:
//!
//! ```text
//! if: false on the Lint step
//! -> the Lint step must carry no condition, found Some(Bool(false))
//! if: false on the build-test job
//! -> the build-test job must carry no condition, found Some(Bool(false))
//! if: false on the Test and Measure Coverage step
//! -> the Test and Measure Coverage step must carry no condition,
//! found Some(Bool(false))
//! if: ${{ github.event_name == 'push' }} on the Lint step
//! -> the Lint step must carry no condition, found Some(String(...))
//! wrap the run value in `if false; then make lint; fi`
//! -> no step runs `make lint` as its whole command
//! change the run value to `make typecheck`
//! -> no step runs `make lint` as its whole command
//! delete the pull_request trigger
//! -> the workflow must run on pull_request, found ["workflow_dispatch"]
//! keep an unconditional step named Lint running something else, and put
//! `make lint` in a second step behind `if: false`
//! -> every step that runs `make lint` as its whole command carries a
//! condition, found [Bool(false)]
//! do the same for the coverage action
//! -> every step that uses [the coverage action] carries a condition,
//! found [Bool(false)]
//! continue-on-error: true on the Lint step
//! -> every step that runs `make lint` as its whole command is non-blocking:
//! sets continue-on-error to Bool(true), so its failure is ignored
//! continue-on-error: true on the build-test job
//! -> the build-test job sets continue-on-error to Bool(true), so its
//! failure is ignored
//! ```
//!
//! Adding a second, skipped step running `make lint` alongside the real one was
//! also applied, and passes: the gate still runs, so there is nothing to fail.
//!
//! Two of these were re-run on 2026-09-14, after [`JobName`] and [`Subject`]
//! replaced the bare `&str` arguments the helpers took, to confirm the
//! rewording did not weaken what they judge. Wrapping the step's `run` value in
//! `if false; then make lint; fi` still fails with "no step runs `make lint` as
//! its whole command", and `if: false` on that step still fails with "every
//! step that runs `make lint` as its whole command is non-blocking: carries the
//! condition Bool(false)".
use fmt;
use ;
use ;
const WORKFLOW: &str = include_str!;
/// The job whose steps carry the policy into CI.
const GATE_JOB: = JobName;
/// A job's name, as the workflow spells it.
///
/// A newtype rather than a bare `&str` so a job name cannot be passed where a
/// failure-message subject is wanted, and the other way round: the two are the
/// same Rust type and read alike at a call site.
;
/// What a failure message calls the thing being judged.
///
/// Carried as its own type for the same reason: it is prose for a human, not a
/// key to look anything up by.
;
/// The command the lint step must run.
const LINT_COMMAND: &str = "make lint";
/// The action that runs the test suite, which includes the policy contracts.
///
/// Matched on the part before the `@`, so a pin bump does not fail the
/// contract while a different action does.
const COVERAGE_ACTION: &str = "leynos/shared-actions/.github/actions/generate-coverage";
/// Parse the workflow into its top-level mapping.
/// Return the workflow's trigger declaration.
///
/// YAML 1.1 reads a bare `on` key as the boolean `true`, so the key is looked
/// up both ways rather than relying on the file continuing to quote it.
/// Return the event names a trigger declaration names.
///
/// GitHub accepts three forms and a workflow runs on a pull request under all
/// of them: a scalar (`on: pull_request`), a sequence
/// (`on: [pull_request, workflow_dispatch]`), and a mapping whose keys are the
/// events. Requiring the mapping form failed the two other spellings, which
/// satisfy the rule this contract exists to enforce. A contract that rejects
/// what the rule permits is a contract that gets rewritten rather than obeyed,
/// and the reachability it was guarding goes with it.
///
/// A form that names nothing yields nothing, which fails the assertion rather
/// than passing it vacuously.
/// Return a named job's mapping.
/// Return a job's steps.
/// Fail if `entry` carries an `if` condition.
///
/// Presence is what is judged, not truth. A condition's value can be a template
/// that is false only on a pull request, and enumerating the ways to write one
/// is the same losing game as enumerating the ways to disable a command.
/// Scenario: the workflow's triggers are read.
/// Invariant: `pull_request` is among them, so the gate runs on the changes it
/// exists to judge. Without it every check is skipped and the pull request is
/// green because nothing looked.
/// Scenario: the job carrying the policy's gates is read.
/// Invariant: it exists and carries no condition, so it cannot be skipped
/// wholesale while its steps still look correct.
/// Return why `entry` would not block a merge on failure, if it would not.
///
/// Two keys do it, and they are not interchangeable. `if` decides whether the
/// step or job runs at all; `continue-on-error` lets it run, fail, and report
/// success anyway. A contract that rejected only the first would accept a gate
/// that runs, finds the policy broken, and goes green.
/// Return whether a workflow value is provably the boolean false.
///
/// `continue-on-error: false` is what GitHub does anyway, and it preserves
/// failure propagation, so treating every present value as non-blocking
/// rejected jobs and steps that block exactly as the policy requires.
///
/// Only the boolean and its unambiguous string spelling qualify. An expression
/// such as `${{ github.event_name == 'push' }}` is decided at run time and
/// cannot be judged here; a contract that guesses at one is worse than a
/// contract that refuses it, which is the same reason a condition is judged by
/// presence rather than by value. Measured on femtologging #455: reading the
/// key as a plain boolean let `${{ true }}` through.
/// Fail unless some step matching `selects` carries no condition.
///
/// The match and the condition check are deliberately the same step. Looking
/// the command up across every step and then the condition up on a step chosen
/// by name lets a workflow keep an unconditional step with the expected name
/// while a second step carries the real command behind `if: false`, at which
/// point both halves pass and the gate never runs.
///
/// One unconditional match is enough. A second, skipped step running the same
/// command changes nothing: the gate still runs.
/// Return whether `step`'s whole `run` value is `command`.
/// Return whether `step` uses `action`, whatever it is pinned to.
/// Scenario: the steps are searched for one that runs the lint target.
/// Invariant: at least one step both runs the command as its whole `run` value
/// and carries no condition. Binding the two to the same step is the point:
/// checked separately, an unconditional step named `Lint` that runs something
/// else would vouch for a second step carrying `make lint` behind `if: false`.
/// Matching a substring rather than the whole value would accept
/// `if false; then make lint; fi`, which runs nothing while reading correctly.
/// Scenario: the steps are searched for one that runs the test suite.
/// Invariant: at least one step both uses the coverage action and carries no
/// condition, bound to the same step for the same reason. The policy contracts
/// are tests, so a skipped test step is a skipped contract, and every other
/// assertion in this file would still pass.
// The fixture cases for the trigger forms and for `continue-on-error` live in
// their own file, per the repository's 400-line cap. The path is relative to
// this file's own directory, `tests/`.