Skip to content

feat(ci): reward-hacking integrity gate (delivery-loop hardening #1)#141

Merged
helebest merged 1 commit into
mainfrom
ci/reward-hacking-gate
Jun 29, 2026
Merged

feat(ci): reward-hacking integrity gate (delivery-loop hardening #1)#141
helebest merged 1 commit into
mainfrom
ci/reward-hacking-gate

Conversation

@helebest

@helebest helebest commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

What

Adds gate-integrity — a deterministic CI gate (and local npm run check:gate) that the in-loop coding agent cannot fool. It diffs the branch against its merge base and fails the PR if the verification itself was weakened:

  • coverage threshold lowered / coverage exclude list grown (vite.config.ts)
  • bundle budget raised (scripts/check-bundle.mjs)
  • e2e retries raised (playwright.config.ts)
  • a test deleted / disabled (.skip/.only/.todo/xit/xdescribe) / stripped of expect() assertions
  • any edit to the gate/CI machinery itself — the script, .github/workflows/**, or fixer.md's forbidden list (the self-guard, so the gate can't be quietly removed)

The good direction — raising a threshold, tightening a budget, adding tests — is always allowed. A deliberate weakening is allowed only when a maintainer attaches the visible gate-change label; the job then passes but prints what it allowed, for the audit trail.

Why

This is item #1 of a delivery-loop hardening effort (see docs/adr/0005-delivery-loop-hardening.md). Until now, "don't weaken the tests / don't lower thresholds" was prose only in CLAUDE.md, docs/review-rubric.md, and the fixer agent — the weakest possible defense, since the agent that edits a threshold also passes the check that reads it. This turns that rule into an exit code.

Why this PR carries the gate-change label

By design, introducing the gate machinery trips gate-machinery-modified (it adds the script + the CI job). That is the intended, auditable approval point — not a bug. The label is this PR's recorded authorization.

Test / verification

  • scripts/check-gate-integrity.test.mjs — 20 unit tests (TDD) covering every detection branch + the "good direction allowed" + override cases.
  • CLI smoke-tested against origin/main: fails with the 2 expected machinery violations; passes with GATE_HAS_OVERRIDE=true.
  • Local gates green: lint, format:check, typecheck, coverage (75.45/66.16/75.25/77.3%, well above 60/45/55/60). Build + full e2e left to CI — this PR touches only tooling/test-config/docs, no src/server/styles runtime code.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a new CI gate that blocks PRs from weakening test coverage, bundle limits, retry settings, or test assertions unless explicitly approved.
    • Added a new command to run the gate check locally.
  • Bug Fixes

    • Improved protection against accidental removal or bypassing of tests and verification checks.
    • Added checks to catch changes that reduce validation strength.
  • Documentation

    • Updated developer-facing docs and changelog to describe the new gate and approval process.

@helebest helebest added the gate-change Authorizes a deliberate, reviewed weakening of a verification gate label Jun 29, 2026
@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

Failed to post review comments.

GitHub was unavailable or timed out while CodeRabbit was posting the review. Please request a new review later if the pull request still needs one. This happened while posting 6 inline comments. Use @coderabbitai full review to retry the review.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: feb009ae-be76-4d59-acdf-54feae27f0ec

📥 Commits

Reviewing files that changed from the base of the PR and between 3091af2 and ec7b5da.

📒 Files selected for processing (12)
  • .claude/agents/fixer.md
  • .claude/skills/dikw-web-delivery-workflow/SKILL.md
  • .github/workflows/ci.yml
  • CHANGELOG.md
  • CLAUDE.md
  • docs/adr/0005-delivery-loop-hardening.md
  • docs/review-rubric.md
  • docs/tdd.md
  • package.json
  • scripts/check-gate-integrity.mjs
  • scripts/check-gate-integrity.test.mjs
  • vite.config.ts
⏰ Context from checks skipped due to timeout. (2)
  • GitHub Check: Verify (typecheck + coverage + build + e2e)
  • GitHub Check: Container scan (trivy)
🧰 Additional context used
📓 Path-based instructions (1)
vite.config.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Do not lower coverage thresholds or otherwise weaken the verification gates in Vite config.

Files:

  • vite.config.ts
🧠 Learnings (2)
📓 Common learnings
Learnt from: CR
Repo: OpenDIKW/dikw-web

Timestamp: 2026-06-29T14:23:45.668Z
Learning: Before non-trivial work, read the relevant product/contract docs in `docs/` (for example `core-contract.md`, `graph-view.md`, `ui-system.md`, `agent.md`, `tdd.md`, `observability.md`, `integration-verification.md`).
Learnt from: CR
Repo: OpenDIKW/dikw-web

Timestamp: 2026-06-29T14:23:45.668Z
Learning: When a root cause depends on data shape, verify it against the live API (`/v1/health`, `/v1/base/graph`, `/v1/base/pages/{path}/links`) before designing around it.
Learnt from: CR
Repo: OpenDIKW/dikw-web

Timestamp: 2026-06-29T14:23:45.668Z
Learning: State assumptions, surface tradeoffs, present multiple interpretations when needed, and ask clarifying questions instead of guessing.
Learnt from: CR
Repo: OpenDIKW/dikw-web

Timestamp: 2026-06-29T14:23:45.668Z
Learning: Prefer the simplest possible change: no speculative features, no unnecessary abstractions, and no unrequested flexibility or error handling.
Learnt from: CR
Repo: OpenDIKW/dikw-web

Timestamp: 2026-06-29T14:23:45.668Z
Learning: Make surgical changes only: do not refactor unrelated code, comments, or formatting; remove only imports/variables/functions orphaned by your own edits.
Learnt from: CR
Repo: OpenDIKW/dikw-web

Timestamp: 2026-06-29T14:23:45.668Z
Learning: Use TDD for behavior changes: failing test first, smallest change to green, then refactor; define success criteria and verify in a loop.
Learnt from: CR
Repo: OpenDIKW/dikw-web

Timestamp: 2026-06-29T14:23:45.668Z
Learning: Do not weaken tests, lower coverage thresholds, or otherwise reduce verification to make a feature pass.
📚 Learning: 2026-06-12T13:01:00.346Z
Learnt from: helebest
Repo: OpenDIKW/dikw-web PR: 83
File: .github/workflows/ci.yml:183-185
Timestamp: 2026-06-12T13:01:00.346Z
Learning: For GitHub Actions workflows in this repository, follow the established convention of referencing GitHub Actions by movable major-version tags (e.g., actions/checkoutv6, actions/setup-nodev6, actions/cachev5, actions/upload-artifactv7) rather than pinning individual steps to commit SHAs. During code review, only suggest SHA pinning for specific steps if the repo has adopted a repo-wide SHA-pinning policy; otherwise, do not flag a single-step SHA pin as inconsistent with the rest of the workflow.

Applied to files:

  • .github/workflows/ci.yml
🪛 LanguageTool
docs/adr/0005-delivery-loop-hardening.md

[uncategorized] ~52-~52: The official name of this software platform is spelled with a capital “H”.
Context: ...* — any edit to the gate script itself, .github/workflows/**, or fixer.md's forbidde...

(GITHUB)


[uncategorized] ~77-~77: The official name of this software platform is spelled with a capital “H”.
Context: ...a proof system. - The job is PR-scoped (if: github.event_name == 'pull_request') because ...

(GITHUB)

CLAUDE.md

[uncategorized] ~101-~101: The official name of this software platform is spelled with a capital “H”.
Context: ...t to the gate/CI machinery (the script, .github/workflows/**, fixer.md's forbidden l...

(GITHUB)

🪛 zizmor (1.26.1)
.github/workflows/ci.yml

[warning] 182-185: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

📝 Walkthrough

Walkthrough

Adds a new gate-integrity CI job and scripts/check-gate-integrity.mjs script that diffs a PR branch against its merge base to detect verification weakening (coverage thresholds, bundle budgets, e2e retries, test skips/deletions, assertion removal, gate machinery edits). A gate-change PR label allows maintainer-approved overrides. Documentation, ADR, CHANGELOG, and agent instructions are updated accordingly.

Changes

gate-integrity Reward-Hacking CI Gate

Layer / File(s) Summary
Parsers and evaluateGate core
scripts/check-gate-integrity.mjs
New script with exported parsers (parseCoverageThresholds, parseBundleBudgets, parsePlaywrightCiRetries, countSkipMarkers, countAssertions, isMachineryPath) and evaluateGate() that computes violations across all weakening categories and applies the gate-change label override.
CLI git plumbing and entry point
scripts/check-gate-integrity.mjs
main() runs git diff --name-status, reads file contents at merge-base and HEAD via git show, calls evaluateGate(), and exits 0 (pass/override) or 1 (failure with printed violation details).
Test suite and vite include glob
scripts/check-gate-integrity.test.mjs, vite.config.ts
Vitest fixture strings, parser unit tests, and comprehensive evaluateGate behavioral tests covering all violation codes and override state; vite.config.ts extended to include scripts/**/*.{test,spec}.mjs.
CI job, npm script, version bump
.github/workflows/ci.yml, package.json
New gate-integrity PR-only CI job computing GATE_BASE_REF and GATE_HAS_OVERRIDE then running the script; check:gate npm script added; version bumped to 0.8.7.
ADR, docs, and agent instructions
docs/adr/0005-delivery-loop-hardening.md, CHANGELOG.md, CLAUDE.md, docs/review-rubric.md, docs/tdd.md, .claude/agents/fixer.md, .claude/skills/dikw-web-delivery-workflow/SKILL.md
New ADR documenting the gate decision, failure conditions, and override workflow; CHANGELOG, CLAUDE.md, review rubric, TDD coverage policy, fixer agent constraints, and delivery skill updated to reference gate-integrity and gate-change.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 A gate has been built in the CI lane,
No sneaky threshold shall lower again!
With check:gate I diff every PR,
Reward-hacking agents won't get very far.
The gate-change label is the magic key,
Only a maintainer can set the tests free! 🔒

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.92% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 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 change: a new CI integrity gate for reward-hacking hardening.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ci/reward-hacking-gate

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.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9fbafa5b05

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread scripts/check-gate-integrity.mjs Outdated
// (a) coverage thresholds must not drop; the exclude list must not grow.
const baseCov = parseCoverageThresholds(input.coverage?.base);
const headCov = parseCoverageThresholds(input.coverage?.head);
if (baseCov && headCov) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reject missing coverage thresholds

If a PR removes the thresholds block, or removes one of its keys, parseCoverageThresholds() returns null for the head file and this if (baseCov && headCov) branch is skipped, so no violation is emitted. That disables Vitest coverage threshold enforcement without needing the gate-change label, which is the exact weakening this gate is meant to catch; a parseable base with a missing/unparseable head should be treated as a violation.

Useful? React with 👍 / 👎.

Comment thread scripts/check-gate-integrity.mjs Outdated
/** Number of entries in the coverage `exclude: [...]` array. */
export function parseCoverageExcludeCount(src) {
if (!src) return null;
const m = src.match(/exclude:\s*\[([\s\S]*?)\]/);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Count coverage excludes instead of test excludes

The current vite.config.ts declares test.exclude before coverage.exclude, so this first-match regex reads the top-level test exclusion list and returns 3 instead of the coverage exclusion count. A PR can add entries to coverage.exclude and this gate will still compare the unchanged test.exclude count, allowing files to be dropped from coverage without a gate-change label.

Useful? React with 👍 / 👎.

Comment thread scripts/check-gate-integrity.mjs Outdated
const TEST_FILE_RE = /\.(test|spec)\.(ts|tsx|mjs|js)$/;
const MACHINERY_PATHS = new Set([
"scripts/check-gate-integrity.mjs",
"scripts/check-bundle.mjs",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Allow legitimate bundle-budget tightening

Because scripts/check-bundle.mjs is listed as machinery, the CLI adds gate-machinery-modified for every budget edit before the budget comparison runs. That means lowering a budget, which the ADR/changelog describe as the always-allowed “good direction,” still fails unless a maintainer adds gate-change; the unit test for lowering a budget misses this because it calls evaluateGate() with machineryTouched: [].

Useful? React with 👍 / 👎.

Comment thread scripts/check-gate-integrity.mjs Outdated
/** Count of `expect(` assertion calls. */
export function countAssertions(src) {
if (!src) return 0;
return (src.match(/\bexpect\s*\(/g) || []).length;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Ignore commented-out assertions

countAssertions() counts raw expect( text, so changing an existing assertion to a comment such as // expect(value).toBe(...) removes the actual assertion while keeping the count unchanged. In that scenario the later comparison does not emit test-assertions-removed, letting a stripped test bypass the new gate; strip comments or parse tests before counting assertions.

Useful? React with 👍 / 👎.

@helebest helebest force-pushed the ci/reward-hacking-gate branch from 9fbafa5 to 98beb8a Compare June 29, 2026 14:14
Turn the prose "don't weaken the tests / don't lower thresholds" rule into a
deterministic check the in-loop agent cannot fool. scripts/check-gate-integrity.mjs
diffs the branch against its merge base and fails if the verification itself was
weakened — lowered coverage thresholds, raised bundle budgets, raised e2e retries,
deleted/disabled tests or stripped assertions, or any edit to the gate/CI machinery
(the script, .github/workflows/**, fixer.md's forbidden list). A maintainer allows a
deliberate change with a visible `gate-change` PR label. Added as a PR-scoped required
CI job; mirrored locally via `npm run check:gate`.
@helebest helebest force-pushed the ci/reward-hacking-gate branch from 98beb8a to ec7b5da Compare June 29, 2026 14:23
@helebest

Copy link
Copy Markdown
Contributor Author

Thanks — all four findings from the Codex review were valid and are now fixed in ec7b5da (TDD: 6 new failing tests added first, then the fixes; suite now 27 gate tests green):

  • P1 — missing thresholds bypass. evaluateGate now treats base parses but head doesn't as a violation for both coverage thresholds and bundle budgets — so deleting the thresholds block (or any key) trips coverage-threshold-lowered without the label. Tests: "flags removing the whole coverage thresholds block", "flags removing a single coverage threshold key", "flags removing the bundle budget block".
  • P1 — wrong exclude parsed. Added extractBlock() (brace-counting) so parseCoverageThresholds/parseCoverageExcludeCount are scoped to the coverage: block and no longer match a preceding test.exclude. Verified against the real vite.config.ts: exclude count now reads 5, not 3. Test: "parses the coverage block's exclude, not a preceding test.exclude".
  • P2 — budget tightening blocked. Removed scripts/check-bundle.mjs from the machinery set — it's now guarded directionally (a raised budget is flagged, a lowered one allowed), consistent with vite.config.ts. Test: "classifies gate machinery paths" asserts isMachineryPath("scripts/check-bundle.mjs") === false.
  • P2 — commented-out assertions. countAssertions now strips line/block comments before counting expect(. Test: "ignores commented-out assertions".

(This is the delivery loop working as intended — a different-model review caught real holes the author's own tests missed.)

@helebest helebest merged commit 3b886a7 into main Jun 29, 2026
10 checks passed
@helebest helebest deleted the ci/reward-hacking-gate branch June 29, 2026 14:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gate-change Authorizes a deliberate, reviewed weakening of a verification gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant