Skip to content

🤖 feat: DRY gate (make dry-check, Quality / DRY) on unclebob/dryer - #7147

Merged
ThomasK33 merged 12 commits into
mainfrom
gauntlet/dry-gate
Oct 22, 2026
Merged

ThomasK33 merged 12 commits into
mainfrom
gauntlet/dry-gate

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds a DRY gate next to the CRAP gate. make dry-check (and a new warn-only Quality / DRY CI job) reports near-duplicate functions that a change creates. It compares each changed function with every production function in src/, using structural fingerprints from Uncle Bob's dryer. A pair that already matched on main stays quiet.

Stack 1 of 3. #7148 adds the mutation gate, and #7149 adds accept markers and an opt-in block mode.

Background

Uncle Bob's "gauntlet" runs three tools: crapper (complexity × coverage), dryer (duplication) and mutator (mutation testing). Xum already runs Crapper (scripts/crap/). This PR adds dryer, and #7148 adds mutator.

Implementation

  • scripts/dry/dryer_report.py (uv script, pins dryer 66ff6d2, plus the Crapper and grammar pins that CRAP uses). Upstream dryer compares every function with every other one, and that run did not finish in 15 minutes on src/ (about 14,000 functions). This script compares only the changed functions with all the others. Extraction takes about 19 s of CPU, plus about 0.13 s per changed function.
  • New-pair rule: a pair counts only when the diff made it. The script finds each changed function's base version through git's -U0 hunks, not by name. A function made only of added lines has no base version. Matching by name hid a new copy that shared a name with an old match, for example two render methods. Regression test: a new copy that shares a name with an old match is a finding.
  • scripts/crap/changes.ts: the diff code moves out of crap/check.ts so both gates agree on what "changed" means. CRAP behavior does not change: its 61 script tests pass unchanged.
  • Makefile: dry-check, test-dry-scripts (in static-check-full), and a shared tree-sitter-grammars download step with retries.
  • .github/workflows/pr.yml: Quality / DRY. It needs only changes, so it starts at once. Every step is soft. Required does not list it.
  • .xum/skills/gauntlet/SKILL.md and one AGENTS.md line: how to read and repair findings.

Validation

On the real repo:

  • I copied centsBucket as feeBucket in workspaceGoalService.ts. make dry-check reported 2 warnings at score 1.00, with exit 1.
  • I edited only centsBucket. The check passed, because that pair is old.

I confirmed that the new-pair test and the same-name test fail on the old rule and pass on the new one. A 200-function sample at the 0.82 threshold found that 7.5% of functions have a near-twin.

Risks

Low. The gate only warns and lives in scripts/. The CRAP refactor is the one shared path, and its tests cover it.


Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high • Cost: $12.44

Reports near-duplicate functions that a change makes: each changed
function against every production function, with dryer fingerprints and
Crapper names. Warn-only. Moves the diff code of crap/check.ts into
scripts/crap/changes.ts so both gates agree on what changed.
A new copy that shares a name with an old match (two render methods)
was read as the old function and hidden.
@mintlify

mintlify Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
Mux 🟢 Ready View Preview Oct 22, 2026, 11:24 AM

💡 Tip: Enable Automations to automatically generate PRs for you.

@ThomasK33
ThomasK33 added this pull request to stack #7150 October 22, 2026 06:40
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-22T16:08:52.394087Z 22475b6 New commits
🔒 Security Review ✅ Completed 2026-10-22T16:05:43.259094Z 22475b6 New commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@ThomasK33 ThomasK33 changed the title gauntlet/dry gate 🤖 feat: DRY gate (make dry-check, Quality / DRY) on unclebob/dryer Oct 22, 2026
@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

🟢 e2e stories (Haiku): 8 passed

21 agent steps · 7 replayed from cache · 24 model calls · 196.1k tokens (74% cached)
Spend: $0.01 of the $1.00 cap (proxy ledger)

All 8 tests in 8 files
Test Agent Time
🟢 chatRestart.e2e.ts · 1 passed 2 steps · 4 calls 24.7s
🟢 a chat survives a restart of the server 2 steps · 4 calls 24.7s
🟢 compaction.e2e.ts · 1 passed 3 steps · 7 calls 47.0s
🟢 /compact replaces the old chat with a summary 3 steps · 7 calls 47.0s
🟢 pluginTrustRestart.e2e.ts · 1 passed 4 steps · 2 calls 11.4s
🟢 a trusted plugin hook stays trusted after the server restarts 4 steps · 2 calls 11.4s
🟢 settingsRoundTrip.e2e.ts · 1 passed 3 steps · 4 calls 31.7s
🟢 a chat survives trips through Settings 3 steps · 4 calls 31.7s
🟢 sidebarPerWorkspace.e2e.ts · 1 passed 4 steps · 1 call 4.5s
🟢 a new workspace starts with its right sidebar collapsed while another keeps it open 4 steps · 1 call 4.5s
🟢 smoke.e2e.ts · 1 passed 2 steps · 1 call 2.8s
🟢 the agent opens the playground workspace 2 steps · 1 call 2.8s
🟢 streamErrors.e2e.ts · 1 passed 1 step · 1 call 8.1s
🟢 stream errors show in the chat, and the chat recovers 1 step · 1 call 8.1s
🟢 todoNudge.e2e.ts · 1 passed 2 steps · 4 calls 24.4s
🟢 the agent finishes open TODO items after the turn-end nudge 2 steps · 4 calls 24.4s

e2e 0.20.0 · 2m 39s · web · run artifacts

🟢 e2e Electron stories (Haiku): 3 passed

3 agent steps · 3 model calls · 8.2k tokens (32% cached)
Spend: $0.00 of the $1.00 cap (proxy ledger)

All 3 tests in 3 files
Test Agent Time
🟢 electron/deepLink.e2e.ts · 1 passed 1 step · 1 call 7.4s
🟢 a deep link of a second launch starts a chat in the running app 1 step · 1 call 7.4s
🟢 electron/menuBar.e2e.ts · 1 passed 1 step · 1 call 8.6s
🟢 the workspace menu bar is the desktop title bar 1 step · 1 call 8.6s
🟢 electron/smoke.e2e.ts · 1 passed 1 step · 1 call 5.7s
🟢 Xum Electron opens the playground workspace 1 step · 1 call 5.7s

e2e 0.20.0 · 24.4s · electron · run artifacts

🟢 e2e agent repros (Haiku): no tests selected

Spend: $0.00 of the $1.00 cap (proxy ledger)

e2e 0.20.0 · 7.4s · 2 targets (web, phone) · run artifacts

@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: f5c0e9e3cc

ℹ️ 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 scripts/dry/dryer_report.py
Comment thread scripts/dry/dryer_report.py
Comment thread scripts/dry/dryer_report.py Outdated
Comment thread scripts/dry/dryer_report.py Outdated
Review round 1 on #7147: a function moved into another file (or out of a
deleted one) now stands for the removed base form it matches, so its old
pairs stay quiet. Two head forms that map to one base form are a new
pair. Naming parses each reported file once.

@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: ee0389e04a

ℹ️ 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 Makefile
Comment thread scripts/dry/dryer_report.py
Review round 2 on #7147: a pure rename from a test path to a production
path has no hunks, so the gate touched none of its functions and missed
their duplicates. The whole head file now counts as changed.
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review · Automatically triggered

Security review completed. No security issues were found in this pull request.

Reviewed commit: 1f0ffc3b8f

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@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: 1f0ffc3b8f

ℹ️ 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 scripts/dry/dryer_report.py Outdated
Comment thread scripts/crap/changes.ts Outdated
… skips hunk bodies

Review round 4 on #7147:
- Two old duplicates moved together (no rename) can each stand for a
  different removed base form, so their pair stays quiet.
- diffHeaders() skips each hunk's body lines by count, so an added line
  `++ counter;` (rendered `+++ counter;`) is not a file header. Both
  parseChangedLines (also used by the CRAP gate) and parseHunks use it.

@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: 55c0ad187d

ℹ️ 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 scripts/dry/dryer_report.py Outdated
Review round 5 on #7147: two removed duplicates and three moved copies
hid the third copy, because each pair picked its own bases. Now, in head
order, each moved form claims its best unclaimed removed form, so the
copy left without one is new.

@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: 62700a57a2

ℹ️ 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 scripts/dry/dryer_report.py Outdated
Comment thread scripts/crap/changes.ts
…k-context=0

Review round 6 on #7147: with diff.interHunkContext set, git -U0 merges
nearby hunks with context lines. diffHeaders counted them twice and could
swallow the next file's headers. It now counts body lines per side, and
changesSince pins --inter-hunk-context=0.
@ThomasK33

Copy link
Copy Markdown
Member Author

Review loop paused at the round cap (6 of 6 Codex rounds on this PR).

Round 6 left two P2 threads:

  1. diff.interHunkContext (scripts/crap/changes.ts): confirmed by execution. With the setting at 20, git diff -U0 merged two hunks with three context lines, and the round-4 diffHeaders() swallowed the next file's headers. I fixed it locally in 8692e50 (per-side body counting plus --inter-hunk-context=0), with a test. It is not pushed, because a push starts round 7.
  2. Moved-form assignment (scripts/dry/dryer_report.py): the greedy one-to-one match can leave a moved form without a base when a complete matching exists. Rounds 4, 5 and 6 each found a new case in this move-detection logic, so the loop is not converging there.

The maintainer decides how to continue: a waiver for one more round (maximum-cardinality matching plus the fix above), a smaller scope for move detection, or a tracked follow-up.


Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high

Review rounds 4 to 6 on #7147 each found a new case in matching moved
forms to removed ones (ties, multiplicity, maximum matching). The user
chose to reduce scope: a form made only of added lines is new, so a
function moved into another file reports its old pairs again. The gate
warns by default, and the gauntlet skill states the limit.
@ThomasK33

Copy link
Copy Markdown
Member Author

The maintainer chose to reduce scope (Option B) and approved one more review round past the cap for this change. The next push removes cross-file move detection from the DRY gate and carries the diff-parser fix 8692e50. A finding in that round stops the loop.

@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: 28ba71f4ec

ℹ️ 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 scripts/dry/dryer_report.py Outdated
@ThomasK33

Copy link
Copy Markdown
Member Author

Review loop stopped. The waived round on 28ba71f found one new P2: a function whose every line is replaced (a names-only rewrite) loses its same-file counterpart, so its old pairs show as new DRY warnings. That is a false positive in a gate that only warns by default. I did not fix it, because the waiver allowed one round and a finding in it ends the loop. The thread stays open for the maintainer's decision.


Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high

Review round 7 on #7147: when a hunk replaced every line of a function
(names and literals only, same shape), no head line mapped to the base, so
its old pairs showed as new. Lines in a replacement hunk now map by
position when nothing else maps. Pure additions still have no base.
@ThomasK33

Copy link
Copy Markdown
Member Author

The maintainer approved one more review round past the cap (Option A). The next push fixes the open #7147 finding (a function rewritten line for line keeps its DRY base form) and merges it up the stack. A finding in this round stops the loop.

# Conflicts:
#	docs/AGENTS.md
#	src/node/services/agentSkills/builtInSkillContent.generated.ts

@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: 22475b6a73

ℹ️ 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 scripts/dry/dryer_report.py
Comment thread scripts/dry/dryer_report.py
@ThomasK33
ThomasK33 added this pull request to the merge queue Oct 22, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 22, 2026
@ThomasK33
ThomasK33 added this pull request to the merge queue Oct 22, 2026
Merged via the queue into main with commit 07a7a92 Oct 22, 2026
72 of 75 checks passed
@ThomasK33
ThomasK33 deleted the gauntlet/dry-gate branch October 22, 2026 17:05
asm pushed a commit to asm/mux that referenced this pull request Oct 22, 2026
…ebob/mutator (coder#7148)

## Summary

Adds a mutation gate. `make mutate-check` (and a warn-only `Quality /
Mutation` CI job) makes small changes ("mutants") in the functions a PR
changed, such as `===` to `!==`, `>` to `>=` or `??` to `||`. It then
lists the mutants that no unit test catches. The mutation sites come
from Uncle Bob's [mutator](https://github.com/unclebob/mutator). Xum
runs the mutants itself.

Stack 2 of 3, on coder#7147.

## Background

A mutant that survives means no test checks that behavior. The first run
found a real gap: `parseGoalTurnCapInput` had no direct test, and all 5
of its mutants survived. coder#7151 adds that test.

## Implementation

- **Why not the upstream runner:** the `mutator` command fails on this
repo with ENAMETOOLONG, because its import scan reads a string in a
source file as a path. It also follows only relative imports, so with
Xum's `@/` imports a mutant can run against the original file.
`scripts/mutate/mutate_report.py` uses mutator's
`discover_raw`/`apply_site` with UTF-8 byte offsets and runs them in its
own workers.
- **Workers:** each worker owns a private copy of the tracked files,
with `node_modules` linked and `src/version.ts` generated. The real
checkout is never touched.
- **Tests per mutant:** the colocated test runs first, then the nearest
other unit-lane tests that import the file, at most 10. Each test file
runs in its own `bun test` process. Several Xum test files in one
process break each other on shared temp config. A failing file or a
timeout (10× the file's baseline, at least 2 s) kills the mutant. The
runner kills the whole process group.
- **Baselines:** each test file must pass alone first. A file that fails
is dropped and listed, and the run exits 3.
- **Crash handling:** a crash in one mutant becomes one `error` row. If
the crash left the worker's copy mutated, the runner retires that
worker.
- **CI:** the job waits for Test / Unit and reuses its LCOV, so sites on
lines no unit test ran are skipped. The job stops at 300 mutants or 15
minutes. The deadline also cuts a running test, and such a mutant
reports `not-run`. `Required` does not list the job.

## Validation

On the real repo:
- I touched `parseGoalTurnCapInput`. Exactly its 5 mutants survived,
with exit 1.
- With a test added, all 5 were killed, with exit 0.
- With a broken test, the report said `BASELINE FAILED`, with exit 3.
- I touched `agentMessageEnvelope.ts`, which 10 unit tests import. Its 4
mutants were killed in 54 s at a host load average of about 200.

The end-to-end tests in throwaway repos run real `bun` processes,
including an `@/` import kill. I confirmed that the crash tests and the
survivor test fail when the code under test is broken.

## Risks

Low for merges, because the job only warns and `Required` does not wait
for it. The job adds runner minutes on every PR that changes `src/` or
CI config.

---

_Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking:
`high` • Cost: `$12.44`_

<!-- mux-attribution: model=anthropic:claude-opus-5-5 thinking=high
costs=12.44 -->
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