Skip to content

fix(gate): close build.map coverage gap, run release negative control - #196

Merged
cuioss-oliver merged 6 commits into
mainfrom
feature/plan-51-release-guard-empirical-control
Aug 8, 2026
Merged

fix(gate): close build.map coverage gap, run release negative control#196
cuioss-oliver merged 6 commits into
mainfrom
feature/plan-51-release-guard-empirical-control

Conversation

@cuioss-oliver

@cuioss-oliver cuioss-oliver commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

Closes a gate-coverage hole and runs the release guard's negative control, in that order.

CLAUDE.md enumerates the file classes whose change obliges a full quality gate. .plan/marshal.json's
build.map.java block — the file-to-build contract build-decision is the sole authority over — did not
cover five of them: .github/workflows/**, Dockerfile*, docker-compose*.yml, *.css, *.ts. A
footprint made only of those paths answered not_necessary and reached main ungated. The direction of
the fix is not up for re-litigation: the map was wrong, the declared rule is rightCLAUDE.md was
never weakened to match the map.

The five entries are then pinned by a test, because the eraser is a command, not a reader: they are
hand-added, no extension's classify_globs() derives them, and build-map seed --force or a
marshall-steward reconcile rewrites the block from the derivation and drops them silently. The contract
test converts that silence into a red build.

Finally, one non-release: key of .github/project.yml is changed, which is the merge that exercises the
release guard's refusal path.

Changes

  • .plan/marshal.json — five build.map.java entries added, each role: config / build_class: verify:
    .github/workflows/*, Dockerfile*, docker-compose*.yml, *.css, *.ts. The recursive ** spelling
    of the prose declaration is respelled .github/workflows/* because the route contract admits single-*
    fnmatch globs and bare basenames but never **. *.ts has no live file in the tree today and is added
    anyway — the map encodes the rule, not the current file census.
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/BuildGateCoverageContractTest.java — new
    contract test pinning those five globs with their role and build_class. Containment, not equality, so
    a legitimate future seed adding a Maven route does not fail it; only losing one of the five, or demoting a
    required entry's build_class away from verify, may. Carries two vacuity guards (required-set
    cardinality, parsed-array non-emptiness) and a matched positive/negative control over the extraction
    helper, so the primary assertion cannot pass by iterating over nothing or by accepting everything.
  • CLAUDE.md — one paragraph beside the Pre-Commit Process rule recording that the enumeration and the
    build.map contract now cover the same classes, and stating the cost.
  • doc/development/release-process.adoc — two subsections under the existing Trigger rules spine: the
    negative-control design with its prediction, and what that control proves and does not.
  • .github/project.ymlgithub-automation.auto-merge-build-timeout 300 → 900. A single non-release:
    key; the release: block is untouched. This edit is the negative control's trigger.

What this costs, stated plainly

Every workflow, Dockerfile, compose, CSS and TypeScript change now pays a full quality gate instead of
passing as documentation-only. That is the intended trade — the price of the rule CLAUDE.md already
declared — not a regression to be tuned away later.

Correction: the claimed "free self-test" is WITHDRAWN

The plan's request predicted a positive control obtained for free: sequence the map fix first, and the
.github/project.yml edit becomes the first footprint the fix gates. That claim is false and is withdrawn
here rather than quietly dropped.
.github/project.yml is under neither .github/workflows/** (the class
actually added to the map) nor a bare .github/** (never proposed as an entry). The fix therefore never
installs the class the self-test needed, and this PR's own footprint is gated by its *.java and *.adoc
content, not by the new entries. The new globs are exercised by the contract test, not by this diff.

The negative control is falsifiable, and NOT YET OBSERVED

The run this PR predicts does not exist until this PR merges. Everything below was written before the
merge that tests it; that ordering is the whole point, since an artifact written afterwards can only agree
with whatever happened.

Identification rule (no run id is committed, and no placeholder for one): the Release workflow run
attached to this pull request's merge commit
.

Predicted signature — a conjunction of four rows:

  1. release / guardsuccess
  2. release.outputs.released-versionempty
  3. release / releaseskipped
  4. Publish the tested image to GHCR (publish-image) — skipped

Row 4 alone is insufficient: the same rendering occurs when the release job fails. Without row 1 you
cannot distinguish refused by decision from died before deciding, and the crashed guard — the more
alarming case — wears exactly the same clothes.

Two further rows are inert in both directions. wait-for-maven-central and propagate-to-consumers are
if:-gated on a non-empty consumers output, and .github/project.yml declares no consumers key at all.
They skip on every run of this workflow and carry no information either way. Left unnamed, a reader counts
three skips and credits two of them.

Escalation condition. A rendering that does not match this prediction is a finding about the guard, not
a run id to file
. It goes in-tree in a follow-up PR, and the artifact's claim is withdrawn until it holds.

The observed run id and the rendered conclusions are posted as a comment on this PR after the merge.

The caveat is undiminished. This signature is also what a guard permanently stuck on "unchanged" would
render. The control proves the guard refused; it does not prove the guard can proceed, and it must
never be cited as though it did. The positive direction is cited rather than re-manufactured (the 0.1.1 cut,
run 31157727796) together with the pinned reusable workflow's checkout depth, which is what settles the
mechanism. The asymmetry is recorded rather than erased: stuck-unchanged fails SAFE, stuck-changed publishes
irrevocably — so the dangerous direction is the one worth an empirical control, and anyone later tempted to
"balance" the two must not do so by weakening this one.

Test Plan

  • Quality gate passed (verify -Ppre-commit)
  • Full verify passed (verify)
  • BuildGateCoverageContractTest green, including its matched positive/negative detector control

The gate was run explicitly for this footprint, regardless of the build-decision verdict. The
deliverable that installs the gate must not itself ship through the hole it closes — accepting a
not_necessary verdict here would have been the exact failure this PR exists to remove.

Intent

The problem. CLAUDE.md declares which file classes oblige a full quality gate, but .plan/marshal.json's build.map.java — the contract build-decision actually reads — covered five of them with nothing: workflow YAML, Dockerfile*, docker-compose*.yml, *.css, *.ts. A footprint made only of those answered not_necessary and reached main ungated. Separately, this epic had verified the release guard only in the direction where it permits a release; it had never observed it refuse.

The chosen approach. Fix the map, not the rule — the declaration is right and was never weakened to match. Then pin the five entries with a JUnit contract test, because they are hand-added, no extension derives them, and build-map seed --force or a steward reconcile rewrites the block from the derivation and drops them with no local symptom. A caveat in prose does not survive a command; a red build does. Finally, change one non-release: key of .github/project.yml, which is the merge that exercises the guard's refusal path — and commit the prediction before that merge, so the observation is falsifiable rather than confirmatory.

Explicit non-goals. No file under .github/workflows/ is edited, and the release: block is untouched. No release is cut and no second positive control is manufactured — the 0.1.1 cut is cited instead. No run id and no placeholder for

[Intent truncated — 1396 of 1702 characters shown; full outline in the plan workspace]


Generated by plan-finalize skill

Summary by CodeRabbit

  • Build & Automation

    • Increased the automatic merge build timeout to allow longer-running verification processes.
    • Expanded verification coverage for stylesheets, TypeScript, workflows, Dockerfiles, and Docker Compose files.
    • Clarified when release publication is skipped because the version is unchanged or its tag already exists.
  • Documentation

    • Clarified pre-commit classification rules and full verification requirements.
    • Added release-process guidance for validation controls, expected outcomes, evidence, and escalation.
  • Tests

    • Added contract coverage to detect missing or incorrect verification mappings.

cuioss-oliver and others added 3 commits August 7, 2026 22:57
build.map.java covered seven path classes while CLAUDE.md declared twelve, so a change touching only a workflow, Dockerfile, compose file, stylesheet or TypeScript source made build-decision answer not_necessary and shipped without the gate the rule demands.

Add the five missing globs (.github/workflows/*, Dockerfile*, docker-compose*.yml, *.css, *.ts) as role=config build_class=verify, verified against build-decision with a probe ladder including a negative control over the probe method itself.

Pin the five hand-added entries with BuildGateCoverageContractTest: they are derivable from no extension's classify_globs(), so build-map seed --force and a steward reconcile erase them silently. The guard was mutation-proved rather than trusted green.

State the agreement, and its cost, next to the rule in CLAUDE.md.
Record, before the merge that produces it, the negative control over the version-changed guard: the design, an identification rule rather than a run id, the predicted four-row signature, why publish-image SKIPPED alone is insufficient, the two inert rows, the release.yml:87-101 provenance, and the escalation condition.

Record what the control does NOT prove: the same signature is what a guard permanently stuck on unchanged would render. Cite the 0.1.1 cut (run 31157727796) and the pinned reusable workflow's checkout depth, and record the fail-safe / fail-dangerous asymmetry with the prohibition on rebalancing by weakening the negative control.

Raise github-automation.auto-merge-build-timeout from 300 to 900 — a non-release key, so merging it fires the release workflow and the guard refuses. The release: block is untouched; that is the control.

No run id and no placeholder for one is committed.
…ontract test

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @cuioss-oliver, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: cuioss/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ab7e9f5d-f1f0-47f7-ba65-ddc91361afd0

📥 Commits

Reviewing files that changed from the base of the PR and between 25d9c7e and fdf78a5.

📒 Files selected for processing (2)
  • .github/workflows/release.yml
  • doc/development/release-process.adoc
🚧 Files skipped from review as they are similar to previous changes (1)
  • doc/development/release-process.adoc

📝 Walkthrough

Walkthrough

The change expands verification path classification, adds contract coverage for the build map and detector, clarifies pre-commit rules, documents release negative controls, clarifies release workflow output behavior, and increases the auto-merge build timeout.

Changes

Build and release controls

Layer / File(s) Summary
Build gate classification and contract coverage
.plan/marshal.json, api-sheriff/src/test/.../BuildGateCoverageContractTest.java, CLAUDE.md
CSS, TypeScript, workflow, Dockerfile, and compose paths are classified for verification. The contract test validates required mappings and detector behavior. The pre-commit documentation reflects the build.map contract.
Release control procedure and timeout
.github/project.yml, doc/development/release-process.adoc, .github/workflows/release.yml
The auto-merge build timeout increases to 900 seconds. The release documentation defines normative trigger rules, a negative-control procedure, control limits, and failure modes. Workflow comments clarify the empty released-version output.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: closing the build.map coverage gap and running the release negative control.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cuioss-review-bot

cuioss-review-bot Bot commented Aug 8, 2026

Copy link
Copy Markdown

PR Reviewer Guide 🔍

(Review updated until commit fdf78a5)

🧪 PR contains tests
🔒 No security concerns identified
⚡ No major issues detected

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: cuioss/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5db4939e-e9d3-4841-aec7-2e015cf64460

📥 Commits

Reviewing files that changed from the base of the PR and between b3a2149 and 25d9c7e.

📒 Files selected for processing (5)
  • .github/project.yml
  • .plan/marshal.json
  • CLAUDE.md
  • api-sheriff/src/test/java/de/cuioss/sheriff/gateway/config/BuildGateCoverageContractTest.java
  • doc/development/release-process.adoc

Comment thread doc/development/release-process.adoc
Comment thread doc/development/release-process.adoc
Comment thread doc/development/release-process.adoc Outdated
cuioss-oliver and others added 3 commits August 8, 2026 12:54
…Exception

Both test methods declared `throws Exception` while only ever surfacing
`IOException`: javaBuildMap() declares exactly `throws IOException`, and
MAPPER.readTree(String) throws JsonProcessingException, a subclass of it.
`java.io.IOException` was already imported, so only the two signature lines
change.

Addresses PR #196 review finding 3f152d (CodeRabbit).

Co-Authored-By: Claude <noreply@anthropic.com>
…e paragraph

release-process.adoc stated the two-predicate refusal contract at
[#guard-refusal-predicates], then cited the release.yml comment as "the
mechanism's declared contract" — and that comment made exactly the
one-predicate claim the same document warns against. The provenance paragraph
endorsed as authoritative the very misreading named two sections earlier.

Both halves are corrected in one commit:

  - release-process.adoc: the provenance paragraph now states what an empty
    released-version actually proves — that no release was produced — and that
    emptiness is the union of both refusal predicates, so it cannot
    discriminate a version that did not move from a tag that already exists.
  - release.yml: the run/skip discriminator comment no longer asserts the
    version-unchanged predicate alone. The comment block is held at exactly
    the same line span so the doc's `lines 87--101` citation stays accurate.

The two-predicate statement is unchanged; this makes the provenance paragraph
consistent with it, not the reverse. No release.yml logic, expression,
condition, job or step is touched — the workflow edit is comment prose only.
The negative control's preamble, predicted signature and escalation condition
are untouched.

Addresses PR #196 review finding 0c6358 (CodeRabbit).

Co-Authored-By: Claude <noreply@anthropic.com>
@cuioss-oliver

Copy link
Copy Markdown
Collaborator Author

/review

@cuioss-oliver
cuioss-oliver added this pull request to the merge queue Aug 8, 2026
Merged via the queue into main with commit 963e422 Aug 8, 2026
26 checks passed
@cuioss-oliver
cuioss-oliver deleted the feature/plan-51-release-guard-empirical-control branch August 8, 2026 12:19
@cuioss-oliver

Copy link
Copy Markdown
Collaborator Author

The negative control: observed

This PR's doc/development/release-process.adoc committed a prediction about the Release run
that this merge would produce, together with an escalation condition. That run now exists. This
comment records what it rendered, against what was predicted.

The run, resolved by the identification rule the artifact committed ("the Release workflow run
attached to this PR's merge commit"): 31256991225
event=pull_request, head fdf78a5, merge commit 963e422.

Predicted vs observed

Job row Predicted Observed
release / guard success success
release / release skipped skipped
Publish the tested image to GHCR skipped skipped
release / wait-for-maven-central inert — carries no information either way skipped
release / propagate-to-consumers inert — carries no information either way skipped

Every predicted row matched. The escalation condition did not fire, so the artifact's claim
stands as written and no follow-up PR is owed.

The two inert rows are named deliberately. Both are gated on a non-empty consumers output, and
.github/project.yml declares no consumers key — so their skips are structural and mean nothing
about the guard. Without naming them a reader counts three skips and credits two of them with
evidence they do not carry.

End-state corroboration, labelled as such

No new tag was produced (0.1.1 remains latest), no Maven Central artifact, no GHCR push. This is
kept as corroboration and explicitly labelled: on its own it passes vacuously, being equally
true of every non-release merge in this repository's history. It is not the evidence; the four-row
conjunction above is.

What this does and does not establish

It establishes that the guard refused. It does not establish that the guard can proceed
a guard permanently stuck on "unchanged" (the fetch-depth failure mode) produces this identical
signature. That direction is evidenced separately by the 0.1.1 cut, run
31157727796, which saw
0.1.0 → 0.1.1 and proceeded; the mechanism itself is settled by reading the pinned reusable
workflow's checkout depth, not by this control. Both are cited in the artifact so the negative
control cannot be read as implying more than it shows.

One scope note

This PR edited .github/workflows/release.yml, which the originating plan spec asserted as an
absence. The edit is comment prose only — no logic, expression, condition, job or step changed —
and it was explicitly authorised after the trade was surfaced, to fix a real contradiction: the
release-process document warned against "a one-predicate reading of a two-predicate guard" and then
cited a workflow comment making exactly that one-predicate claim. Per the spec's own wording the
override is reported rather than silently absorbed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant