@fraction12/deepclean 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +171 -0
  4. package/dist/args.d.ts +9 -0
  5. package/dist/args.js +105 -0
  6. package/dist/args.js.map +1 -0
  7. package/dist/candidates.d.ts +6 -0
  8. package/dist/candidates.js +319 -0
  9. package/dist/candidates.js.map +1 -0
  10. package/dist/cli.d.ts +2 -0
  11. package/dist/cli.js +521 -0
  12. package/dist/cli.js.map +1 -0
  13. package/dist/clusters.d.ts +7 -0
  14. package/dist/clusters.js +399 -0
  15. package/dist/clusters.js.map +1 -0
  16. package/dist/defaults.d.ts +5 -0
  17. package/dist/defaults.js +130 -0
  18. package/dist/defaults.js.map +1 -0
  19. package/dist/discovery.d.ts +10 -0
  20. package/dist/discovery.js +48 -0
  21. package/dist/discovery.js.map +1 -0
  22. package/dist/evidence.d.ts +16 -0
  23. package/dist/evidence.js +853 -0
  24. package/dist/evidence.js.map +1 -0
  25. package/dist/ids.d.ts +4 -0
  26. package/dist/ids.js +16 -0
  27. package/dist/ids.js.map +1 -0
  28. package/dist/json.d.ts +3 -0
  29. package/dist/json.js +12 -0
  30. package/dist/json.js.map +1 -0
  31. package/dist/plans.d.ts +3 -0
  32. package/dist/plans.js +171 -0
  33. package/dist/plans.js.map +1 -0
  34. package/dist/reporting.d.ts +13 -0
  35. package/dist/reporting.js +227 -0
  36. package/dist/reporting.js.map +1 -0
  37. package/dist/reviewers.d.ts +22 -0
  38. package/dist/reviewers.js +461 -0
  39. package/dist/reviewers.js.map +1 -0
  40. package/dist/state.d.ts +41 -0
  41. package/dist/state.js +211 -0
  42. package/dist/state.js.map +1 -0
  43. package/dist/synthesis.d.ts +17 -0
  44. package/dist/synthesis.js +396 -0
  45. package/dist/synthesis.js.map +1 -0
  46. package/dist/types.d.ts +389 -0
  47. package/dist/types.js +236 -0
  48. package/dist/types.js.map +1 -0
  49. package/dist/verification.d.ts +11 -0
  50. package/dist/verification.js +111 -0
  51. package/dist/verification.js.map +1 -0
  52. package/docs/privacy-and-trust.md +33 -0
  53. package/docs/public-readiness.md +19 -0
  54. package/docs/reviewer-references.md +33 -0
  55. package/docs/troubleshooting.md +80 -0
  56. package/package.json +55 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ - Added repo-specific verification inference so generated candidates and plans point at real local checks such as Makefile targets, frontend package scripts, and admin package scripts instead of generic commands.
6
+ - Added explicit `reportPath`, `markdownPath`, `jsonPath`, and `planPath` fields to JSON output for automation.
7
+ - Improved Markdown reports with a focused `Agent Queue`, bounded themes before too-broad themes, and a capped candidate appendix while preserving full raw records in JSON.
8
+
9
+ ## 0.1.0-alpha.0 - 2026-05-24
10
+
11
+ - Added the public-alpha CLI package surface with installable `deepclean` binary metadata.
12
+ - Added global flag parsing before or after commands, including `deepclean --root ./repo scan --synthesize`.
13
+ - Added configurable local candidate caps, broad-theme splitting, and too-broad theme warnings.
14
+ - Added report recommendations with `Start Here`, suggested plan targets, theme warnings, and machine-readable recommendation data.
15
+ - Added configurable reviewer packs with built-in reviewer selection and source-safe custom reviewer path loading.
16
+ - Added privacy/trust and troubleshooting docs.
17
+ - Added package smoke testing from the packed tarball.
18
+ - Added CI and release checks that reject private/local artifacts from the package tarball.
19
+ - Added SARIF ingestion and optional `jscpd` external duplicate evidence.
20
+ - Removed self-dogfood state from the repo working tree and tightened ignore rules for local artifacts.
21
+ - Added a vendored MIT-licensed Matt Pocock skills reference snapshot and distilled reviewer rubrics for deep module discipline, feedback loops, and agent-ready cleanup slices.
22
+ - Hardened Clawpatch-style deslop mapping: TS/JS `.js` source specifiers now resolve to local TS source files, dynamic imports and `require(...)` are included in graph evidence, optional Semgrep orchestration is supported, report recommendations prefer strong synthesized findings over weak metric noise, and generated plans dedupe repeated file references.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OpenClaw contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,171 @@
1
+ # deepclean
2
+
3
+ Clawpatch-style cleanup reports for working-but-sloppy codebases.
4
+
5
+ `deepclean` is intended to help after a few days of AI-assisted coding: the app works, but the codebase needs architecture tightening, duplication removal, complexity reduction, better seams, stronger tests, and clearer domain language.
6
+
7
+ ## Status
8
+
9
+ Public-alpha ready local CLI. Product planning lives in `openspec/`.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install -g @fraction12/deepclean
15
+ deepclean --version
16
+ ```
17
+
18
+ From a fresh repo:
19
+
20
+ ```bash
21
+ deepclean init
22
+ deepclean scan --json
23
+ deepclean report
24
+ deepclean next
25
+ deepclean plan candidate-001
26
+ ```
27
+
28
+ With local Codex synthesis:
29
+
30
+ ```bash
31
+ deepclean scan --synthesize --json
32
+ deepclean report
33
+ deepclean plan theme-001
34
+ ```
35
+
36
+ Global flags work before or after the command:
37
+
38
+ ```bash
39
+ deepclean --root ./some-repo scan --synthesize
40
+ deepclean scan --root ./some-repo --synthesize
41
+ ```
42
+
43
+ ## Intended Flow
44
+
45
+ ```bash
46
+ deepclean init
47
+ deepclean scan
48
+ deepclean scan --synthesize
49
+ deepclean report
50
+ deepclean cluster
51
+ deepclean plan theme-001 --format codex
52
+ deepclean next
53
+ deepclean show <candidate-id>
54
+ deepclean triage <candidate-id> --status ignored --note "intentional boundary"
55
+ deepclean handoff <candidate-id> --format codex
56
+ ```
57
+
58
+ ## Principles
59
+
60
+ - Report first; no source mutation in the MVP.
61
+ - Structured local evidence before model review.
62
+ - Durable state under `.deepclean/`.
63
+ - Local code stays local unless the user explicitly enables researched context.
64
+ - Architecture review is one reviewer, not the whole product.
65
+
66
+ ## Agent UX
67
+
68
+ All core commands support `--json` for machine-readable output.
69
+
70
+ ```bash
71
+ deepclean scan --json
72
+ deepclean scan --synthesize --json
73
+ deepclean report --json
74
+ deepclean cluster --json
75
+ deepclean plan theme-001 --json
76
+ deepclean next --json
77
+ deepclean show candidate-001 --json
78
+ deepclean handoff candidate-001 --json
79
+ ```
80
+
81
+ Useful global flags:
82
+
83
+ - `--root <path>`
84
+ - `--state-dir <path>`
85
+ - `--config <path>`
86
+ - `--no-input`
87
+ - `--quiet`
88
+ - `--debug`
89
+
90
+ ## Codex Synthesis
91
+
92
+ `deepclean scan --synthesize` runs the local `codex` CLI in read-only mode over the collected evidence bundle. The model is asked to return strict JSON, and candidates without valid evidence IDs are rejected.
93
+
94
+ Synthesis uses a built-in reviewer pack rather than whatever agent skills happen to be installed locally. That keeps runs reproducible. The current pack covers architecture deepening, deep module discipline, conceptual duplication, dependency graph blast radius, testability, feedback loop discipline, domain language drift, agent-ready cleanup slices, AI-slop patterns, and a critic pass that rejects weak one-metric findings.
95
+
96
+ The reviewer pack is informed by a vendored MIT-licensed snapshot of Matt Pocock's engineering skills. Deepclean uses those skills as reference material and distills the useful principles into stable built-in rubrics instead of loading the full upstream skill text dynamically on every run.
97
+
98
+ Reviewer packs can be configured in `.deepclean/config.json`:
99
+
100
+ ```json
101
+ {
102
+ "reviewers": {
103
+ "enabled": ["architecture-deepening", "testability", "critic-pass"],
104
+ "customPaths": ["./deepclean-reviewers/security.md"]
105
+ }
106
+ }
107
+ ```
108
+
109
+ Before prompting Codex, Deepclean also maps evidence and existing local candidates into bounded cleanup surfaces. This is the Clawpatch-inspired part: the model reviews mapped repo areas and graph-connected themes rather than a loose pile of metrics.
110
+
111
+ Source samples are redacted from the synthesis prompt by default. Use `--allow-source-in-model` only when the target repository and provider configuration make that acceptable.
112
+
113
+ See [Reviewer References](docs/reviewer-references.md), [Privacy And Trust](docs/privacy-and-trust.md), and [Troubleshooting](docs/troubleshooting.md) before using synthesis on private repos.
114
+
115
+ ## Themes And Plans
116
+
117
+ `deepclean cluster` groups related candidates into cleanup themes using shared files, shared evidence, module areas, title language, and the local import graph. Themes are persisted under `.deepclean/clusters/` and use stable `theme-001` style IDs for agent workflows. Individual cleanup candidates use `candidate-001` style IDs. Broad themes are split where possible and marked `too-broad` when they should not be handed to an agent as a single plan.
118
+
119
+ `deepclean plan <candidate-or-theme-id>` writes a Codex-ready cleanup plan under `.deepclean/plans/`. Use theme plans when the report points at a larger cleanup area such as a tangled Next.js app area or a backend service boundary; use candidate plans for narrow local cleanup.
120
+
121
+ Deepclean currently collects TypeScript, JavaScript, and Python source evidence. The local graph supports TS/JS relative imports and Python module imports, with cache/build/output directories excluded by default.
122
+ For TS/JS projects using NodeNext-style source imports, Deepclean resolves emitted `.js` specifiers back to local `.ts`, `.tsx`, `.mts`, and `.cts` files so the graph maps source boundaries instead of falsely reporting an empty graph.
123
+
124
+ ## Evidence Engines
125
+
126
+ Deepclean runs local evidence first and model synthesis second. The built-in layer includes:
127
+
128
+ - file metrics
129
+ - normalized line-window duplication
130
+ - source/import graph summaries
131
+ - TypeScript/JavaScript function and wrapper structure
132
+ - Python import graph support
133
+ - git churn signals
134
+ - nearby test discovery
135
+ - SARIF ingestion from Semgrep or similar tools
136
+ - optional Semgrep SARIF orchestration when configured
137
+ - optional `jscpd` duplicate ingestion when configured
138
+
139
+ To use external analyzer evidence:
140
+
141
+ ```json
142
+ {
143
+ "enabledAdapters": ["file-metrics", "sarif-ingest", "jscpd", "code-graph"],
144
+ "externalAnalyzers": {
145
+ "semgrep": {
146
+ "enabled": true,
147
+ "command": "semgrep",
148
+ "config": "auto",
149
+ "timeoutMs": 120000,
150
+ "maxFindings": 80
151
+ },
152
+ "jscpd": {
153
+ "enabled": true,
154
+ "command": "jscpd",
155
+ "minTokens": 80,
156
+ "maxFindings": 20
157
+ },
158
+ "sarifPaths": ["semgrep.sarif", ".semgrep/semgrep.sarif"]
159
+ }
160
+ }
161
+ ```
162
+
163
+ ## Release Checks
164
+
165
+ ```bash
166
+ npm run ci
167
+ npm run spec:validate
168
+ npm run release:check
169
+ ```
170
+
171
+ The release check builds the package, runs tests, validates OpenSpec locally when available, packs the tarball, and rejects private/local artifacts such as `.deepclean/`, `.codex/`, `node_modules/`, source files, and local reports.
package/dist/args.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ export interface ParsedArgs {
2
+ command?: string;
3
+ positional: string[];
4
+ flags: Record<string, string | boolean>;
5
+ }
6
+ export declare function parseArgs(argv: string[]): ParsedArgs;
7
+ export declare function legacyParseArgs(argv: string[]): ParsedArgs;
8
+ export declare function flagString(flags: Record<string, string | boolean>, key: string): string | undefined;
9
+ export declare function flagBoolean(flags: Record<string, string | boolean>, key: string): boolean;
package/dist/args.js ADDED
@@ -0,0 +1,105 @@
1
+ export function parseArgs(argv) {
2
+ let command;
3
+ const positional = [];
4
+ const flags = {};
5
+ for (let index = 0; index < argv.length; index += 1) {
6
+ const token = argv[index];
7
+ if (!token) {
8
+ continue;
9
+ }
10
+ if (token.startsWith("--")) {
11
+ const withoutPrefix = token.slice(2);
12
+ const equalsIndex = withoutPrefix.indexOf("=");
13
+ if (equalsIndex >= 0) {
14
+ const key = withoutPrefix.slice(0, equalsIndex);
15
+ const value = withoutPrefix.slice(equalsIndex + 1);
16
+ flags[key] = value;
17
+ continue;
18
+ }
19
+ const next = argv[index + 1];
20
+ if (valueFlags.has(withoutPrefix)) {
21
+ if (next && !next.startsWith("-")) {
22
+ flags[withoutPrefix] = next;
23
+ index += 1;
24
+ }
25
+ else {
26
+ flags[withoutPrefix] = "";
27
+ }
28
+ }
29
+ else {
30
+ flags[withoutPrefix] = true;
31
+ }
32
+ continue;
33
+ }
34
+ if (token.startsWith("-") && token.length > 1) {
35
+ const shortFlags = token.slice(1).split("");
36
+ for (const flag of shortFlags) {
37
+ flags[flag] = true;
38
+ }
39
+ continue;
40
+ }
41
+ if (!command) {
42
+ command = token;
43
+ }
44
+ else {
45
+ positional.push(token);
46
+ }
47
+ }
48
+ return command ? { command, positional, flags } : { positional, flags };
49
+ }
50
+ const valueFlags = new Set([
51
+ "root",
52
+ "state-dir",
53
+ "config",
54
+ "model",
55
+ "status",
56
+ "note",
57
+ "format",
58
+ ]);
59
+ export function legacyParseArgs(argv) {
60
+ const [command, ...rest] = argv;
61
+ const positional = [];
62
+ const flags = {};
63
+ for (let index = 0; index < rest.length; index += 1) {
64
+ const token = rest[index];
65
+ if (!token) {
66
+ continue;
67
+ }
68
+ if (token.startsWith("--")) {
69
+ const withoutPrefix = token.slice(2);
70
+ const equalsIndex = withoutPrefix.indexOf("=");
71
+ if (equalsIndex >= 0) {
72
+ const key = withoutPrefix.slice(0, equalsIndex);
73
+ const value = withoutPrefix.slice(equalsIndex + 1);
74
+ flags[key] = value;
75
+ continue;
76
+ }
77
+ const next = rest[index + 1];
78
+ if (next && !next.startsWith("-")) {
79
+ flags[withoutPrefix] = next;
80
+ index += 1;
81
+ }
82
+ else {
83
+ flags[withoutPrefix] = true;
84
+ }
85
+ continue;
86
+ }
87
+ if (token.startsWith("-") && token.length > 1) {
88
+ const shortFlags = token.slice(1).split("");
89
+ for (const flag of shortFlags) {
90
+ flags[flag] = true;
91
+ }
92
+ continue;
93
+ }
94
+ positional.push(token);
95
+ }
96
+ return command ? { command, positional, flags } : { positional, flags };
97
+ }
98
+ export function flagString(flags, key) {
99
+ const value = flags[key];
100
+ return typeof value === "string" ? value : undefined;
101
+ }
102
+ export function flagBoolean(flags, key) {
103
+ return flags[key] === true;
104
+ }
105
+ //# sourceMappingURL=args.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"args.js","sourceRoot":"","sources":["../src/args.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import { type VerificationProfile } from "./verification.js";
2
+ import { type CandidateRecord, type DeepcleanConfig, type EvidenceRecord } from "./types.js";
3
+ export type CandidateCaps = DeepcleanConfig["candidateCaps"];
4
+ export declare function candidatesFromEvidence(runId: string, evidence: EvidenceRecord[], createdAt: string, caps?: CandidateCaps, verificationProfile?: VerificationProfile): CandidateRecord[];
5
+ export declare function rankCandidates(candidates: CandidateRecord[]): CandidateRecord[];
6
+ export declare function reassignCandidateIds(candidates: CandidateRecord[]): CandidateRecord[];
@@ -0,0 +1,319 @@
1
+ import { candidateId, } from "./ids.js";
2
+ import { commandsForFiles } from "./verification.js";
3
+ import { schemaVersion, } from "./types.js";
4
+ const priorityScore = {
5
+ P0: 0,
6
+ P1: 1,
7
+ P2: 2,
8
+ P3: 3,
9
+ };
10
+ export function candidatesFromEvidence(runId, evidence, createdAt, caps, verificationProfile) {
11
+ const sortedEvidence = [...evidence].sort(compareEvidence);
12
+ const candidates = [];
13
+ const kindCounts = new Map();
14
+ const areaKindCounts = new Map();
15
+ for (const record of sortedEvidence) {
16
+ if (!shouldCreateLocalCandidate(record, kindCounts, areaKindCounts, caps)) {
17
+ continue;
18
+ }
19
+ const candidate = candidateForEvidence(record, runId, createdAt, candidates.length, verificationProfile);
20
+ if (!candidate) {
21
+ continue;
22
+ }
23
+ candidates.push(candidate);
24
+ kindCounts.set(record.kind, (kindCounts.get(record.kind) ?? 0) + 1);
25
+ for (const area of candidateAreas(candidate)) {
26
+ const key = `${record.kind}:${area}`;
27
+ areaKindCounts.set(key, (areaKindCounts.get(key) ?? 0) + 1);
28
+ }
29
+ }
30
+ return reassignCandidateIds(rankCandidates(candidates));
31
+ }
32
+ export function rankCandidates(candidates) {
33
+ return [...candidates].sort((a, b) => {
34
+ const priorityDelta = priorityScore[a.priority] - priorityScore[b.priority];
35
+ if (priorityDelta !== 0) {
36
+ return priorityDelta;
37
+ }
38
+ const provenanceDelta = provenanceScore(b) - provenanceScore(a);
39
+ if (provenanceDelta !== 0) {
40
+ return provenanceDelta;
41
+ }
42
+ const impactDelta = impactScore(b.impact) - impactScore(a.impact);
43
+ if (impactDelta !== 0) {
44
+ return impactDelta;
45
+ }
46
+ const confidenceDelta = confidenceScore(b.confidence) - confidenceScore(a.confidence);
47
+ if (confidenceDelta !== 0) {
48
+ return confidenceDelta;
49
+ }
50
+ return a.id.localeCompare(b.id);
51
+ });
52
+ }
53
+ function shouldCreateLocalCandidate(evidence, kindCounts, areaKindCounts, caps) {
54
+ const kindLimit = localCandidateKindLimit(evidence.kind, caps);
55
+ if (kindLimit !== undefined && (kindCounts.get(evidence.kind) ?? 0) >= kindLimit) {
56
+ return false;
57
+ }
58
+ const areaLimit = localCandidateAreaLimit(evidence.kind, caps);
59
+ if (areaLimit === undefined) {
60
+ return true;
61
+ }
62
+ const areas = evidence.files.length > 0
63
+ ? [...new Set(evidence.files.map((file) => candidateArea(file.path)))]
64
+ : [evidence.kind];
65
+ return areas.some((area) => (areaKindCounts.get(`${evidence.kind}:${area}`) ?? 0) < areaLimit);
66
+ }
67
+ function localCandidateKindLimit(kind, caps) {
68
+ const configured = caps?.byKind[kind];
69
+ if (configured !== undefined) {
70
+ return configured;
71
+ }
72
+ switch (kind) {
73
+ case "duplicate-cluster":
74
+ return 16;
75
+ case "dependency-hotspot":
76
+ return 24;
77
+ case "large-function":
78
+ return 24;
79
+ case "large-file":
80
+ return 24;
81
+ case "test-gap":
82
+ return 24;
83
+ case "churn-hotspot":
84
+ return 12;
85
+ case "shallow-wrapper-cluster":
86
+ return 16;
87
+ default:
88
+ return undefined;
89
+ }
90
+ }
91
+ function localCandidateAreaLimit(kind, caps) {
92
+ const configured = caps?.byKindAndArea[kind];
93
+ if (configured !== undefined) {
94
+ return configured;
95
+ }
96
+ switch (kind) {
97
+ case "duplicate-cluster":
98
+ return 4;
99
+ case "dependency-hotspot":
100
+ case "large-function":
101
+ case "large-file":
102
+ case "test-gap":
103
+ return 8;
104
+ default:
105
+ return undefined;
106
+ }
107
+ }
108
+ export function reassignCandidateIds(candidates) {
109
+ return candidates.map((candidate, index) => ({
110
+ ...candidate,
111
+ id: candidateId(index),
112
+ }));
113
+ }
114
+ function candidateForEvidence(evidence, runId, createdAt, index, verificationProfile) {
115
+ const verification = commandsForFiles(verificationProfile ?? {
116
+ defaultCommands: ["npm test", "npm run typecheck"],
117
+ pythonCommands: ["npm test", "npm run typecheck"],
118
+ frontendCommands: ["npm test", "npm run typecheck"],
119
+ adminCommands: ["npm test", "npm run typecheck"],
120
+ }, evidence.files, ["npm test", "npm run typecheck"]);
121
+ const base = {
122
+ schemaVersion,
123
+ recordType: "candidate",
124
+ id: candidateId(index),
125
+ runId,
126
+ status: "open",
127
+ files: evidence.files,
128
+ evidenceIds: [evidence.id],
129
+ provenance: {
130
+ source: "local-evidence",
131
+ },
132
+ createdAt,
133
+ updatedAt: createdAt,
134
+ };
135
+ switch (evidence.kind) {
136
+ case "duplicate-cluster":
137
+ case "external-duplicate":
138
+ return {
139
+ ...base,
140
+ title: evidence.title,
141
+ category: "duplication",
142
+ priority: evidence.confidence === "high" ? "P1" : "P2",
143
+ confidence: evidence.confidence,
144
+ impact: evidence.files.length >= 3 ? "cross-cutting" : "feature",
145
+ effort: "medium",
146
+ risk: "moderate",
147
+ whyItMatters: "Repeated code paths tend to drift after later AI edits, creating inconsistent behavior that tests may not cover.",
148
+ likelyRootCause: "Similar behavior was implemented in multiple places instead of being pulled into one domain-level module or shared component.",
149
+ suggestedDirection: "Inspect the duplicated call sites and decide whether the shared concept should become a single module, helper, component, or explicit abstraction.",
150
+ verification,
151
+ };
152
+ case "sarif-finding":
153
+ return {
154
+ ...base,
155
+ title: evidence.title,
156
+ category: "diagnostic",
157
+ priority: evidence.confidence === "high" ? "P1" : "P2",
158
+ confidence: evidence.confidence,
159
+ impact: evidence.files.length >= 3 ? "cross-cutting" : "feature",
160
+ effort: "medium",
161
+ risk: "moderate",
162
+ whyItMatters: "External analyzer findings can point at maintainability, correctness, or policy issues that local Deepclean heuristics should preserve as evidence.",
163
+ likelyRootCause: "A specialized analyzer found a source-level issue that needs human or agent review before structural cleanup.",
164
+ suggestedDirection: "Inspect the analyzer finding, decide whether it is real, and either address it directly or triage it with a note before deeper cleanup.",
165
+ verification,
166
+ };
167
+ case "dependency-hotspot":
168
+ return {
169
+ ...base,
170
+ title: evidence.title,
171
+ category: "architecture",
172
+ priority: evidence.confidence === "high" ? "P1" : "P2",
173
+ confidence: evidence.confidence,
174
+ impact: "cross-cutting",
175
+ effort: "medium",
176
+ risk: "design-needed",
177
+ whyItMatters: "Files with high fan-in or fan-out become expensive to change because unrelated features may depend on their shape.",
178
+ likelyRootCause: "Responsibilities may be concentrated in a broad helper, orchestration module, or feature file that needs clearer boundaries.",
179
+ suggestedDirection: "Review callers and imports, then separate stable domain logic from feature-specific coordination if the coupling is accidental.",
180
+ verification,
181
+ };
182
+ case "large-file":
183
+ case "large-function":
184
+ return {
185
+ ...base,
186
+ title: evidence.title,
187
+ category: "complexity",
188
+ priority: evidence.confidence === "high" ? "P1" : "P2",
189
+ confidence: evidence.confidence,
190
+ impact: "feature",
191
+ effort: evidence.confidence === "high" ? "large" : "medium",
192
+ risk: "moderate",
193
+ whyItMatters: "Large units are harder for agents and humans to review, test, and change without accidentally mixing concerns.",
194
+ likelyRootCause: "Fetching, state, validation, rendering, or orchestration logic may have accumulated in one place during fast implementation.",
195
+ suggestedDirection: "Map the responsibilities in this unit, extract stable domain logic first, and leave UI or orchestration behavior thin.",
196
+ verification,
197
+ };
198
+ case "test-gap":
199
+ return {
200
+ ...base,
201
+ title: evidence.title,
202
+ category: "testability",
203
+ priority: "P2",
204
+ confidence: evidence.confidence,
205
+ impact: "feature",
206
+ effort: "small",
207
+ risk: "safe",
208
+ whyItMatters: "Cleanup work is riskier when feature logic has weak nearby tests or no obvious verification path.",
209
+ likelyRootCause: "The feature may have been implemented before test seams or source-to-test structure were established.",
210
+ suggestedDirection: "Add targeted tests around the behavior before significant refactoring, especially for validation, state transitions, and edge cases.",
211
+ verification,
212
+ };
213
+ case "churn-hotspot":
214
+ return {
215
+ ...base,
216
+ title: evidence.title,
217
+ category: "architecture",
218
+ priority: evidence.confidence === "high" ? "P1" : "P2",
219
+ confidence: evidence.confidence,
220
+ impact: "feature",
221
+ effort: "medium",
222
+ risk: "design-needed",
223
+ whyItMatters: "High-churn files are often where messy abstractions and changing product behavior collide.",
224
+ likelyRootCause: "The module may be absorbing multiple concerns or serving as the easiest place for repeated agent edits.",
225
+ suggestedDirection: "Compare the churn history with current responsibilities and look for concepts that should move closer to their owners.",
226
+ verification,
227
+ };
228
+ case "shallow-wrapper-cluster":
229
+ return {
230
+ ...base,
231
+ title: evidence.title,
232
+ category: "ai-slop",
233
+ priority: "P2",
234
+ confidence: evidence.confidence,
235
+ impact: "feature",
236
+ effort: "small",
237
+ risk: "moderate",
238
+ whyItMatters: "Clusters of shallow wrappers create indirection without leverage and make agents chase names instead of concepts.",
239
+ likelyRootCause: "Fast AI-assisted implementation may have introduced wrapper helpers to make code look organized without creating real boundaries.",
240
+ suggestedDirection: "Review the cluster and keep only wrappers that name real domain concepts, centralize policy, or provide stable seams.",
241
+ verification,
242
+ };
243
+ default:
244
+ return undefined;
245
+ }
246
+ }
247
+ function compareEvidence(a, b) {
248
+ const kindDelta = evidenceKindScore(b.kind) - evidenceKindScore(a.kind);
249
+ if (kindDelta !== 0) {
250
+ return kindDelta;
251
+ }
252
+ const confidenceDelta = confidenceScore(b.confidence) - confidenceScore(a.confidence);
253
+ if (confidenceDelta !== 0) {
254
+ return confidenceDelta;
255
+ }
256
+ return a.id.localeCompare(b.id);
257
+ }
258
+ function evidenceKindScore(kind) {
259
+ switch (kind) {
260
+ case "duplicate-cluster":
261
+ return 80;
262
+ case "dependency-hotspot":
263
+ return 75;
264
+ case "large-function":
265
+ return 70;
266
+ case "large-file":
267
+ return 65;
268
+ case "churn-hotspot":
269
+ return 60;
270
+ case "test-gap":
271
+ return 45;
272
+ case "shallow-wrapper-cluster":
273
+ return 20;
274
+ default:
275
+ return 0;
276
+ }
277
+ }
278
+ function confidenceScore(confidence) {
279
+ switch (confidence) {
280
+ case "high":
281
+ return 3;
282
+ case "medium":
283
+ return 2;
284
+ case "low":
285
+ return 1;
286
+ }
287
+ }
288
+ function impactScore(impact) {
289
+ switch (impact) {
290
+ case "cross-cutting":
291
+ return 3;
292
+ case "feature":
293
+ return 2;
294
+ case "local":
295
+ return 1;
296
+ }
297
+ }
298
+ function provenanceScore(candidate) {
299
+ return candidate.provenance.source === "model-synthesis" ? 2 : 1;
300
+ }
301
+ function candidateAreas(candidate) {
302
+ return [...new Set(candidate.files.map((file) => candidateArea(file.path)))];
303
+ }
304
+ function candidateArea(filePath) {
305
+ const parts = filePath.split("/").filter(Boolean);
306
+ if (parts.length === 0) {
307
+ return ".";
308
+ }
309
+ const srcIndex = parts.indexOf("src");
310
+ if (srcIndex >= 0) {
311
+ const end = Math.min(parts.length - 1, srcIndex + 3);
312
+ return parts.slice(0, end).join("/") || (parts[0] ?? ".");
313
+ }
314
+ if (parts.length <= 2) {
315
+ return parts.length === 1 ? "." : parts[0] ?? ".";
316
+ }
317
+ return parts.slice(0, 2).join("/");
318
+ }
319
+ //# sourceMappingURL=candidates.js.map