Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/MAINTAIN.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ All scopes live in `src/schedule/recipes.ts` as `*_SELECTION` constants.
| `lint-fix` (v5) | Apply the linter's **autofixable** rules only | No behavior-changing fixers; bounded to a focus area, never `--fix` the whole repo |
| `dep-bump` (v6) | Bump ONE dependency a patch/minor and prove tests pass | Requires a real test suite; never a major version |
| `consolidate-dupes` (v7) | Merge ONE exact-duplicate helper pair, repointing callers | Bodies must be *exactly* equivalent (near-dupes out); every caller resolved via the code graph or it blocks — a missed caller breaks the build |
| `extract-helper` (v8) | Collapse ONE near-duplicate cluster into a parameterized shared helper; originals become thin delegating wrappers | Only clusters with a *mechanical* parameterize proposal (`find_similar_helpers`); **zero call-site edits** — signatures/exports unchanged; a wrapper that can't be pure delegation blocks |

**Beyond exact duplicates:** the `find_similar_helpers` tool (`src/codegraph/helper-extract.ts`)
detects *near*-duplicate helpers — copy-pasted-then-tweaked functions (same structure, a different
Expand Down
2 changes: 2 additions & 0 deletions src/cli/maintain-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ const SCOPES: ScopeDemo[] = [
receipt: 'status: ✅ opened\nPR: https://github.com/you/app/pull/331\nverified: ✓ npm test (full suite)\nfiles: package.json (zod 3.22.4 → 3.23.8)' },
{ name: 'consolidate-dupes', blurb: 'merge ONE exact-duplicate helper pair — code-graph proves every caller', verdict: 'opened',
receipt: 'status: ✅ opened\nPR: https://github.com/you/app/pull/338\nverified: ✓ npm test · ✓ tsc\nfiles: src/format.ts (removed dup toKebab → src/util/case.ts), src/routes/api.ts' },
{ name: 'extract-helper', blurb: 'collapse ONE near-dupe cluster into a parameterized helper — originals become thin wrappers, zero call-site edits', verdict: 'opened',
receipt: 'status: ✅ opened\nPR: https://github.com/you/app/pull/341\nverified: ✓ npm test · ✓ tsc\nfiles: src/checks.ts (positive/negative/nonneg → addRangeCheck(kind, inclusive); signatures unchanged)' },
];

const STEPS = ['Code-graph analysis', 'Prove safe (or block)', 'Sandbox branch', 'Verify (tests + types)', 'Open PR', 'Trust receipt'];
Expand Down
28 changes: 25 additions & 3 deletions src/schedule/recipes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ function verifiedPrPrompt(goal: string): string {
}

/** The self-improving `maintain` recipe has SCOPES — each a conservative, provable cleanup. */
export type MaintainScope = 'dead-code' | 'unused-imports' | 'unused-locals' | 'unused-params' | 'lint-fix' | 'dep-bump' | 'consolidate-dupes';
export const MAINTAIN_SCOPES: readonly MaintainScope[] = ['dead-code', 'unused-imports', 'unused-locals', 'unused-params', 'lint-fix', 'dep-bump', 'consolidate-dupes'] as const;
export type MaintainScope = 'dead-code' | 'unused-imports' | 'unused-locals' | 'unused-params' | 'lint-fix' | 'dep-bump' | 'consolidate-dupes' | 'extract-helper';
export const MAINTAIN_SCOPES: readonly MaintainScope[] = ['dead-code', 'unused-imports', 'unused-locals', 'unused-params', 'lint-fix', 'dep-bump', 'consolidate-dupes', 'extract-helper'] as const;

/** Parse a maintain prompt into its scope + optional path focus + dry-run flag. PURE.
* Forms: "" (→ dead-code) · "unused-imports src/" · "lint-fix --dry-run" · "dep-bump" · "src/utils". */
Expand All @@ -81,7 +81,7 @@ export function parseMaintainScope(prompt: string): { scope: MaintainScope; focu
const dryRun = /(^|\s)(--dry-run|dry-run)(\s|$)/i.test(rest);
rest = rest.replace(/(^|\s)(--dry-run|dry-run)(\s|$)/ig, ' ').trim();
let scope: MaintainScope = 'dead-code';
const m = /^(unused[-_]?imports|imports|unused[-_]?locals|locals|unused[-_]?params|params|lint[-_]?fix|lint|dep[-_]?bump|deps?|dependenc(?:y|ies)|consolidate[-_]?dupes?|consolidate|duplicates?|dupes?|dedupe|dead[-_]?code)\b/i.exec(rest);
const m = /^(unused[-_]?imports|imports|unused[-_]?locals|locals|unused[-_]?params|params|lint[-_]?fix|lint|dep[-_]?bump|deps?|dependenc(?:y|ies)|consolidate[-_]?dupes?|consolidate|duplicates?|dupes?|dedupe|extract[-_]?helpers?|parameteri[sz]e|helpers?|dead[-_]?code)\b/i.exec(rest);
if (m) {
const k = m[1]!;
scope = /imports/i.test(k) ? 'unused-imports'
Expand All @@ -90,6 +90,7 @@ export function parseMaintainScope(prompt: string): { scope: MaintainScope; focu
: /lint/i.test(k) ? 'lint-fix'
: /dep/i.test(k) ? 'dep-bump'
: /consolidate|duplicate|dupe|dedupe/i.test(k) ? 'consolidate-dupes'
: /extract|parameteri|helper/i.test(k) ? 'extract-helper'
: 'dead-code';
rest = rest.slice(m[0].length).trim();
}
Expand Down Expand Up @@ -203,6 +204,26 @@ const CONSOLIDATE_DUPES_SELECTION = [
'e. This single consolidation IS your GOAL. Verification (tests + types) MUST pass or it does not ship.',
];

const EXTRACT_HELPER_SELECTION = [
'SCOPE (v8 — SAFE HELPER EXTRACTION, parameterized): collapse ONE cluster of near-duplicate',
'functions into a shared parameterized helper — WITHOUT touching a single call site. The',
'originals become thin delegating wrappers, so every signature, export, and caller stays',
'byte-for-byte compatible. This is the zero-caller-risk form of deduplication.',
'',
'SELECTION — mechanical proposal or block:',
'a. Run `find_similar_helpers`. Pick ONE cluster that comes back WITH a parameterize proposal',
' (the tool emits one only when the bodies align token-for-token and ≤4 spots vary). No',
' proposal on any cluster → BLOCK: manual judgment is required and 3am is not the time.',
'b. Skip clusters touching test files, generated code, or members the proposal lists as',
' `dropped` (structurally different) — the cluster must be fully covered by the proposal.',
'c. Create the shared helper (private to the module or a sibling shared file — do NOT create a',
' new public export unless the cluster spans files that already share an import path).',
'd. Rewrite EACH original function as a one-line wrapper delegating to the helper with its',
' argument mapping from the proposal. NEVER remove, rename, or re-sign the originals; NEVER',
' edit a call site. If a wrapper cannot be a pure delegation, BLOCK.',
'e. The helper + wrappers ARE your GOAL. Verification (tests + types) MUST pass or nothing ships.',
];

/**
* `maintain` — the self-improving codebase recipe. Each scope is the SAFEST improvement of its
* kind, proven (code-graph / toolchain) and shipped through the verified-PR protocol. Conservative
Expand All @@ -217,6 +238,7 @@ function maintainPrompt(prompt: string): string {
: scope === 'lint-fix' ? LINT_FIX_SELECTION
: scope === 'dep-bump' ? DEP_BUMP_SELECTION
: scope === 'consolidate-dupes' ? CONSOLIDATE_DUPES_SELECTION
: scope === 'extract-helper' ? EXTRACT_HELPER_SELECTION
: DEAD_CODE_SELECTION;
const focusLine = focus ? `Focus area (optional hint): ${focus}.` : '';
const dryRunBlock = dryRun ? [
Expand Down
2 changes: 1 addition & 1 deletion test/maintain-demo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe('buildMaintainDemoHtml', () => {

it('shows the nightly loop steps and all six scopes', () => {
for (const step of ['Code-graph analysis', 'Verify (tests + types)', 'Open PR', 'Trust receipt']) expect(html).toContain(step);
for (const scope of ['dead-code', 'unused-imports', 'unused-locals', 'unused-params', 'lint-fix', 'dep-bump', 'consolidate-dupes']) expect(html).toContain(scope);
for (const scope of ['dead-code', 'unused-imports', 'unused-locals', 'unused-params', 'lint-fix', 'dep-bump', 'consolidate-dupes', 'extract-helper']) expect(html).toContain(scope);
});

it('shows a trust receipt and the honesty claim (measured, not fabricated)', () => {
Expand Down
15 changes: 14 additions & 1 deletion test/schedule-recipes-delivery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,20 @@ describe('recipes — Autonomous Verified PR', () => {
expect(p).toMatch(/Near-duplicates .* are OUT of scope/i); // only exact equivalence
expect(p).toMatch(/enumerate EVERY caller/i); // a missed caller breaks the build
expect(p).toContain('```qodex-receipt');
expect(MAINTAIN_SCOPES).toEqual(['dead-code', 'unused-imports', 'unused-locals', 'unused-params', 'lint-fix', 'dep-bump', 'consolidate-dupes']);
});

it('maintain scope=extract-helper: mechanical proposal only, delegating wrappers, zero call-site edits', () => {
expect(parseMaintainScope('extract-helper src/')).toEqual({ scope: 'extract-helper', focus: 'src/', dryRun: false });
expect(parseMaintainScope('parameterize --dry-run')).toEqual({ scope: 'extract-helper', focus: '', dryRun: true });
expect(parseMaintainScope('helpers')).toEqual({ scope: 'extract-helper', focus: '', dryRun: false });
const p = buildRecipePrompt('maintain', 'extract-helper');
expect(p).toContain('SAFE HELPER EXTRACTION');
expect(p).toMatch(/WITHOUT touching a single call site/i);
expect(p).toMatch(/thin delegating wrappers/i);
expect(p).toMatch(/No[\s\S]*proposal on any cluster → BLOCK/i); // mechanical-or-block gate
expect(p).toMatch(/NEVER remove, rename, or re-sign/i);
expect(p).toContain('```qodex-receipt');
expect(MAINTAIN_SCOPES).toEqual(['dead-code', 'unused-imports', 'unused-locals', 'unused-params', 'lint-fix', 'dep-bump', 'consolidate-dupes', 'extract-helper']);
});
});

Expand Down