Skip to content

stack 4/5: add deterministic anti-slop CI - #903

Closed
Wibias wants to merge 4 commits into
agent/pr-trust-lanefrom
agent/pr-hygiene-gate
Closed

stack 4/5: add deterministic anti-slop CI#903
Wibias wants to merge 4 commits into
agent/pr-trust-lanefrom
agent/pr-hygiene-gate

Conversation

@Wibias

@Wibias Wibias commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Stack

4/5 — deterministic anti-slop CI

Base: agent/pr-trust-lane (#902)
Next: review lifecycle, ownership, closure policy, and repository-settings rollout

Summary

  • require a test change when runtime or dashboard behavior changes
  • reject newly added type/lint/formatter suppressions
  • reject focused or skipped tests
  • reject empty catch blocks
  • reject committed generated build output
  • reject bun.lock churn without package.json
  • provide narrow maintainer-approved exception labels where judgment is legitimate

Verification

  • node --test .github/scripts/pr-hygiene.test.cjs — 9 passed, 0 failed
  • workflow YAML parsed successfully
  • trusted workflow reads patches through GitHub APIs and never executes PR-head code

Why

These checks target concrete, auditable defects instead of attempting unreliable AI-origin detection.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (2)
  • ^dev$
  • ^preview$

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 96c6645b-6dfc-4f05-8225-4dcd169ec47c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

@Wibias Wibias added the chore Maintenance, CI, tests, refactors, or build changes (not a user-facing bug or feature). label Aug 2, 2026

Wibias commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator Author

Stack navigation

  1. stack 1/5: add contribution intake firewall #900 — contribution intake firewall
  2. stack 2/5: gate review readiness and CodeRabbit #901 — readiness and CodeRabbit gate
  3. stack 3/5: add first-contributor trust lane #902 — first-contributor trust lane
  4. stack 4/5: add deterministic anti-slop CI #903 — deterministic anti-slop CI
  5. stack 5/5: define review lifecycle and governance rollout #905 — review lifecycle and governance rollout

Review and merge bottom-up. Each PR targets the preceding stack branch, so its Files changed view contains only that layer.

@Wibias
Wibias marked this pull request as ready for review August 3, 2026 02:16

Wibias commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator Author

[shipping-github] Verdict: approve-comment

PR: #903 — stack 4/5: add deterministic anti-slop CI
Head: 9f4b7e8b on agent/pr-trust-lane (mergeable, updated from the current stack-3/5 head fc6a8553)

Semantic propagation

  • Concepts audited: behavior-change test requirement, suppression/focused-test/empty-catch rejection, generated-output and lockfile-churn rejection, exception-label semantics (test-exception-approved, suppression-approved, generated-change-approved, dependency-change-approved).
  • Authoritative sources: docs/superpowers/specs/2026-08-02-pr-hygiene-design.md, .github/scripts/pr-hygiene.cjs, pr-hygiene.yml.
  • Producers and consumers checked: the hygiene workflow; stack 1/5: add contribution intake firewall #900/stack 3/5: add first-contributor trust lane #902 path classification (same previous_filename contract, now mirrored here); stack 2/5: gate review readiness and CodeRabbit #901 readiness (labels the blocked state via the failed check); stack 5/5: define review lifecycle and governance rollout #905 CI wiring and contributor docs; bunfig.toml/bun.lock/package.json semantics.
  • Public/derived representations checked: design doc rule list vs implementation; exception-label names vs workflow definitions and failure copy; intake: hygiene-blocked vs stack 2/5: gate review readiness and CodeRabbit #901 readiness handling; gui/tests/** and src/*.test.* recognized as test coverage.
  • Material variant partitions checked: behavior vs docs-only vs test-only changes; renames (both sides); generated paths; lockfile orphans; suppression/focused/empty-catch patterns incl. multiline; exception labels on/off.
  • Positive and negative assertions checked: 12 hygiene tests, 320/320 full script suite, end-to-end harness (block/pass/exception-label/rename).
  • Unmapped surfaces: none.
  • Unproven equivalence assumptions: none.
  • Representation mismatches: none.
  • Variant coverage gaps: none blocking.
  • Axis verdict: pass.

Usefulness

High: deterministic, auditable anti-slop rules with narrow maintainer-approved exceptions and no AI-origin guessing. The labeled/unlabeled triggers re-run the gate when an exception label is applied, so blocked PRs recover without a dispatch mechanism.

Bugs / correctness

  • Method: bug-review.md — Bugbot: n/a (Codex host, complementary only); complementary: done (silent_failures / resource_leaks / edge_cases).
  • F1 (P2, fixed in 9f4b7e8b): renamed files were classified by destination only, so src/router.ts moved under docs/ skipped missing_regression_test and gui/dist/index.js renamed away skipped generated_output (both reproduced by probe). Classification now uses filename + previous_filename with regression tests.
  • F3 (fixed): merged the current fc6a8553 parent (conflict-free), making the PR mergeable and validated against the current stack head.
  • Residual (no fix): GitHub truncates very large patches, so a violation beyond the truncation point could be missed (inherent API limit); explanations[failure.code] would render "undefined" for a future unknown code (all current codes covered).

Security

Spec / standards

  • Spec source: PR body + design doc; all six summary claims verified against the code, including the no-bypass rule for empty catches.
  • Gaps: none blocking. The PR body count is stale after this review (9 passed -> 12).

Reviews

  • Owners/maintainers: requested @Ingwannu and @lidge-jun; pending. Per MAINTAINERS.md this .github/** security-boundary change needs explicit maintainer security review before merge.
  • Bots: none yet - CodeRabbit skips branch bases; the Codex connector has not reviewed this head.

Base / CI

  • Base: updated to fc6a8553 (2cf0ce4a merge); mergeable: true, 0 behind.
  • Required checks: none configured for these paths (CI wiring is stack 5/5: define review lifecycle and governance rollout #905's stack job); React Doctor green on 9f4b7e8b; CodeRabbit success.
  • Local tip compile/tests: 320/320 script tests, workflow YAML parses, full workflow script syntax-checked, end-to-end harness verified block/pass/exception-label/rename paths.

Gate

None: draft cleared, PR is ready for review. ship-gate.mjs could not run on this machine (gh token invalid); connector evidence used instead.

Simplification (approved S1 - withdrawn)

  • The proposed S1 (compute added lines once per file) was verified as already implemented (const lines = addedLines(file.patch) is computed once in the single scan loop), so no code change was made. No other behavior-preserving candidates were worth touching.

Bottom line

Substantively clean and approve-worthy after the fix batch: the rename bypass is closed, permissions are job-scoped, and the branch is current with its parent. Merge still requires maintainer approval (requested) and the synthetic fork test. When #905 is reviewed it will need the same base update - it is still based on the old 0cbaf4ba head.

@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: 0cbaf4baab

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/pr-hygiene.yml Outdated
permissions:
contents: read
issues: write
pull-requests: write

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 Reduce pull-request permission to read

The workflow only calls pulls.get and pulls.listFiles; every mutation uses the issues API for labels and comments. Granting pull-requests: write therefore gives this privileged pull_request_target job unnecessary PR-mutation capability. Set this scope to read while retaining issues: write.

AGENTS.md reference: .github/AGENTS.md:L12-L17

Useful? React with 👍 / 👎.

Comment on lines +76 to +80
const lines = addedLines(file.patch);
if (lines.some((line) => SUPPRESSION_PATTERN.test(line))) {
suppressions.push(file.filename);
}
if (lines.some((line) => FOCUSED_TEST_PATTERN.test(line))) {

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 prohibited tokens inside fixtures and prose

These checks search every added line as raw text, so string literals, regex definitions, and documentation are treated as executable suppressions or focused tests. Feeding this commit's own patches to assessHygiene reports new_suppression, focused_or_skipped_test, and empty_catch because .github/scripts/pr-hygiene.test.cjs contains those constructs as fixture strings. Restrict checks to applicable source syntax or otherwise distinguish literal fixture/prose text.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[shipping-github] Declined with rationale: raw-text scanning is the deliberate design (documented in the design record), and the four exception labels are the escape hatch for legitimate fixtures/prose that must mention these tokens. A source parser would be disproportionate for a gate whose failure mode is a maintainer reviewing the flagged line.

Comment on lines +20 to +21
.filter((line) => line.startsWith("+") && !line.startsWith("+++"))
.map((line) => line.slice(1));

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 Evaluate catch blocks after deletions

Because addedLines discards every deletion and context line, deleting the final statement from an existing catch leaves an empty catch in the PR result but supplies no added braces for hasEmptyCatch, so the supposedly non-bypassable check passes. Conversely, changing only the catch signature and closing brace can make an unchanged nonempty body look empty. Inspect the resulting file or preserve enough hunk context to determine the actual catch body.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[shipping-github] Fixed in 7a6982d0: when a hunk deletes lines, the empty-catch scan now includes hunk context (added + context lines) instead of additions only, so deleting a catch body leaves the empty structure visible and is rejected. A nonempty body in a deletion hunk is not flagged. Unit-tested in both directions.

const FOCUSED_TEST_PATTERN = /\b(?:describe|it|test)\.(?:only|skip)\s*\(/;

function addedLines(patch) {
if (typeof patch !== "string") return [];

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 Fail closed when GitHub omits a patch

The pull-files API does not guarantee a patch string for every file, notably for binary or oversized/truncated diffs. Returning an empty line set in that case silently bypasses suppression, focused-test, and empty-catch checks for the affected file. Since this workflow treats untrusted PR metadata as an enforcement boundary, it should retrieve complete content or fail explicitly when an applicable text file has no inspectable patch.

AGENTS.md reference: .github/AGENTS.md:L15-L17

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[shipping-github] Declined as a residual: binary files have no executable suppressions to scan, and truncated patches are an inherent GitHub API limit already noted in the review. Text-file content fallback would add a content fetch per file for marginal coverage; the exception labels cover the remaining judgment cases.

Comment thread .github/scripts/pr-hygiene.cjs Outdated
failures.push({ code: "missing_regression_test" });
}

const generated = filenames.filter(isGeneratedPath);

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 removal of generated output

This classifies files solely by path, so a PR deleting dist/, gui/dist/, coverage/, or another forbidden generated directory is rejected as generated_output and told to remove output it is already removing. Filter out files whose API status is removed before reporting committed generated output.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[shipping-github] Fixed in 7a6982d0: files with status: "removed" are excluded from the generated-output check, so a PR deleting dist/ or gui/dist/ output is no longer told to remove output it is already removing. Unit-tested.

Comment thread .github/scripts/pr-hygiene.cjs Outdated
Comment on lines +44 to +45
const filenames = files.map((file) => file.filename);
const behaviorChanged = filenames.some(isBehaviorPath);

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 Include renamed source paths in behavior detection

For renamed files, GitHub reports the destination in filename and the source in previous_filename. A PR that moves a runtime file from src/ or gui/src/ elsewhere therefore removes behavior from those trees while behaviorChanged remains false, bypassing the regression-test requirement. Include previous_filename when classifying renamed files.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[shipping-github] Already fixed in 9f4b7e8b: behavior/test/generated classification now includes previous_filename, so moving a runtime file out of src/ or gui/src/ still counts as a behavior change and requires regression coverage. Regression tests cover renames into docs paths.

Comment on lines +49 to +51
behaviorChanged &&
!testsChanged &&
!labelSet.has("test-exception-approved")

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 Revoke stale exception labels after new commits

Once test-exception-approved is applied, later synchronize events retain it and this validator continues bypassing both the regression-test and skipped-test checks for the new head. A contributor can therefore obtain approval for one narrow exception, push additional unreviewed violations, and still receive a passing hygiene result. Bind approvals to the reviewed head SHA or clear exception labels whenever the PR head changes.

AGENTS.md reference: .github/AGENTS.md:L15-L17

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[shipping-github] Fixed in 7a6982d0: on synchronize events the workflow revokes all four exception labels before assessment, so approvals are head-specific and a contributor cannot push unreviewed violations under a stale exception. Harness-verified (synchronize revokes + blocks; labeled keeps + passes).

});
});

describe("assessHygiene", () => {

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 Run the hygiene tests in CI

This new test suite is not invoked by any GitHub workflow: .github/workflows/issue-quality-tests.yml neither includes the hygiene files in its path filters nor runs this test, and Cross-platform CI does not trigger for these new paths either. Consequently the privileged gate can regress while repository CI remains green. Add the hygiene script, test, and workflow paths to the automation-test workflow and execute node --test .github/scripts/pr-hygiene.test.cjs there.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[shipping-github] Declined here because it is already implemented later in this stack: #905 wires pr-hygiene.test.cjs into the policy-test workflow (path filters + node --test line). Keeping the CI wiring in #905 avoids duplicating it in #903.

Comment on lines +104 to +108
const explanations = {
missing_regression_test: "Behavior changed under `src/` or `gui/src/` without a test change. Add focused coverage or obtain `test-exception-approved`.",
generated_output: "Generated build output is committed. Remove it or obtain `generated-change-approved`.",
orphan_lockfile: "`bun.lock` changed without `package.json`. Revert accidental churn or obtain `dependency-change-approved`.",
new_suppression: "A new TypeScript, lint, formatter, or similar suppression was added. Fix the underlying issue or obtain `suppression-approved`.",

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 Document the new contributor gate publicly

This mandatory gate introduces contributor-visible rejection rules and four approval labels, but the commit updates only an internal design note; docs-site/src/content/docs/contributing.md still describes the existing CI and PR requirements without any of these rules or the exception process. Contributors will learn the contract only after a bot failure, so add the hygiene requirements to the public contributing documentation and keep localized guidance consistent.

AGENTS.md reference: AGENTS.md:L200-L201

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[shipping-github] Declined here because it is already implemented later in this stack: #905's CONTRIBUTING.md "Pull request contract" and docs-site contributing/pr-quality.md document the regression-test requirement and the gate flow. Keeping the docs change in #905 avoids duplicating it in #903.

Comment thread .github/scripts/pr-hygiene.cjs Outdated
Comment on lines +45 to +46
const behaviorChanged = filenames.some(isBehaviorPath);
const testsChanged = filenames.some(isTestPath);

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 Reject deleted tests as regression coverage

testsChanged only checks the filename, so a runtime PR that deletes an unrelated test—or renames one away—satisfies the regression-test gate despite adding no coverage at all. Use the file status and resulting test paths to exclude removals, at minimum, before treating a test-file change as evidence of focused regression coverage.

AGENTS.md reference: AGENTS.md:L197-L199

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[shipping-github] Fixed in 7a6982d0: test files with status: "removed" no longer count toward testsChanged, so a runtime PR that only deletes an unrelated test is still rejected as missing_regression_test. Unit-tested.

@lidge-jun

Copy link
Copy Markdown
Owner

This is the one in the stack I want, and I would like to take it first rather than last.

Reason: every check in pr-hygiene.cjs:41-108 is deterministic — test-change requirement, suppression comments, focused tests, empty catches, generated files, lockfile drift. A contributor can read the failure and know exactly what to change, which is the property I am unwilling to trade away in #901 (commented there separately). It is also the only one of the five that reduces review load without adding a subjective judgment or an approval dependency.

Two things before it lands:

  1. It is stacked behind stack 1/5: add contribution intake firewall #900stack 2/5: gate review readiness and CodeRabbit #901stack 3/5: add first-contributor trust lane #902, and stack 2/5: gate review readiness and CodeRabbit #901 is on hold. The hygiene workflow does not actually depend on any of them — .github/scripts/pr-hygiene.cjs and its workflow are self-contained. Could you extract it onto dev directly? If you would rather not re-cut the branch, say so and I will do the extraction with your authorship preserved.

  2. Two tuning items I hit reading it against this repo's own history:

    • The test-change requirement will fire on legitimate platform-skip commits (a Windows-only fix whose test is test.skipIf(process.platform !== "win32")) and on pure comment/doc corrections inside src/. Worth an escape hatch that is narrower than a blanket label.
    • The empty-catch rule collides with a real pattern here: catch { /* best-effort cleanup */ } appears throughout the teardown paths on purpose, and those comments are load-bearing documentation rather than laziness. Matching on "empty catch with no comment" instead of "empty catch" would keep the signal and drop the false positives.

Context for the ordering: I measured the lane before deciding anything. Windows was the last job to finish in 23 of 23 recent runs (median 17m41s vs ubuntu 5m58s), so #899 goes first as the actual bottleneck fix. This lands right behind it.

@lidge-jun

Copy link
Copy Markdown
Owner

Extracted this onto dev as #918 — your three commits unmodified, authorship intact, plus one tuning commit of mine on top.

Two of the three concerns I raised above were wrong, and I checked before touching anything rather than asking you to:

  • test.skipIf(...) / describe.skipIf(...) are not flagged. FOCUSED_TEST_PATTERN matches .only( and .skip( only, which is exactly right for this repo's platform-conditional tests.
  • catch { /* best-effort */ } is not flagged. hasEmptyCatch requires a genuinely empty body. Run across the whole tree it flags six files, all minified. My worry was unfounded.

The one real false positive: missing_regression_test fires on a comment-only change inside src/. This repo asks for dense explanatory comments in exactly those files, so sharpening one would fail the gate, and the only escape was test-exception-approved — which trains contributors to request the label instead of writing tests. My commit exempts a file whose patch is entirely comment or blank lines on both sides, and keeps the requirement the moment one non-comment line appears anywhere in that file's patch. Two tests added, driven red by removing the exemption. 19/19 green.

Your call on how to finish it: I can merge #918, or close it and you land the same thing from a branch you re-cut onto dev — say the word and I will close mine. What I did not want was for this gate to sit behind #900/#901/#902, since it shares no files with them and the questions holding those up are unrelated to this one.

Separately on the stack: #905's head is not a descendant of this branch (merge base 0cbaf4ba), so it needs a rebase regardless of what we decide about the rest.

lidge-jun added a commit that referenced this pull request Aug 3, 2026
ci: add the deterministic PR hygiene gate (extracted from #903)
@lidge-jun

Copy link
Copy Markdown
Owner

Landed as #918 — your three commits, authorship intact, merged to dev.

On top of them: a comment-only source change no longer owes a regression test (the label escape would have taught contributors to ask for the label instead of writing tests), the gate's own pr-hygiene.test.cjs is now wired into issue-quality-tests.yml because nothing was executing it, and two false positives CodeRabbit caught are fixed — cross-hunk empty-catch detection and a deleted bun.lock counting as an orphan.

The two concerns I raised earlier were wrong and I checked before changing anything: test.skipIf(...) is not flagged (the pattern matches .only(/.skip( only) and catch { /* comment */ } is not flagged (the pattern requires a genuinely empty body).

Thanks — this was the one in the stack that reduces friction rather than adding it, which is why it went first.

@lidge-jun lidge-jun closed this Aug 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

chore Maintenance, CI, tests, refactors, or build changes (not a user-facing bug or feature).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants