edsger 0.75.0 → 0.76.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.
- package/dist/commands/find-architecture/index.d.ts +3 -1
- package/dist/commands/find-architecture/index.js +14 -5
- package/dist/commands/find-bugs/index.d.ts +3 -1
- package/dist/commands/find-bugs/index.js +14 -5
- package/dist/commands/find-smells/index.d.ts +3 -1
- package/dist/commands/find-smells/index.js +14 -5
- package/dist/index.js +9 -6
- package/dist/phases/find-architecture/index.d.ts +4 -1
- package/dist/phases/find-architecture/index.js +37 -24
- package/dist/phases/find-bugs/index.d.ts +4 -1
- package/dist/phases/find-bugs/index.js +29 -18
- package/dist/phases/find-shared/mcp.d.ts +18 -3
- package/dist/phases/find-shared/mcp.js +39 -4
- package/dist/phases/find-smells/index.d.ts +4 -1
- package/dist/phases/find-smells/index.js +34 -21
- package/package.json +1 -1
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
* and files each new finding as an issue.
|
|
6
6
|
*/
|
|
7
7
|
export interface FindArchitectureCliOptions {
|
|
8
|
+
/** Repo-only mode: scan a single `repositories` row with no product. */
|
|
9
|
+
repoId?: string;
|
|
8
10
|
full?: boolean;
|
|
9
11
|
branch?: string;
|
|
10
12
|
maxFiles?: number;
|
|
11
13
|
verbose?: boolean;
|
|
12
14
|
}
|
|
13
|
-
export declare function runFindArchitecture(productId: string, options: FindArchitectureCliOptions): Promise<void>;
|
|
15
|
+
export declare function runFindArchitecture(productId: string | undefined, options: FindArchitectureCliOptions): Promise<void>;
|
|
@@ -4,22 +4,31 @@
|
|
|
4
4
|
* coupling, cycles, missing/duplicated abstractions, responsibility creep)
|
|
5
5
|
* and files each new finding as an issue.
|
|
6
6
|
*/
|
|
7
|
-
import { getGitHubConfigByProduct } from '../../api/github.js';
|
|
7
|
+
import { getGitHubConfigByProduct, getGitHubConfigByRepository, } from '../../api/github.js';
|
|
8
8
|
import { scanForArchitecture } from '../../phases/find-architecture/index.js';
|
|
9
9
|
import { logError, logInfo, logSuccess } from '../../utils/logger.js';
|
|
10
10
|
export async function runFindArchitecture(productId, options) {
|
|
11
|
-
const { full, branch, maxFiles, verbose } = options;
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
const { repoId, full, branch, maxFiles, verbose } = options;
|
|
12
|
+
if (!productId && !repoId) {
|
|
13
|
+
throw new Error('Provide a productId or --repo-id for find-architecture');
|
|
14
|
+
}
|
|
15
|
+
const scopeLabel = productId
|
|
16
|
+
? `product ${productId}`
|
|
17
|
+
: `repository ${repoId}`;
|
|
18
|
+
logInfo(`Starting architecture scan for ${scopeLabel}`);
|
|
19
|
+
const githubConfig = productId
|
|
20
|
+
? await getGitHubConfigByProduct(productId, verbose)
|
|
21
|
+
: await getGitHubConfigByRepository(repoId, verbose);
|
|
14
22
|
if (!githubConfig.configured ||
|
|
15
23
|
!githubConfig.token ||
|
|
16
24
|
!githubConfig.owner ||
|
|
17
25
|
!githubConfig.repo) {
|
|
18
|
-
logError(`GitHub not configured for
|
|
26
|
+
logError(`GitHub not configured for ${scopeLabel}: ${githubConfig.message || 'No installation found'}`);
|
|
19
27
|
process.exit(1);
|
|
20
28
|
}
|
|
21
29
|
const result = await scanForArchitecture({
|
|
22
30
|
productId,
|
|
31
|
+
repoId,
|
|
23
32
|
githubToken: githubConfig.token,
|
|
24
33
|
owner: githubConfig.owner,
|
|
25
34
|
repo: githubConfig.repo,
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
* Audits the product's repository for bugs and files each new finding as an issue.
|
|
4
4
|
*/
|
|
5
5
|
export interface FindBugsCliOptions {
|
|
6
|
+
/** Repo-only mode: scan a single `repositories` row with no product. */
|
|
7
|
+
repoId?: string;
|
|
6
8
|
full?: boolean;
|
|
7
9
|
branch?: string;
|
|
8
10
|
maxFiles?: number;
|
|
9
11
|
verbose?: boolean;
|
|
10
12
|
}
|
|
11
|
-
export declare function runFindBugs(productId: string, options: FindBugsCliOptions): Promise<void>;
|
|
13
|
+
export declare function runFindBugs(productId: string | undefined, options: FindBugsCliOptions): Promise<void>;
|
|
@@ -2,22 +2,31 @@
|
|
|
2
2
|
* CLI command: edsger find-bugs <productId>
|
|
3
3
|
* Audits the product's repository for bugs and files each new finding as an issue.
|
|
4
4
|
*/
|
|
5
|
-
import { getGitHubConfigByProduct } from '../../api/github.js';
|
|
5
|
+
import { getGitHubConfigByProduct, getGitHubConfigByRepository, } from '../../api/github.js';
|
|
6
6
|
import { scanForBugs } from '../../phases/find-bugs/index.js';
|
|
7
7
|
import { logError, logInfo, logSuccess } from '../../utils/logger.js';
|
|
8
8
|
export async function runFindBugs(productId, options) {
|
|
9
|
-
const { full, branch, maxFiles, verbose } = options;
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
const { repoId, full, branch, maxFiles, verbose } = options;
|
|
10
|
+
if (!productId && !repoId) {
|
|
11
|
+
throw new Error('Provide a productId or --repo-id for find-bugs');
|
|
12
|
+
}
|
|
13
|
+
const scopeLabel = productId
|
|
14
|
+
? `product ${productId}`
|
|
15
|
+
: `repository ${repoId}`;
|
|
16
|
+
logInfo(`Starting bug scan for ${scopeLabel}`);
|
|
17
|
+
const githubConfig = productId
|
|
18
|
+
? await getGitHubConfigByProduct(productId, verbose)
|
|
19
|
+
: await getGitHubConfigByRepository(repoId, verbose);
|
|
12
20
|
if (!githubConfig.configured ||
|
|
13
21
|
!githubConfig.token ||
|
|
14
22
|
!githubConfig.owner ||
|
|
15
23
|
!githubConfig.repo) {
|
|
16
|
-
logError(`GitHub not configured for
|
|
24
|
+
logError(`GitHub not configured for ${scopeLabel}: ${githubConfig.message || 'No installation found'}`);
|
|
17
25
|
process.exit(1);
|
|
18
26
|
}
|
|
19
27
|
const result = await scanForBugs({
|
|
20
28
|
productId,
|
|
29
|
+
repoId,
|
|
21
30
|
githubToken: githubConfig.token,
|
|
22
31
|
owner: githubConfig.owner,
|
|
23
32
|
repo: githubConfig.repo,
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { type SmellCategory } from '../../phases/find-smells/types.js';
|
|
7
7
|
export interface FindSmellsCliOptions {
|
|
8
|
+
/** Repo-only mode: scan a single `repositories` row with no product. */
|
|
9
|
+
repoId?: string;
|
|
8
10
|
full?: boolean;
|
|
9
11
|
branch?: string;
|
|
10
12
|
maxFiles?: number;
|
|
@@ -18,4 +20,4 @@ export interface FindSmellsCliOptions {
|
|
|
18
20
|
* matches user intuition for `--categories=`.
|
|
19
21
|
*/
|
|
20
22
|
export declare function parseCategoriesOption(raw: string): SmellCategory[] | undefined;
|
|
21
|
-
export declare function runFindSmells(productId: string, options: FindSmellsCliOptions): Promise<void>;
|
|
23
|
+
export declare function runFindSmells(productId: string | undefined, options: FindSmellsCliOptions): Promise<void>;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Audits the product's repository for code smells (refactor candidates, perf
|
|
4
4
|
* cliffs, dead code, etc.) and files each new finding as an issue.
|
|
5
5
|
*/
|
|
6
|
-
import { getGitHubConfigByProduct } from '../../api/github.js';
|
|
6
|
+
import { getGitHubConfigByProduct, getGitHubConfigByRepository, } from '../../api/github.js';
|
|
7
7
|
import { scanForSmells } from '../../phases/find-smells/index.js';
|
|
8
8
|
import { isSmellCategory, SMELL_CATEGORIES, } from '../../phases/find-smells/types.js';
|
|
9
9
|
import { logError, logInfo, logSuccess } from '../../utils/logger.js';
|
|
@@ -31,18 +31,27 @@ export function parseCategoriesOption(raw) {
|
|
|
31
31
|
return Array.from(new Set(tokens));
|
|
32
32
|
}
|
|
33
33
|
export async function runFindSmells(productId, options) {
|
|
34
|
-
const { full, branch, maxFiles, categories, verbose } = options;
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
const { repoId, full, branch, maxFiles, categories, verbose } = options;
|
|
35
|
+
if (!productId && !repoId) {
|
|
36
|
+
throw new Error('Provide a productId or --repo-id for find-smells');
|
|
37
|
+
}
|
|
38
|
+
const scopeLabel = productId
|
|
39
|
+
? `product ${productId}`
|
|
40
|
+
: `repository ${repoId}`;
|
|
41
|
+
logInfo(`Starting smell scan for ${scopeLabel}`);
|
|
42
|
+
const githubConfig = productId
|
|
43
|
+
? await getGitHubConfigByProduct(productId, verbose)
|
|
44
|
+
: await getGitHubConfigByRepository(repoId, verbose);
|
|
37
45
|
if (!githubConfig.configured ||
|
|
38
46
|
!githubConfig.token ||
|
|
39
47
|
!githubConfig.owner ||
|
|
40
48
|
!githubConfig.repo) {
|
|
41
|
-
logError(`GitHub not configured for
|
|
49
|
+
logError(`GitHub not configured for ${scopeLabel}: ${githubConfig.message || 'No installation found'}`);
|
|
42
50
|
process.exit(1);
|
|
43
51
|
}
|
|
44
52
|
const result = await scanForSmells({
|
|
45
53
|
productId,
|
|
54
|
+
repoId,
|
|
46
55
|
githubToken: githubConfig.token,
|
|
47
56
|
owner: githubConfig.owner,
|
|
48
57
|
repo: githubConfig.repo,
|
package/dist/index.js
CHANGED
|
@@ -802,8 +802,9 @@ program
|
|
|
802
802
|
// Subcommand: edsger find-bugs <productId>
|
|
803
803
|
// ============================================================
|
|
804
804
|
program
|
|
805
|
-
.command('find-bugs
|
|
806
|
-
.description("AI-audit a product's repository for bugs and file each new finding as an issue")
|
|
805
|
+
.command('find-bugs [productId]')
|
|
806
|
+
.description("AI-audit a product's (or a standalone repository's) repo for bugs and file each new finding as an issue")
|
|
807
|
+
.option('--repo-id <id>', 'Run in repo-only mode against a single repositories row (no product context)')
|
|
807
808
|
.option('--full', 'Force a full scan even if previous-scan state exists')
|
|
808
809
|
.option('--branch <name>', 'Branch to scan (defaults to repo default branch)')
|
|
809
810
|
.option('--max-files <n>', 'Upper bound on files the auditor may Read (default 200)', (value) => {
|
|
@@ -1030,8 +1031,9 @@ program
|
|
|
1030
1031
|
// Subcommand: edsger find-smells <productId>
|
|
1031
1032
|
// ============================================================
|
|
1032
1033
|
program
|
|
1033
|
-
.command('find-smells
|
|
1034
|
-
.description("AI-audit a product's repository for code smells (refactor candidates, perf cliffs, dead code, type-safety gaps) and file each new finding as an issue")
|
|
1034
|
+
.command('find-smells [productId]')
|
|
1035
|
+
.description("AI-audit a product's (or a standalone repository's) repo for code smells (refactor candidates, perf cliffs, dead code, type-safety gaps) and file each new finding as an issue")
|
|
1036
|
+
.option('--repo-id <id>', 'Run in repo-only mode against a single repositories row (no product context)')
|
|
1035
1037
|
.option('--full', 'Force a full scan even if previous-scan state exists')
|
|
1036
1038
|
.option('--branch <name>', 'Branch to scan (defaults to repo default branch)')
|
|
1037
1039
|
.option('--max-files <n>', `Upper bound on files the auditor may Read (default ${FIND_SMELLS_DEFAULT_MAX_FILES})`, (value) => {
|
|
@@ -1055,8 +1057,9 @@ program
|
|
|
1055
1057
|
// Subcommand: edsger find-architecture <productId>
|
|
1056
1058
|
// ============================================================
|
|
1057
1059
|
program
|
|
1058
|
-
.command('find-architecture
|
|
1059
|
-
.description("AI-audit a product's repository for architectural concerns (layering, coupling, cycles, missing/duplicated abstractions, responsibility creep) and file each new finding as an issue")
|
|
1060
|
+
.command('find-architecture [productId]')
|
|
1061
|
+
.description("AI-audit a product's (or a standalone repository's) repo for architectural concerns (layering, coupling, cycles, missing/duplicated abstractions, responsibility creep) and file each new finding as an issue")
|
|
1062
|
+
.option('--repo-id <id>', 'Run in repo-only mode against a single repositories row (no product context)')
|
|
1060
1063
|
.option('--full', 'Force a full scan even if previous-scan state exists')
|
|
1061
1064
|
.option('--branch <name>', 'Branch to scan (defaults to repo default branch)')
|
|
1062
1065
|
.option('--max-files <n>', `Upper bound on files the auditor may Read (default ${FIND_ARCHITECTURE_DEFAULT_MAX_FILES})`, (value) => {
|
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
* phases share the same workspace + state pattern.
|
|
10
10
|
*/
|
|
11
11
|
export interface FindArchitectureOptions {
|
|
12
|
-
|
|
12
|
+
/** Product scope. Provide this OR repoId (repo-scoped scan). */
|
|
13
|
+
productId?: string;
|
|
14
|
+
/** Repository scope: scan a bare `repositories` row with no product. */
|
|
15
|
+
repoId?: string;
|
|
13
16
|
githubToken: string;
|
|
14
17
|
owner: string;
|
|
15
18
|
repo: string;
|
|
@@ -13,7 +13,7 @@ import { DEFAULT_MODEL } from '../../constants.js';
|
|
|
13
13
|
import { logError, logInfo, logSuccess, logWarning, } from '../../utils/logger.js';
|
|
14
14
|
import { cleanupIssueRepo, cloneIssueRepo, ensureWorkspaceDir, syncRepoToRef, } from '../../workspace/workspace-manager.js';
|
|
15
15
|
import { detectDefaultBranch, gitRevParse, isAncestor, listChangedPaths, } from '../find-shared/git.js';
|
|
16
|
-
import { createIssue, fetchOpenIssues, fetchProductBasics, } from '../find-shared/mcp.js';
|
|
16
|
+
import { createIssue, fetchOpenIssues, fetchOpenIssuesByRepo, fetchProductBasics, fetchRepositoryBasics, } from '../find-shared/mcp.js';
|
|
17
17
|
import { createPromptGenerator, extractTextFromContent, tryExtractResult, } from '../pr-shared/agent-utils.js';
|
|
18
18
|
import { createFindArchitectureSystemPrompt, createFindArchitectureUserPrompt, } from './prompts.js';
|
|
19
19
|
import { acquireFindArchitectureLock, loadFindArchitectureState, updateFindArchitectureState, } from './state.js';
|
|
@@ -33,30 +33,36 @@ const MAX_TURNS = 200;
|
|
|
33
33
|
*/
|
|
34
34
|
// eslint-disable-next-line complexity
|
|
35
35
|
export async function scanForArchitecture(options) {
|
|
36
|
-
const { productId, githubToken, owner, repo, full, maxFiles, verbose } = options;
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
const { productId, repoId, githubToken, owner, repo, full, maxFiles, verbose } = options;
|
|
37
|
+
// State/lock/workspace are keyed by an opaque scope id so product and repo
|
|
38
|
+
// scans never collide; repo keys are prefixed to namespace them clearly.
|
|
39
|
+
const scopeId = productId ?? `repo-${repoId}`;
|
|
40
|
+
const scopeLabel = productId
|
|
41
|
+
? `product ${productId}`
|
|
42
|
+
: `repository ${repoId}`;
|
|
43
|
+
logInfo(`Starting architecture scan for ${scopeLabel} (${owner}/${repo})`);
|
|
44
|
+
const lock = acquireFindArchitectureLock(scopeId);
|
|
39
45
|
if (!lock) {
|
|
40
|
-
logWarning(`Another architecture scan is already in progress for
|
|
46
|
+
logWarning(`Another architecture scan is already in progress for ${scopeLabel}; skipping.`);
|
|
41
47
|
return {
|
|
42
48
|
status: 'error',
|
|
43
|
-
message: 'Another architecture scan is already in progress for this
|
|
49
|
+
message: 'Another architecture scan is already in progress for this scope',
|
|
44
50
|
};
|
|
45
51
|
}
|
|
46
52
|
let repoPath;
|
|
47
53
|
let scanSucceeded = false;
|
|
48
54
|
try {
|
|
49
|
-
updateFindArchitectureState(
|
|
55
|
+
updateFindArchitectureState(scopeId, {
|
|
50
56
|
lastAttemptedAt: new Date().toISOString(),
|
|
51
57
|
});
|
|
52
58
|
const workspaceRoot = ensureWorkspaceDir();
|
|
53
|
-
const repoKey = `${WORKSPACE_KEY}-${
|
|
59
|
+
const repoKey = `${WORKSPACE_KEY}-${scopeId}`;
|
|
54
60
|
({ repoPath } = cloneIssueRepo(workspaceRoot, repoKey, owner, repo, githubToken));
|
|
55
61
|
const branch = options.branch ?? detectDefaultBranch(repoPath);
|
|
56
62
|
logInfo(`Syncing ${owner}/${repo} to branch ${branch}`);
|
|
57
63
|
syncRepoToRef(repoPath, { branch }, githubToken);
|
|
58
64
|
const headSha = gitRevParse(repoPath, 'HEAD');
|
|
59
|
-
const state = loadFindArchitectureState(
|
|
65
|
+
const state = loadFindArchitectureState(scopeId);
|
|
60
66
|
const baseSha = full ? undefined : state.lastScannedCommitSha;
|
|
61
67
|
let scope = 'full';
|
|
62
68
|
let changedPaths;
|
|
@@ -68,7 +74,7 @@ export async function scanForArchitecture(options) {
|
|
|
68
74
|
logInfo(`Incremental scan: ${changedPaths.length} files changed since ${baseSha.slice(0, 8)}`);
|
|
69
75
|
if (changedPaths.length === 0) {
|
|
70
76
|
logSuccess('No code changes since last scan; nothing to do.');
|
|
71
|
-
updateFindArchitectureState(
|
|
77
|
+
updateFindArchitectureState(scopeId, {
|
|
72
78
|
lastScannedCommitSha: headSha,
|
|
73
79
|
lastScannedAt: new Date().toISOString(),
|
|
74
80
|
lastError: undefined,
|
|
@@ -89,7 +95,7 @@ export async function scanForArchitecture(options) {
|
|
|
89
95
|
}
|
|
90
96
|
else if (baseSha === headSha) {
|
|
91
97
|
logSuccess('HEAD unchanged since last scan; nothing to do.');
|
|
92
|
-
updateFindArchitectureState(
|
|
98
|
+
updateFindArchitectureState(scopeId, {
|
|
93
99
|
lastScannedAt: new Date().toISOString(),
|
|
94
100
|
lastError: undefined,
|
|
95
101
|
});
|
|
@@ -102,8 +108,12 @@ export async function scanForArchitecture(options) {
|
|
|
102
108
|
issuesCreated: 0,
|
|
103
109
|
};
|
|
104
110
|
}
|
|
105
|
-
const product =
|
|
106
|
-
|
|
111
|
+
const product = productId
|
|
112
|
+
? await fetchProductBasics(productId)
|
|
113
|
+
: await fetchRepositoryBasics(repoId);
|
|
114
|
+
const existingIssues = productId
|
|
115
|
+
? await fetchOpenIssues(productId)
|
|
116
|
+
: await fetchOpenIssuesByRepo(repoId);
|
|
107
117
|
logInfo(`Loaded ${existingIssues.length} existing issues for dedup context`);
|
|
108
118
|
const systemPrompt = createFindArchitectureSystemPrompt();
|
|
109
119
|
const userPrompt = createFindArchitectureUserPrompt({
|
|
@@ -157,7 +167,7 @@ export async function scanForArchitecture(options) {
|
|
|
157
167
|
}
|
|
158
168
|
if (!scanResult) {
|
|
159
169
|
const msg = 'Audit failed: could not parse a scan_result from the agent';
|
|
160
|
-
updateFindArchitectureState(
|
|
170
|
+
updateFindArchitectureState(scopeId, { lastError: msg });
|
|
161
171
|
return {
|
|
162
172
|
status: 'error',
|
|
163
173
|
message: msg,
|
|
@@ -168,18 +178,20 @@ export async function scanForArchitecture(options) {
|
|
|
168
178
|
const deferredBugs = scanResult.deferred_to_bugs ?? [];
|
|
169
179
|
const deferredFeatures = scanResult.deferred_to_features ?? [];
|
|
170
180
|
const deferredSmells = scanResult.deferred_to_smells ?? [];
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
logDeferred(
|
|
181
|
+
// CLI suggestion argument: a productId positional, or the --repo-id flag.
|
|
182
|
+
const scopeArg = productId ?? `--repo-id ${repoId}`;
|
|
183
|
+
logDeferred(deferredBugs, 'find-bugs', scopeArg);
|
|
184
|
+
logDeferred(deferredFeatures, 'find-features', scopeArg);
|
|
185
|
+
logDeferred(deferredSmells, 'find-smells', scopeArg);
|
|
174
186
|
let created = 0;
|
|
175
187
|
for (const finding of findings) {
|
|
176
|
-
const issueId = await createIssueForFinding(productId, finding);
|
|
188
|
+
const issueId = await createIssueForFinding({ productId, repoId }, finding);
|
|
177
189
|
if (issueId) {
|
|
178
190
|
created++;
|
|
179
191
|
logSuccess(`Filed issue ${issueId}: ${finding.title}`);
|
|
180
192
|
}
|
|
181
193
|
}
|
|
182
|
-
updateFindArchitectureState(
|
|
194
|
+
updateFindArchitectureState(scopeId, {
|
|
183
195
|
lastScannedCommitSha: headSha,
|
|
184
196
|
lastScannedAt: new Date().toISOString(),
|
|
185
197
|
lastError: undefined,
|
|
@@ -200,7 +212,7 @@ export async function scanForArchitecture(options) {
|
|
|
200
212
|
catch (error) {
|
|
201
213
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
202
214
|
logError(`Architecture scan failed: ${errorMessage}`);
|
|
203
|
-
updateFindArchitectureState(
|
|
215
|
+
updateFindArchitectureState(scopeId, { lastError: errorMessage });
|
|
204
216
|
return {
|
|
205
217
|
status: 'error',
|
|
206
218
|
message: `Architecture scan failed: ${errorMessage}`,
|
|
@@ -213,19 +225,20 @@ export async function scanForArchitecture(options) {
|
|
|
213
225
|
lock.release();
|
|
214
226
|
}
|
|
215
227
|
}
|
|
216
|
-
function logDeferred(deferred, siblingPhase,
|
|
228
|
+
function logDeferred(deferred, siblingPhase, scopeArg) {
|
|
217
229
|
if (deferred.length === 0) {
|
|
218
230
|
return;
|
|
219
231
|
}
|
|
220
|
-
logInfo(`${deferred.length} finding(s) deferred to ${siblingPhase} — run \`edsger ${siblingPhase} ${
|
|
232
|
+
logInfo(`${deferred.length} finding(s) deferred to ${siblingPhase} — run \`edsger ${siblingPhase} ${scopeArg}\` to pick them up:`);
|
|
221
233
|
for (const d of deferred) {
|
|
222
234
|
const loc = d.file ? ` (${d.file}${d.line ? `:${d.line}` : ''})` : '';
|
|
223
235
|
logInfo(` • ${d.title}${loc} — ${d.reason}`);
|
|
224
236
|
}
|
|
225
237
|
}
|
|
226
|
-
async function createIssueForFinding(
|
|
238
|
+
async function createIssueForFinding(scope, finding) {
|
|
227
239
|
return createIssue({
|
|
228
|
-
productId,
|
|
240
|
+
productId: scope.productId,
|
|
241
|
+
repoId: scope.repoId,
|
|
229
242
|
title: finding.title,
|
|
230
243
|
description: formatIssueDescription(finding),
|
|
231
244
|
});
|
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
* incremental, scoped to commits since the previous successful scan.
|
|
5
5
|
*/
|
|
6
6
|
export interface FindBugsOptions {
|
|
7
|
-
|
|
7
|
+
/** Product scope. Provide this OR repoId (repo-scoped scan). */
|
|
8
|
+
productId?: string;
|
|
9
|
+
/** Repository scope: scan a bare `repositories` row with no product. */
|
|
10
|
+
repoId?: string;
|
|
8
11
|
githubToken: string;
|
|
9
12
|
owner: string;
|
|
10
13
|
repo: string;
|
|
@@ -8,7 +8,7 @@ import { DEFAULT_MODEL } from '../../constants.js';
|
|
|
8
8
|
import { logError, logInfo, logSuccess, logWarning, } from '../../utils/logger.js';
|
|
9
9
|
import { cleanupIssueRepo, cloneIssueRepo, ensureWorkspaceDir, syncRepoToRef, } from '../../workspace/workspace-manager.js';
|
|
10
10
|
import { detectDefaultBranch, gitRevParse, isAncestor, listChangedPaths, } from '../find-shared/git.js';
|
|
11
|
-
import { createIssue, fetchOpenIssues, fetchProductBasics, } from '../find-shared/mcp.js';
|
|
11
|
+
import { createIssue, fetchOpenIssues, fetchOpenIssuesByRepo, fetchProductBasics, fetchRepositoryBasics, } from '../find-shared/mcp.js';
|
|
12
12
|
import { createPromptGenerator, extractTextFromContent, tryExtractResult, } from '../pr-shared/agent-utils.js';
|
|
13
13
|
import { createFindBugsSystemPrompt, createFindBugsUserPrompt, } from './prompts.js';
|
|
14
14
|
import { acquireFindBugsLock, loadFindBugsState, updateFindBugsState, } from './state.js';
|
|
@@ -32,33 +32,39 @@ const MAX_TURNS = 200;
|
|
|
32
32
|
*/
|
|
33
33
|
// eslint-disable-next-line complexity
|
|
34
34
|
export async function scanForBugs(options) {
|
|
35
|
-
const { productId, githubToken, owner, repo, full, maxFiles, verbose } = options;
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
const { productId, repoId, githubToken, owner, repo, full, maxFiles, verbose } = options;
|
|
36
|
+
// State/lock/workspace are keyed by an opaque scope id so product and repo
|
|
37
|
+
// scans never collide; repo keys are prefixed to namespace them clearly.
|
|
38
|
+
const scopeId = productId ?? `repo-${repoId}`;
|
|
39
|
+
const scopeLabel = productId
|
|
40
|
+
? `product ${productId}`
|
|
41
|
+
: `repository ${repoId}`;
|
|
42
|
+
logInfo(`Starting bug scan for ${scopeLabel} (${owner}/${repo})`);
|
|
43
|
+
const lock = acquireFindBugsLock(scopeId);
|
|
38
44
|
if (!lock) {
|
|
39
|
-
logWarning(`Another bug scan is already in progress for
|
|
45
|
+
logWarning(`Another bug scan is already in progress for ${scopeLabel}; skipping.`);
|
|
40
46
|
return {
|
|
41
47
|
status: 'error',
|
|
42
|
-
message: 'Another bug scan is already in progress for this
|
|
48
|
+
message: 'Another bug scan is already in progress for this scope',
|
|
43
49
|
};
|
|
44
50
|
}
|
|
45
51
|
let repoPath;
|
|
46
52
|
let scanSucceeded = false;
|
|
47
53
|
try {
|
|
48
|
-
updateFindBugsState(
|
|
54
|
+
updateFindBugsState(scopeId, {
|
|
49
55
|
lastAttemptedAt: new Date().toISOString(),
|
|
50
56
|
});
|
|
51
57
|
const workspaceRoot = ensureWorkspaceDir();
|
|
52
58
|
// Each run re-clones into a per-product directory and removes it on
|
|
53
59
|
// success. Incremental scope is recovered from the persisted state file
|
|
54
60
|
// (~/.edsger/find-bugs-state/<productId>.json), not from the workspace.
|
|
55
|
-
const repoKey = `${WORKSPACE_KEY}-${
|
|
61
|
+
const repoKey = `${WORKSPACE_KEY}-${scopeId}`;
|
|
56
62
|
({ repoPath } = cloneIssueRepo(workspaceRoot, repoKey, owner, repo, githubToken));
|
|
57
63
|
const branch = options.branch ?? detectDefaultBranch(repoPath);
|
|
58
64
|
logInfo(`Syncing ${owner}/${repo} to branch ${branch}`);
|
|
59
65
|
syncRepoToRef(repoPath, { branch }, githubToken);
|
|
60
66
|
const headSha = gitRevParse(repoPath, 'HEAD');
|
|
61
|
-
const state = loadFindBugsState(
|
|
67
|
+
const state = loadFindBugsState(scopeId);
|
|
62
68
|
const baseSha = full ? undefined : state.lastScannedCommitSha;
|
|
63
69
|
let scope = 'full';
|
|
64
70
|
let changedPaths;
|
|
@@ -70,7 +76,7 @@ export async function scanForBugs(options) {
|
|
|
70
76
|
logInfo(`Incremental scan: ${changedPaths.length} files changed since ${baseSha.slice(0, 8)}`);
|
|
71
77
|
if (changedPaths.length === 0) {
|
|
72
78
|
logSuccess('No code changes since last scan; nothing to do.');
|
|
73
|
-
updateFindBugsState(
|
|
79
|
+
updateFindBugsState(scopeId, {
|
|
74
80
|
lastScannedCommitSha: headSha,
|
|
75
81
|
lastScannedAt: new Date().toISOString(),
|
|
76
82
|
lastError: undefined,
|
|
@@ -100,8 +106,12 @@ export async function scanForBugs(options) {
|
|
|
100
106
|
issuesCreated: 0,
|
|
101
107
|
};
|
|
102
108
|
}
|
|
103
|
-
const product =
|
|
104
|
-
|
|
109
|
+
const product = productId
|
|
110
|
+
? await fetchProductBasics(productId)
|
|
111
|
+
: await fetchRepositoryBasics(repoId);
|
|
112
|
+
const existingIssues = productId
|
|
113
|
+
? await fetchOpenIssues(productId)
|
|
114
|
+
: await fetchOpenIssuesByRepo(repoId);
|
|
105
115
|
logInfo(`Loaded ${existingIssues.length} existing issues for dedup context`);
|
|
106
116
|
const systemPrompt = createFindBugsSystemPrompt();
|
|
107
117
|
const userPrompt = createFindBugsUserPrompt({
|
|
@@ -155,7 +165,7 @@ export async function scanForBugs(options) {
|
|
|
155
165
|
}
|
|
156
166
|
if (!scanResult) {
|
|
157
167
|
const msg = 'Audit failed: could not parse a scan_result from the agent';
|
|
158
|
-
updateFindBugsState(
|
|
168
|
+
updateFindBugsState(scopeId, { lastError: msg });
|
|
159
169
|
return {
|
|
160
170
|
status: 'error',
|
|
161
171
|
message: msg,
|
|
@@ -165,13 +175,13 @@ export async function scanForBugs(options) {
|
|
|
165
175
|
logInfo(`Audit produced ${bugs.length} candidate bugs. ${summary}`);
|
|
166
176
|
let created = 0;
|
|
167
177
|
for (const bug of bugs) {
|
|
168
|
-
const issueId = await createIssueForBug(productId, bug);
|
|
178
|
+
const issueId = await createIssueForBug({ productId, repoId }, bug);
|
|
169
179
|
if (issueId) {
|
|
170
180
|
created++;
|
|
171
181
|
logSuccess(`Filed issue ${issueId}: ${bug.title}`);
|
|
172
182
|
}
|
|
173
183
|
}
|
|
174
|
-
updateFindBugsState(
|
|
184
|
+
updateFindBugsState(scopeId, {
|
|
175
185
|
lastScannedCommitSha: headSha,
|
|
176
186
|
lastScannedAt: new Date().toISOString(),
|
|
177
187
|
lastError: undefined,
|
|
@@ -189,7 +199,7 @@ export async function scanForBugs(options) {
|
|
|
189
199
|
catch (error) {
|
|
190
200
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
191
201
|
logError(`Bug scan failed: ${errorMessage}`);
|
|
192
|
-
updateFindBugsState(
|
|
202
|
+
updateFindBugsState(scopeId, { lastError: errorMessage });
|
|
193
203
|
return {
|
|
194
204
|
status: 'error',
|
|
195
205
|
message: `Bug scan failed: ${errorMessage}`,
|
|
@@ -202,9 +212,10 @@ export async function scanForBugs(options) {
|
|
|
202
212
|
lock.release();
|
|
203
213
|
}
|
|
204
214
|
}
|
|
205
|
-
async function createIssueForBug(
|
|
215
|
+
async function createIssueForBug(scope, bug) {
|
|
206
216
|
return createIssue({
|
|
207
|
-
productId,
|
|
217
|
+
productId: scope.productId,
|
|
218
|
+
repoId: scope.repoId,
|
|
208
219
|
title: bug.title,
|
|
209
220
|
description: formatIssueDescription(bug),
|
|
210
221
|
});
|
|
@@ -12,22 +12,37 @@ export interface ProductBasics {
|
|
|
12
12
|
description?: string;
|
|
13
13
|
}
|
|
14
14
|
export declare function fetchProductBasics(productId: string): Promise<ProductBasics>;
|
|
15
|
+
/**
|
|
16
|
+
* Build prompt "product basics" from a bare repository row (repo-scoped scans
|
|
17
|
+
* have no product to read). Falls back to the repo id as the display name.
|
|
18
|
+
*/
|
|
19
|
+
export declare function fetchRepositoryBasics(repoId: string): Promise<ProductBasics>;
|
|
15
20
|
/**
|
|
16
21
|
* Fetch the product's open issues for dedup context. Filters out terminal
|
|
17
22
|
* statuses (shipped/archived/closed/...) since those can't conflict with new
|
|
18
23
|
* findings the agent might surface.
|
|
19
24
|
*/
|
|
20
25
|
export declare function fetchOpenIssues(productId: string): Promise<IssueInfo[]>;
|
|
26
|
+
/**
|
|
27
|
+
* Same as `fetchOpenIssues` but scoped to a single repository (repo-scoped
|
|
28
|
+
* scans). Used for dedup context when filing findings against a bare repo.
|
|
29
|
+
*/
|
|
30
|
+
export declare function fetchOpenIssuesByRepo(repoId: string): Promise<IssueInfo[]>;
|
|
21
31
|
export declare function isTerminalStatus(status: string): boolean;
|
|
22
32
|
export interface CreateIssueInput {
|
|
23
|
-
|
|
33
|
+
/** Product scope. Provide this OR repoId (repo-scoped scans). */
|
|
34
|
+
productId?: string;
|
|
35
|
+
/** Repository scope. Provide this OR productId. */
|
|
36
|
+
repoId?: string;
|
|
24
37
|
/** Issue title for `name`. */
|
|
25
38
|
title: string;
|
|
26
39
|
/** Already-formatted markdown body. The caller decides the layout. */
|
|
27
40
|
description: string;
|
|
28
41
|
}
|
|
29
42
|
/**
|
|
30
|
-
* File a new issue via MCP.
|
|
31
|
-
*
|
|
43
|
+
* File a new issue via MCP. The issue is scoped to a product OR a single
|
|
44
|
+
* repository — exactly one of `productId` / `repoId` should be set. Returns the
|
|
45
|
+
* new issue id, or null if MCP returned an error / unexpected shape (already
|
|
46
|
+
* logged).
|
|
32
47
|
*/
|
|
33
48
|
export declare function createIssue(input: CreateIssueInput): Promise<string | null>;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* Centralising the calls means a schema change in MCP only needs touching one
|
|
7
7
|
* place, and the per-phase orchestrators stay focused on their own logic.
|
|
8
8
|
*/
|
|
9
|
+
import { getRepositoryBasics } from '../../api/github.js';
|
|
9
10
|
import { callMcpEndpoint } from '../../api/mcp-client.js';
|
|
10
11
|
import { getSupabase, hasSupabaseSession } from '../../supabase/client.js';
|
|
11
12
|
import { logError, logWarning } from '../../utils/logger.js';
|
|
@@ -25,6 +26,17 @@ export async function fetchProductBasics(productId) {
|
|
|
25
26
|
return { name: productId };
|
|
26
27
|
}
|
|
27
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Build prompt "product basics" from a bare repository row (repo-scoped scans
|
|
31
|
+
* have no product to read). Falls back to the repo id as the display name.
|
|
32
|
+
*/
|
|
33
|
+
export async function fetchRepositoryBasics(repoId) {
|
|
34
|
+
const basics = await getRepositoryBasics(repoId).catch(() => null);
|
|
35
|
+
return {
|
|
36
|
+
name: basics?.fullName || repoId,
|
|
37
|
+
description: basics?.description ?? undefined,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
28
40
|
/**
|
|
29
41
|
* Fetch the product's open issues for dedup context. Filters out terminal
|
|
30
42
|
* statuses (shipped/archived/closed/...) since those can't conflict with new
|
|
@@ -43,6 +55,23 @@ export async function fetchOpenIssues(productId) {
|
|
|
43
55
|
return [];
|
|
44
56
|
}
|
|
45
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Same as `fetchOpenIssues` but scoped to a single repository (repo-scoped
|
|
60
|
+
* scans). Used for dedup context when filing findings against a bare repo.
|
|
61
|
+
*/
|
|
62
|
+
export async function fetchOpenIssuesByRepo(repoId) {
|
|
63
|
+
try {
|
|
64
|
+
const result = (await callMcpEndpoint('issues/list', {
|
|
65
|
+
repository_id: repoId,
|
|
66
|
+
}));
|
|
67
|
+
const all = result.issues || [];
|
|
68
|
+
return all.filter((i) => !isTerminalStatus(i.status));
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
logWarning(`Could not load existing issues for dedup: ${error instanceof Error ? error.message : String(error)}`);
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
46
75
|
export function isTerminalStatus(status) {
|
|
47
76
|
return (status === 'shipped' ||
|
|
48
77
|
status === 'archived' ||
|
|
@@ -51,17 +80,22 @@ export function isTerminalStatus(status) {
|
|
|
51
80
|
status === 'completed');
|
|
52
81
|
}
|
|
53
82
|
/**
|
|
54
|
-
* File a new issue via MCP.
|
|
55
|
-
*
|
|
83
|
+
* File a new issue via MCP. The issue is scoped to a product OR a single
|
|
84
|
+
* repository — exactly one of `productId` / `repoId` should be set. Returns the
|
|
85
|
+
* new issue id, or null if MCP returned an error / unexpected shape (already
|
|
86
|
+
* logged).
|
|
56
87
|
*/
|
|
57
88
|
export async function createIssue(input) {
|
|
89
|
+
const productId = input.productId ?? null;
|
|
90
|
+
const repositoryId = input.repoId ?? null;
|
|
58
91
|
try {
|
|
59
92
|
if (hasSupabaseSession()) {
|
|
60
93
|
try {
|
|
61
94
|
const { data, error } = await getSupabase()
|
|
62
95
|
.from('issues')
|
|
63
96
|
.insert({
|
|
64
|
-
product_id:
|
|
97
|
+
product_id: productId,
|
|
98
|
+
repository_id: repositoryId,
|
|
65
99
|
name: input.title,
|
|
66
100
|
description: input.description,
|
|
67
101
|
})
|
|
@@ -77,7 +111,8 @@ export async function createIssue(input) {
|
|
|
77
111
|
}
|
|
78
112
|
}
|
|
79
113
|
const result = (await callMcpEndpoint('issues/create', {
|
|
80
|
-
product_id:
|
|
114
|
+
...(productId ? { product_id: productId } : {}),
|
|
115
|
+
...(repositoryId ? { repository_id: repositoryId } : {}),
|
|
81
116
|
name: input.title,
|
|
82
117
|
description: input.description,
|
|
83
118
|
}));
|
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { type SmellCategory } from './types.js';
|
|
11
11
|
export interface FindSmellsOptions {
|
|
12
|
-
|
|
12
|
+
/** Product scope. Provide this OR repoId (repo-scoped scan). */
|
|
13
|
+
productId?: string;
|
|
14
|
+
/** Repository scope: scan a bare `repositories` row with no product. */
|
|
15
|
+
repoId?: string;
|
|
13
16
|
githubToken: string;
|
|
14
17
|
owner: string;
|
|
15
18
|
repo: string;
|
|
@@ -12,7 +12,7 @@ import { DEFAULT_MODEL } from '../../constants.js';
|
|
|
12
12
|
import { logError, logInfo, logSuccess, logWarning, } from '../../utils/logger.js';
|
|
13
13
|
import { cleanupIssueRepo, cloneIssueRepo, ensureWorkspaceDir, syncRepoToRef, } from '../../workspace/workspace-manager.js';
|
|
14
14
|
import { detectDefaultBranch, gitRevParse, isAncestor, listChangedPaths, } from '../find-shared/git.js';
|
|
15
|
-
import { createIssue, fetchOpenIssues, fetchProductBasics, } from '../find-shared/mcp.js';
|
|
15
|
+
import { createIssue, fetchOpenIssues, fetchOpenIssuesByRepo, fetchProductBasics, fetchRepositoryBasics, } from '../find-shared/mcp.js';
|
|
16
16
|
import { createPromptGenerator, extractTextFromContent, tryExtractResult, } from '../pr-shared/agent-utils.js';
|
|
17
17
|
import { createFindSmellsSystemPrompt, createFindSmellsUserPrompt, } from './prompts.js';
|
|
18
18
|
import { acquireFindSmellsLock, loadFindSmellsState, updateFindSmellsState, } from './state.js';
|
|
@@ -31,30 +31,36 @@ const MAX_TURNS = 200;
|
|
|
31
31
|
*/
|
|
32
32
|
// eslint-disable-next-line complexity
|
|
33
33
|
export async function scanForSmells(options) {
|
|
34
|
-
const { productId, githubToken, owner, repo, full, maxFiles, categories, verbose, } = options;
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
const { productId, repoId, githubToken, owner, repo, full, maxFiles, categories, verbose, } = options;
|
|
35
|
+
// State/lock/workspace are keyed by an opaque scope id so product and repo
|
|
36
|
+
// scans never collide; repo keys are prefixed to namespace them clearly.
|
|
37
|
+
const scopeId = productId ?? `repo-${repoId}`;
|
|
38
|
+
const scopeLabel = productId
|
|
39
|
+
? `product ${productId}`
|
|
40
|
+
: `repository ${repoId}`;
|
|
41
|
+
logInfo(`Starting smell scan for ${scopeLabel} (${owner}/${repo})`);
|
|
42
|
+
const lock = acquireFindSmellsLock(scopeId);
|
|
37
43
|
if (!lock) {
|
|
38
|
-
logWarning(`Another smell scan is already in progress for
|
|
44
|
+
logWarning(`Another smell scan is already in progress for ${scopeLabel}; skipping.`);
|
|
39
45
|
return {
|
|
40
46
|
status: 'error',
|
|
41
|
-
message: 'Another smell scan is already in progress for this
|
|
47
|
+
message: 'Another smell scan is already in progress for this scope',
|
|
42
48
|
};
|
|
43
49
|
}
|
|
44
50
|
let repoPath;
|
|
45
51
|
let scanSucceeded = false;
|
|
46
52
|
try {
|
|
47
|
-
updateFindSmellsState(
|
|
53
|
+
updateFindSmellsState(scopeId, {
|
|
48
54
|
lastAttemptedAt: new Date().toISOString(),
|
|
49
55
|
});
|
|
50
56
|
const workspaceRoot = ensureWorkspaceDir();
|
|
51
|
-
const repoKey = `${WORKSPACE_KEY}-${
|
|
57
|
+
const repoKey = `${WORKSPACE_KEY}-${scopeId}`;
|
|
52
58
|
({ repoPath } = cloneIssueRepo(workspaceRoot, repoKey, owner, repo, githubToken));
|
|
53
59
|
const branch = options.branch ?? detectDefaultBranch(repoPath);
|
|
54
60
|
logInfo(`Syncing ${owner}/${repo} to branch ${branch}`);
|
|
55
61
|
syncRepoToRef(repoPath, { branch }, githubToken);
|
|
56
62
|
const headSha = gitRevParse(repoPath, 'HEAD');
|
|
57
|
-
const state = loadFindSmellsState(
|
|
63
|
+
const state = loadFindSmellsState(scopeId);
|
|
58
64
|
const baseSha = full ? undefined : state.lastScannedCommitSha;
|
|
59
65
|
let scope = 'full';
|
|
60
66
|
let changedPaths;
|
|
@@ -66,7 +72,7 @@ export async function scanForSmells(options) {
|
|
|
66
72
|
logInfo(`Incremental scan: ${changedPaths.length} files changed since ${baseSha.slice(0, 8)}`);
|
|
67
73
|
if (changedPaths.length === 0) {
|
|
68
74
|
logSuccess('No code changes since last scan; nothing to do.');
|
|
69
|
-
updateFindSmellsState(
|
|
75
|
+
updateFindSmellsState(scopeId, {
|
|
70
76
|
lastScannedCommitSha: headSha,
|
|
71
77
|
lastScannedAt: new Date().toISOString(),
|
|
72
78
|
lastError: undefined,
|
|
@@ -90,7 +96,7 @@ export async function scanForSmells(options) {
|
|
|
90
96
|
// the sha didn't move, advance lastScannedAt + clear lastError so the
|
|
91
97
|
// state file accurately reflects when we last verified the repo.
|
|
92
98
|
logSuccess('HEAD unchanged since last scan; nothing to do.');
|
|
93
|
-
updateFindSmellsState(
|
|
99
|
+
updateFindSmellsState(scopeId, {
|
|
94
100
|
lastScannedAt: new Date().toISOString(),
|
|
95
101
|
lastError: undefined,
|
|
96
102
|
});
|
|
@@ -103,8 +109,12 @@ export async function scanForSmells(options) {
|
|
|
103
109
|
issuesCreated: 0,
|
|
104
110
|
};
|
|
105
111
|
}
|
|
106
|
-
const product =
|
|
107
|
-
|
|
112
|
+
const product = productId
|
|
113
|
+
? await fetchProductBasics(productId)
|
|
114
|
+
: await fetchRepositoryBasics(repoId);
|
|
115
|
+
const existingIssues = productId
|
|
116
|
+
? await fetchOpenIssues(productId)
|
|
117
|
+
: await fetchOpenIssuesByRepo(repoId);
|
|
108
118
|
logInfo(`Loaded ${existingIssues.length} existing issues for dedup context`);
|
|
109
119
|
const systemPrompt = createFindSmellsSystemPrompt();
|
|
110
120
|
const userPrompt = createFindSmellsUserPrompt({
|
|
@@ -159,7 +169,7 @@ export async function scanForSmells(options) {
|
|
|
159
169
|
}
|
|
160
170
|
if (!scanResult) {
|
|
161
171
|
const msg = 'Audit failed: could not parse a scan_result from the agent';
|
|
162
|
-
updateFindSmellsState(
|
|
172
|
+
updateFindSmellsState(scopeId, { lastError: msg });
|
|
163
173
|
return {
|
|
164
174
|
status: 'error',
|
|
165
175
|
message: msg,
|
|
@@ -169,15 +179,17 @@ export async function scanForSmells(options) {
|
|
|
169
179
|
logInfo(`Audit produced ${smells.length} candidate smells. ${summary}`);
|
|
170
180
|
const deferredBugs = scanResult.deferred_to_bugs ?? [];
|
|
171
181
|
const deferredFeatures = scanResult.deferred_to_features ?? [];
|
|
182
|
+
// CLI suggestion argument: a productId positional, or the --repo-id flag.
|
|
183
|
+
const scopeArg = productId ?? `--repo-id ${repoId}`;
|
|
172
184
|
if (deferredBugs.length > 0) {
|
|
173
|
-
logInfo(`${deferredBugs.length} finding(s) deferred to find-bugs — run \`edsger find-bugs ${
|
|
185
|
+
logInfo(`${deferredBugs.length} finding(s) deferred to find-bugs — run \`edsger find-bugs ${scopeArg}\` to pick them up:`);
|
|
174
186
|
for (const d of deferredBugs) {
|
|
175
187
|
const loc = d.file ? ` (${d.file}${d.line ? `:${d.line}` : ''})` : '';
|
|
176
188
|
logInfo(` • ${d.title}${loc} — ${d.reason}`);
|
|
177
189
|
}
|
|
178
190
|
}
|
|
179
191
|
if (deferredFeatures.length > 0) {
|
|
180
|
-
logInfo(`${deferredFeatures.length} finding(s) deferred to find-features — run \`edsger find-features ${
|
|
192
|
+
logInfo(`${deferredFeatures.length} finding(s) deferred to find-features — run \`edsger find-features ${scopeArg}\` to pick them up:`);
|
|
181
193
|
for (const d of deferredFeatures) {
|
|
182
194
|
const loc = d.file ? ` (${d.file}${d.line ? `:${d.line}` : ''})` : '';
|
|
183
195
|
logInfo(` • ${d.title}${loc} — ${d.reason}`);
|
|
@@ -192,13 +204,13 @@ export async function scanForSmells(options) {
|
|
|
192
204
|
}
|
|
193
205
|
let created = 0;
|
|
194
206
|
for (const smell of filteredSmells) {
|
|
195
|
-
const issueId = await createIssueForSmell(productId, smell);
|
|
207
|
+
const issueId = await createIssueForSmell({ productId, repoId }, smell);
|
|
196
208
|
if (issueId) {
|
|
197
209
|
created++;
|
|
198
210
|
logSuccess(`Filed issue ${issueId}: ${smell.title}`);
|
|
199
211
|
}
|
|
200
212
|
}
|
|
201
|
-
updateFindSmellsState(
|
|
213
|
+
updateFindSmellsState(scopeId, {
|
|
202
214
|
lastScannedCommitSha: headSha,
|
|
203
215
|
lastScannedAt: new Date().toISOString(),
|
|
204
216
|
lastError: undefined,
|
|
@@ -219,7 +231,7 @@ export async function scanForSmells(options) {
|
|
|
219
231
|
catch (error) {
|
|
220
232
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
221
233
|
logError(`Smell scan failed: ${errorMessage}`);
|
|
222
|
-
updateFindSmellsState(
|
|
234
|
+
updateFindSmellsState(scopeId, { lastError: errorMessage });
|
|
223
235
|
return {
|
|
224
236
|
status: 'error',
|
|
225
237
|
message: `Smell scan failed: ${errorMessage}`,
|
|
@@ -232,9 +244,10 @@ export async function scanForSmells(options) {
|
|
|
232
244
|
lock.release();
|
|
233
245
|
}
|
|
234
246
|
}
|
|
235
|
-
async function createIssueForSmell(
|
|
247
|
+
async function createIssueForSmell(scope, smell) {
|
|
236
248
|
return createIssue({
|
|
237
|
-
productId,
|
|
249
|
+
productId: scope.productId,
|
|
250
|
+
repoId: scope.repoId,
|
|
238
251
|
title: smell.title,
|
|
239
252
|
description: formatIssueDescription(smell),
|
|
240
253
|
});
|