edsger 0.75.1 → 0.77.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 (52) hide show
  1. package/dist/commands/api-docs/index.d.ts +18 -0
  2. package/dist/commands/api-docs/index.js +41 -0
  3. package/dist/commands/find-architecture/index.d.ts +3 -1
  4. package/dist/commands/find-architecture/index.js +14 -5
  5. package/dist/commands/find-bugs/index.d.ts +3 -1
  6. package/dist/commands/find-bugs/index.js +14 -5
  7. package/dist/commands/find-smells/index.d.ts +3 -1
  8. package/dist/commands/find-smells/index.js +14 -5
  9. package/dist/commands/quality-benchmark/index.d.ts +5 -0
  10. package/dist/commands/quality-benchmark/index.js +28 -0
  11. package/dist/index.js +38 -6
  12. package/dist/phases/api-docs/index.d.ts +47 -0
  13. package/dist/phases/api-docs/index.js +254 -0
  14. package/dist/phases/api-docs/mcp-server.d.ts +25 -0
  15. package/dist/phases/api-docs/mcp-server.js +82 -0
  16. package/dist/phases/api-docs/prompts.d.ts +16 -0
  17. package/dist/phases/api-docs/prompts.js +65 -0
  18. package/dist/phases/api-docs/types.d.ts +22 -0
  19. package/dist/phases/api-docs/types.js +10 -0
  20. package/dist/phases/find-architecture/index.d.ts +4 -1
  21. package/dist/phases/find-architecture/index.js +46 -26
  22. package/dist/phases/find-architecture/prompts.d.ts +2 -1
  23. package/dist/phases/find-architecture/prompts.js +3 -2
  24. package/dist/phases/find-bugs/index.d.ts +4 -1
  25. package/dist/phases/find-bugs/index.js +32 -19
  26. package/dist/phases/find-shared/baseline.d.ts +45 -0
  27. package/dist/phases/find-shared/baseline.js +56 -0
  28. package/dist/phases/find-shared/custom-rules.d.ts +39 -0
  29. package/dist/phases/find-shared/custom-rules.js +75 -0
  30. package/dist/phases/find-shared/detect-context.d.ts +40 -0
  31. package/dist/phases/find-shared/detect-context.js +247 -0
  32. package/dist/phases/find-shared/mcp.d.ts +24 -3
  33. package/dist/phases/find-shared/mcp.js +41 -4
  34. package/dist/phases/find-shared/rule-config.d.ts +37 -0
  35. package/dist/phases/find-shared/rule-config.js +67 -0
  36. package/dist/phases/find-shared/rule-packs.d.ts +65 -0
  37. package/dist/phases/find-shared/rule-packs.js +124 -0
  38. package/dist/phases/find-shared/scoped-read.d.ts +12 -0
  39. package/dist/phases/find-shared/scoped-read.js +33 -0
  40. package/dist/phases/find-smells/index.d.ts +4 -1
  41. package/dist/phases/find-smells/index.js +43 -23
  42. package/dist/phases/find-smells/prompts.d.ts +2 -1
  43. package/dist/phases/find-smells/prompts.js +4 -3
  44. package/dist/phases/quality-benchmark/gate.d.ts +50 -0
  45. package/dist/phases/quality-benchmark/gate.js +91 -0
  46. package/dist/phases/quality-benchmark/index.js +15 -1
  47. package/dist/phases/quality-benchmark/parsers.d.ts +23 -0
  48. package/dist/phases/quality-benchmark/parsers.js +210 -0
  49. package/dist/phases/quality-benchmark/rubric.md +37 -0
  50. package/dist/phases/quality-benchmark/tool-catalog.js +58 -1
  51. package/dist/phases/quality-benchmark/types.d.ts +8 -1
  52. package/package.json +1 -1
@@ -0,0 +1,25 @@
1
+ /**
2
+ * In-process MCP server exposing a single api-docs tool:
3
+ *
4
+ * - submit_api_docs capture the generated API reference (Markdown + an
5
+ * optional OpenAPI document + summary + endpoint count)
6
+ *
7
+ * Unlike the recipes toolkit (which commits each mutation as it goes) the
8
+ * api-docs artifact is a single document, so the tool just captures into an
9
+ * in-memory buffer. The orchestrator persists the captured artifact and flips
10
+ * the run row to success once the agent's turn ends — a clean all-or-nothing
11
+ * write that can't leave a half-written reference behind.
12
+ */
13
+ import { z } from 'zod';
14
+ import { type ApiDocsArtifact } from './types.js';
15
+ export interface ApiDocsCaptureState {
16
+ captured: ApiDocsArtifact | null;
17
+ }
18
+ export declare function createApiDocsCaptureState(): ApiDocsCaptureState;
19
+ export declare function createSubmitApiDocsTool(capture: ApiDocsCaptureState): import("@anthropic-ai/claude-agent-sdk").SdkMcpToolDefinition<{
20
+ markdown: z.ZodString;
21
+ summary: z.ZodString;
22
+ endpoint_count: z.ZodNumber;
23
+ openapi_json: z.ZodOptional<z.ZodString>;
24
+ }>;
25
+ export declare function createApiDocsMcpServer(capture: ApiDocsCaptureState): import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * In-process MCP server exposing a single api-docs tool:
3
+ *
4
+ * - submit_api_docs capture the generated API reference (Markdown + an
5
+ * optional OpenAPI document + summary + endpoint count)
6
+ *
7
+ * Unlike the recipes toolkit (which commits each mutation as it goes) the
8
+ * api-docs artifact is a single document, so the tool just captures into an
9
+ * in-memory buffer. The orchestrator persists the captured artifact and flips
10
+ * the run row to success once the agent's turn ends — a clean all-or-nothing
11
+ * write that can't leave a half-written reference behind.
12
+ */
13
+ import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk';
14
+ import { z } from 'zod';
15
+ import { API_DOCS_MARKDOWN_MAX, API_DOCS_SUMMARY_MAX, } from './types.js';
16
+ export function createApiDocsCaptureState() {
17
+ return { captured: null };
18
+ }
19
+ function textError(message) {
20
+ return {
21
+ content: [{ type: 'text', text: message }],
22
+ isError: true,
23
+ };
24
+ }
25
+ function textOk(message) {
26
+ return {
27
+ content: [{ type: 'text', text: message }],
28
+ };
29
+ }
30
+ export function createSubmitApiDocsTool(capture) {
31
+ return tool('submit_api_docs', 'Submit the final API reference for this repository. Call this exactly once, at the end, after you have explored the codebase. Provide a Markdown reference (always) and, when the repo exposes an HTTP API, a complete OpenAPI 3.x document as a JSON string.', {
32
+ markdown: z
33
+ .string()
34
+ .min(1)
35
+ .max(API_DOCS_MARKDOWN_MAX)
36
+ .describe('The human-readable API reference in Markdown. Document each endpoint / exported interface: method + path (or signature), purpose, parameters, request/response shapes, auth, and an example where useful.'),
37
+ summary: z
38
+ .string()
39
+ .min(1)
40
+ .max(API_DOCS_SUMMARY_MAX)
41
+ .describe('One-paragraph overview of the API surface (what it exposes and how it is consumed).'),
42
+ endpoint_count: z
43
+ .number()
44
+ .int()
45
+ .min(0)
46
+ .describe('How many distinct endpoints / operations the reference documents (0 if the repo exposes no callable API surface).'),
47
+ openapi_json: z
48
+ .string()
49
+ .optional()
50
+ .describe('A complete OpenAPI 3.x document serialized as a JSON string. Omit when the repository exposes no HTTP API (e.g. a library or CLI).'),
51
+ }, async (args) => {
52
+ let openapi = null;
53
+ if (args.openapi_json && args.openapi_json.trim()) {
54
+ try {
55
+ const parsed = JSON.parse(args.openapi_json);
56
+ if (typeof parsed !== 'object' ||
57
+ parsed === null ||
58
+ Array.isArray(parsed)) {
59
+ return textError('openapi_json must be a JSON object (an OpenAPI document), not an array or scalar.');
60
+ }
61
+ openapi = parsed;
62
+ }
63
+ catch (error) {
64
+ return textError(`openapi_json is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
65
+ }
66
+ }
67
+ capture.captured = {
68
+ openapi,
69
+ markdown: args.markdown,
70
+ summary: args.summary.trim(),
71
+ endpointCount: args.endpoint_count,
72
+ };
73
+ return textOk(`API docs captured (${args.endpoint_count} endpoint${args.endpoint_count === 1 ? '' : 's'}${openapi ? ', with OpenAPI spec' : ''}). You can end your turn now.`);
74
+ });
75
+ }
76
+ export function createApiDocsMcpServer(capture) {
77
+ return createSdkMcpServer({
78
+ name: 'api-docs',
79
+ version: '1.0.0',
80
+ tools: [createSubmitApiDocsTool(capture)],
81
+ });
82
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Prompts for the api-docs phase.
3
+ *
4
+ * Agent's job: explore the cloned repository, work out what API surface it
5
+ * exposes (HTTP/REST routes, RPC handlers, edge functions, exported SDK
6
+ * functions, CLI commands — whatever the repo actually offers), and produce
7
+ * an API reference. Output is submitted via the single `submit_api_docs` MCP
8
+ * tool — no fenced JSON, no chat summary.
9
+ */
10
+ export interface ApiDocsPromptContext {
11
+ repoName: string;
12
+ repoDescription?: string;
13
+ guidance?: string;
14
+ }
15
+ export declare function createApiDocsSystemPrompt(): string;
16
+ export declare function createApiDocsUserPrompt(ctx: ApiDocsPromptContext): string;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Prompts for the api-docs phase.
3
+ *
4
+ * Agent's job: explore the cloned repository, work out what API surface it
5
+ * exposes (HTTP/REST routes, RPC handlers, edge functions, exported SDK
6
+ * functions, CLI commands — whatever the repo actually offers), and produce
7
+ * an API reference. Output is submitted via the single `submit_api_docs` MCP
8
+ * tool — no fenced JSON, no chat summary.
9
+ */
10
+ export function createApiDocsSystemPrompt() {
11
+ return `You are a senior engineer writing the API reference for a codebase.
12
+
13
+ The current working directory is a fresh clone of the repository. Use Glob/Grep/Read (and Bash for git/log when helpful) to explore it before writing anything.
14
+
15
+ ## Step 1 — Determine the API surface
16
+
17
+ Repositories expose their API in different ways. Figure out which apply here (a repo may have more than one):
18
+ - **HTTP / REST** — route definitions, controllers, request handlers (Express/Fastify/Next.js route handlers, Flask/FastAPI/Django, Spring, Go net/http, etc.).
19
+ - **Action-routed / RPC functions** — e.g. serverless / edge functions dispatched by an \`action\` field on the body, gRPC services, tRPC routers.
20
+ - **Library / SDK** — the public, exported functions/classes other code imports. Document the exported surface, not internal helpers.
21
+ - **CLI** — the commands and flags the tool exposes.
22
+
23
+ Do NOT assume it is REST. Read the code and document what is actually there.
24
+
25
+ ## Step 2 — Produce two artifacts
26
+
27
+ 1. **Markdown reference (always).** A clear, navigable reference. For each endpoint / operation / exported function document: its identifier (method + path, or signature), purpose, parameters (name, type, required, description), request body and response shapes, auth/permissions, and a short example where it helps. Group by resource/module. Note relative file paths so readers can jump to the source.
28
+
29
+ 2. **OpenAPI 3.x document (only when the repo exposes an HTTP API).** A complete, spec-valid OpenAPI document covering the HTTP endpoints — paths, methods, parameters, requestBody/response schemas under \`components.schemas\`, and security schemes. Omit this entirely for pure libraries or CLIs (there is nothing meaningful to put in an OpenAPI \`paths\` object).
30
+
31
+ ## Output protocol
32
+
33
+ Submit your result with the \`submit_api_docs\` MCP tool — call it EXACTLY ONCE at the end:
34
+
35
+ - \`markdown\` — the full Markdown reference.
36
+ - \`summary\` — one paragraph describing the API surface.
37
+ - \`endpoint_count\` — the number of distinct endpoints/operations you documented (0 for a non-callable surface).
38
+ - \`openapi_json\` — the OpenAPI document as a JSON string, or omit it when there is no HTTP API.
39
+
40
+ Do NOT paste the reference into chat. Do NOT wrap it in fenced code blocks in a message. Explore, then call the tool, then end your turn.
41
+
42
+ ## Rules
43
+
44
+ - Ground everything in the actual source. Don't invent endpoints, parameters, or response fields. If something is ambiguous, describe what the code does and say so.
45
+ - Keep the Markdown focused on the externally-callable surface. Don't document private/internal helpers as if they were API.
46
+ - Prefer accuracy over completeness padding: a small repo gets a short, correct reference.`;
47
+ }
48
+ export function createApiDocsUserPrompt(ctx) {
49
+ const lines = [];
50
+ lines.push(`# Repository: ${ctx.repoName}`);
51
+ if (ctx.repoDescription) {
52
+ lines.push('');
53
+ lines.push('## Description');
54
+ lines.push(ctx.repoDescription);
55
+ }
56
+ if (ctx.guidance && ctx.guidance.trim()) {
57
+ lines.push('');
58
+ lines.push('## Reviewer guidance (focus or exclusions)');
59
+ lines.push(ctx.guidance.trim());
60
+ }
61
+ lines.push('');
62
+ lines.push('## Task');
63
+ lines.push('Explore the cloned repository, determine what API surface it exposes, and write its API reference. Then call `submit_api_docs` exactly once with the Markdown reference, a one-paragraph summary, the endpoint count, and (only if the repo exposes an HTTP API) the OpenAPI document as a JSON string. End your turn after submitting.');
64
+ return lines.join('\n');
65
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Shapes the api-docs MCP tool accepts. The Zod schema lives in mcp-server.ts;
3
+ * these plain TS types are what the orchestrator / persistence helpers consume.
4
+ *
5
+ * Defensive caps mirror the DB CHECK constraints from
6
+ * 20260605000000_create_api_doc_runs.sql so the MCP tool can reject oversized
7
+ * output with an actionable message instead of a Postgres constraint violation.
8
+ */
9
+ /** A spec-compliant OpenAPI document. Kept loose — the agent owns the shape. */
10
+ export type OpenApiSpec = Record<string, unknown>;
11
+ export interface ApiDocsArtifact {
12
+ /** Parsed OpenAPI document, or null when the repo exposes no HTTP API. */
13
+ openapi: OpenApiSpec | null;
14
+ /** Human-readable Markdown API reference (always produced). */
15
+ markdown: string;
16
+ /** One-paragraph overview for the tab header. */
17
+ summary: string;
18
+ /** Number of documented endpoints/operations (0 for non-HTTP surfaces). */
19
+ endpointCount: number;
20
+ }
21
+ export declare const API_DOCS_MARKDOWN_MAX = 200000;
22
+ export declare const API_DOCS_SUMMARY_MAX = 1000;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Shapes the api-docs MCP tool accepts. The Zod schema lives in mcp-server.ts;
3
+ * these plain TS types are what the orchestrator / persistence helpers consume.
4
+ *
5
+ * Defensive caps mirror the DB CHECK constraints from
6
+ * 20260605000000_create_api_doc_runs.sql so the MCP tool can reject oversized
7
+ * output with an actionable message instead of a Postgres constraint violation.
8
+ */
9
+ export const API_DOCS_MARKDOWN_MAX = 200_000;
10
+ export const API_DOCS_SUMMARY_MAX = 1000;
@@ -9,7 +9,10 @@
9
9
  * phases share the same workspace + state pattern.
10
10
  */
11
11
  export interface FindArchitectureOptions {
12
- productId: string;
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,8 +12,11 @@ import { query } from '@anthropic-ai/claude-agent-sdk';
12
12
  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
+ import { resolveScanBase } from '../find-shared/baseline.js';
16
+ import { resolveCustomRules } from '../find-shared/custom-rules.js';
15
17
  import { detectDefaultBranch, gitRevParse, isAncestor, listChangedPaths, } from '../find-shared/git.js';
16
- import { createIssue, fetchOpenIssues, fetchProductBasics, } from '../find-shared/mcp.js';
18
+ import { createIssue, fetchOpenIssues, fetchOpenIssuesByRepo, fetchProductBasics, fetchRepositoryBasics, } from '../find-shared/mcp.js';
19
+ import { resolveStackRulePacks } from '../find-shared/rule-config.js';
17
20
  import { createPromptGenerator, extractTextFromContent, tryExtractResult, } from '../pr-shared/agent-utils.js';
18
21
  import { createFindArchitectureSystemPrompt, createFindArchitectureUserPrompt, } from './prompts.js';
19
22
  import { acquireFindArchitectureLock, loadFindArchitectureState, updateFindArchitectureState, } from './state.js';
@@ -33,31 +36,35 @@ const MAX_TURNS = 200;
33
36
  */
34
37
  // eslint-disable-next-line complexity
35
38
  export async function scanForArchitecture(options) {
36
- const { productId, githubToken, owner, repo, full, maxFiles, verbose } = options;
37
- logInfo(`Starting architecture scan for product ${productId} (${owner}/${repo})`);
38
- const lock = acquireFindArchitectureLock(productId);
39
+ const { productId, repoId, githubToken, owner, repo, full, maxFiles, verbose, } = options;
40
+ // State/lock/workspace are keyed by an opaque scope id so product and repo
41
+ // scans never collide; repo keys are prefixed to namespace them clearly.
42
+ const scopeId = productId ?? `repo-${repoId}`;
43
+ const scopeLabel = productId ? `product ${productId}` : `repository ${repoId}`;
44
+ logInfo(`Starting architecture scan for ${scopeLabel} (${owner}/${repo})`);
45
+ const lock = acquireFindArchitectureLock(scopeId);
39
46
  if (!lock) {
40
- logWarning(`Another architecture scan is already in progress for product ${productId}; skipping.`);
47
+ logWarning(`Another architecture scan is already in progress for ${scopeLabel}; skipping.`);
41
48
  return {
42
49
  status: 'error',
43
- message: 'Another architecture scan is already in progress for this product',
50
+ message: 'Another architecture scan is already in progress for this scope',
44
51
  };
45
52
  }
46
53
  let repoPath;
47
54
  let scanSucceeded = false;
48
55
  try {
49
- updateFindArchitectureState(productId, {
56
+ updateFindArchitectureState(scopeId, {
50
57
  lastAttemptedAt: new Date().toISOString(),
51
58
  });
52
59
  const workspaceRoot = ensureWorkspaceDir();
53
- const repoKey = `${WORKSPACE_KEY}-${productId}`;
60
+ const repoKey = `${WORKSPACE_KEY}-${scopeId}`;
54
61
  ({ repoPath } = cloneIssueRepo(workspaceRoot, repoKey, owner, repo, githubToken));
55
62
  const branch = options.branch ?? detectDefaultBranch(repoPath);
56
63
  logInfo(`Syncing ${owner}/${repo} to branch ${branch}`);
57
64
  syncRepoToRef(repoPath, { branch }, githubToken);
58
65
  const headSha = gitRevParse(repoPath, 'HEAD');
59
- const state = loadFindArchitectureState(productId);
60
- const baseSha = full ? undefined : state.lastScannedCommitSha;
66
+ const state = loadFindArchitectureState(scopeId);
67
+ const baseSha = await resolveScanBase({ productId, repoId }, { full, lastScannedSha: state.lastScannedCommitSha });
61
68
  let scope = 'full';
62
69
  let changedPaths;
63
70
  if (baseSha && baseSha !== headSha) {
@@ -68,7 +75,7 @@ export async function scanForArchitecture(options) {
68
75
  logInfo(`Incremental scan: ${changedPaths.length} files changed since ${baseSha.slice(0, 8)}`);
69
76
  if (changedPaths.length === 0) {
70
77
  logSuccess('No code changes since last scan; nothing to do.');
71
- updateFindArchitectureState(productId, {
78
+ updateFindArchitectureState(scopeId, {
72
79
  lastScannedCommitSha: headSha,
73
80
  lastScannedAt: new Date().toISOString(),
74
81
  lastError: undefined,
@@ -89,7 +96,7 @@ export async function scanForArchitecture(options) {
89
96
  }
90
97
  else if (baseSha === headSha) {
91
98
  logSuccess('HEAD unchanged since last scan; nothing to do.');
92
- updateFindArchitectureState(productId, {
99
+ updateFindArchitectureState(scopeId, {
93
100
  lastScannedAt: new Date().toISOString(),
94
101
  lastError: undefined,
95
102
  });
@@ -102,10 +109,19 @@ export async function scanForArchitecture(options) {
102
109
  issuesCreated: 0,
103
110
  };
104
111
  }
105
- const product = await fetchProductBasics(productId);
106
- const existingIssues = await fetchOpenIssues(productId);
112
+ const product = productId
113
+ ? await fetchProductBasics(productId)
114
+ : await fetchRepositoryBasics(repoId);
115
+ const existingIssues = productId
116
+ ? await fetchOpenIssues(productId)
117
+ : await fetchOpenIssuesByRepo(repoId);
107
118
  logInfo(`Loaded ${existingIssues.length} existing issues for dedup context`);
108
- const systemPrompt = createFindArchitectureSystemPrompt();
119
+ const rulePacks = await resolveStackRulePacks(repoPath, {
120
+ productId,
121
+ repoId,
122
+ });
123
+ const customRules = await resolveCustomRules({ productId, repoId }, 'architecture');
124
+ const systemPrompt = createFindArchitectureSystemPrompt(rulePacks, customRules);
109
125
  const userPrompt = createFindArchitectureUserPrompt({
110
126
  productName: product.name,
111
127
  productDescription: product.description,
@@ -157,7 +173,7 @@ export async function scanForArchitecture(options) {
157
173
  }
158
174
  if (!scanResult) {
159
175
  const msg = 'Audit failed: could not parse a scan_result from the agent';
160
- updateFindArchitectureState(productId, { lastError: msg });
176
+ updateFindArchitectureState(scopeId, { lastError: msg });
161
177
  return {
162
178
  status: 'error',
163
179
  message: msg,
@@ -168,18 +184,20 @@ export async function scanForArchitecture(options) {
168
184
  const deferredBugs = scanResult.deferred_to_bugs ?? [];
169
185
  const deferredFeatures = scanResult.deferred_to_features ?? [];
170
186
  const deferredSmells = scanResult.deferred_to_smells ?? [];
171
- logDeferred(deferredBugs, 'find-bugs', productId);
172
- logDeferred(deferredFeatures, 'find-features', productId);
173
- logDeferred(deferredSmells, 'find-smells', productId);
187
+ // CLI suggestion argument: a productId positional, or the --repo-id flag.
188
+ const scopeArg = productId ?? `--repo-id ${repoId}`;
189
+ logDeferred(deferredBugs, 'find-bugs', scopeArg);
190
+ logDeferred(deferredFeatures, 'find-features', scopeArg);
191
+ logDeferred(deferredSmells, 'find-smells', scopeArg);
174
192
  let created = 0;
175
193
  for (const finding of findings) {
176
- const issueId = await createIssueForFinding(productId, finding);
194
+ const issueId = await createIssueForFinding({ productId, repoId }, finding);
177
195
  if (issueId) {
178
196
  created++;
179
197
  logSuccess(`Filed issue ${issueId}: ${finding.title}`);
180
198
  }
181
199
  }
182
- updateFindArchitectureState(productId, {
200
+ updateFindArchitectureState(scopeId, {
183
201
  lastScannedCommitSha: headSha,
184
202
  lastScannedAt: new Date().toISOString(),
185
203
  lastError: undefined,
@@ -200,7 +218,7 @@ export async function scanForArchitecture(options) {
200
218
  catch (error) {
201
219
  const errorMessage = error instanceof Error ? error.message : String(error);
202
220
  logError(`Architecture scan failed: ${errorMessage}`);
203
- updateFindArchitectureState(productId, { lastError: errorMessage });
221
+ updateFindArchitectureState(scopeId, { lastError: errorMessage });
204
222
  return {
205
223
  status: 'error',
206
224
  message: `Architecture scan failed: ${errorMessage}`,
@@ -213,21 +231,23 @@ export async function scanForArchitecture(options) {
213
231
  lock.release();
214
232
  }
215
233
  }
216
- function logDeferred(deferred, siblingPhase, productId) {
234
+ function logDeferred(deferred, siblingPhase, scopeArg) {
217
235
  if (deferred.length === 0) {
218
236
  return;
219
237
  }
220
- logInfo(`${deferred.length} finding(s) deferred to ${siblingPhase} — run \`edsger ${siblingPhase} ${productId}\` to pick them up:`);
238
+ logInfo(`${deferred.length} finding(s) deferred to ${siblingPhase} — run \`edsger ${siblingPhase} ${scopeArg}\` to pick them up:`);
221
239
  for (const d of deferred) {
222
240
  const loc = d.file ? ` (${d.file}${d.line ? `:${d.line}` : ''})` : '';
223
241
  logInfo(` • ${d.title}${loc} — ${d.reason}`);
224
242
  }
225
243
  }
226
- async function createIssueForFinding(productId, finding) {
244
+ async function createIssueForFinding(scope, finding) {
227
245
  return createIssue({
228
- productId,
246
+ productId: scope.productId,
247
+ repoId: scope.repoId,
229
248
  title: finding.title,
230
249
  description: formatIssueDescription(finding),
250
+ source: 'quality',
231
251
  });
232
252
  }
233
253
  function formatIssueDescription(finding) {
@@ -12,7 +12,8 @@
12
12
  * multiple files: module boundaries, layering, coupling, cycles, missing
13
13
  * or duplicated abstractions, responsibility creep, cross-cutting concerns.
14
14
  */
15
- export declare function createFindArchitectureSystemPrompt(): string;
15
+ import { type RulePack } from '../find-shared/rule-packs.js';
16
+ export declare function createFindArchitectureSystemPrompt(packs?: RulePack[], customRules?: string): string;
16
17
  export interface FindArchitectureUserPromptParams {
17
18
  productName: string;
18
19
  productDescription?: string;
@@ -12,7 +12,8 @@
12
12
  * multiple files: module boundaries, layering, coupling, cycles, missing
13
13
  * or duplicated abstractions, responsibility creep, cross-cutting concerns.
14
14
  */
15
- export function createFindArchitectureSystemPrompt() {
15
+ import { renderRulePacks } from '../find-shared/rule-packs.js';
16
+ export function createFindArchitectureSystemPrompt(packs = [], customRules = '') {
16
17
  return `You are a senior staff engineer auditing a codebase for **architectural problems and improvement opportunities**. You have read-only access via Read/Grep/Glob and may run shallow Bash queries (e.g. \`git log\`, \`wc -l\`, \`grep\`) to navigate.
17
18
 
18
19
  **What counts as an architectural finding worth filing**:
@@ -26,7 +27,7 @@ export function createFindArchitectureSystemPrompt() {
26
27
  8. **Responsibility creep**: a service / component that started focused and now does five things
27
28
  9. **Cross-cutting concerns**: logging, auth, retries, error handling implemented inconsistently across modules
28
29
  10. **Inconsistency**: same problem solved different ways in different parts of the codebase, drifting conventions
29
- 11. **Scalability shape**: data-flow / control-flow patterns that won't survive the next obvious axis of growth
30
+ 11. **Scalability shape**: data-flow / control-flow patterns that won't survive the next obvious axis of growth${renderRulePacks(packs, 'architecture')}${customRules}
30
31
 
31
32
  **What does NOT count** (skip these — wrong tool):
32
33
  - Real bugs (security holes, logic errors, races, data corruption) — those belong in \`edsger find-bugs\`. **Don't drop them silently — list them in \`deferred_to_bugs\`.**
@@ -4,7 +4,10 @@
4
4
  * incremental, scoped to commits since the previous successful scan.
5
5
  */
6
6
  export interface FindBugsOptions {
7
- productId: string;
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;
@@ -7,8 +7,9 @@ import { query } from '@anthropic-ai/claude-agent-sdk';
7
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
+ import { resolveScanBase } from '../find-shared/baseline.js';
10
11
  import { detectDefaultBranch, gitRevParse, isAncestor, listChangedPaths, } from '../find-shared/git.js';
11
- import { createIssue, fetchOpenIssues, fetchProductBasics, } from '../find-shared/mcp.js';
12
+ import { createIssue, fetchOpenIssues, fetchOpenIssuesByRepo, fetchProductBasics, fetchRepositoryBasics, } from '../find-shared/mcp.js';
12
13
  import { createPromptGenerator, extractTextFromContent, tryExtractResult, } from '../pr-shared/agent-utils.js';
13
14
  import { createFindBugsSystemPrompt, createFindBugsUserPrompt, } from './prompts.js';
14
15
  import { acquireFindBugsLock, loadFindBugsState, updateFindBugsState, } from './state.js';
@@ -32,34 +33,40 @@ const MAX_TURNS = 200;
32
33
  */
33
34
  // eslint-disable-next-line complexity
34
35
  export async function scanForBugs(options) {
35
- const { productId, githubToken, owner, repo, full, maxFiles, verbose } = options;
36
- logInfo(`Starting bug scan for product ${productId} (${owner}/${repo})`);
37
- const lock = acquireFindBugsLock(productId);
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 bug scan for ${scopeLabel} (${owner}/${repo})`);
44
+ const lock = acquireFindBugsLock(scopeId);
38
45
  if (!lock) {
39
- logWarning(`Another bug scan is already in progress for product ${productId}; skipping.`);
46
+ logWarning(`Another bug scan is already in progress for ${scopeLabel}; skipping.`);
40
47
  return {
41
48
  status: 'error',
42
- message: 'Another bug scan is already in progress for this product',
49
+ message: 'Another bug scan is already in progress for this scope',
43
50
  };
44
51
  }
45
52
  let repoPath;
46
53
  let scanSucceeded = false;
47
54
  try {
48
- updateFindBugsState(productId, {
55
+ updateFindBugsState(scopeId, {
49
56
  lastAttemptedAt: new Date().toISOString(),
50
57
  });
51
58
  const workspaceRoot = ensureWorkspaceDir();
52
59
  // Each run re-clones into a per-product directory and removes it on
53
60
  // success. Incremental scope is recovered from the persisted state file
54
61
  // (~/.edsger/find-bugs-state/<productId>.json), not from the workspace.
55
- const repoKey = `${WORKSPACE_KEY}-${productId}`;
62
+ const repoKey = `${WORKSPACE_KEY}-${scopeId}`;
56
63
  ({ repoPath } = cloneIssueRepo(workspaceRoot, repoKey, owner, repo, githubToken));
57
64
  const branch = options.branch ?? detectDefaultBranch(repoPath);
58
65
  logInfo(`Syncing ${owner}/${repo} to branch ${branch}`);
59
66
  syncRepoToRef(repoPath, { branch }, githubToken);
60
67
  const headSha = gitRevParse(repoPath, 'HEAD');
61
- const state = loadFindBugsState(productId);
62
- const baseSha = full ? undefined : state.lastScannedCommitSha;
68
+ const state = loadFindBugsState(scopeId);
69
+ const baseSha = await resolveScanBase({ productId, repoId }, { full, lastScannedSha: state.lastScannedCommitSha });
63
70
  let scope = 'full';
64
71
  let changedPaths;
65
72
  if (baseSha && baseSha !== headSha) {
@@ -70,7 +77,7 @@ export async function scanForBugs(options) {
70
77
  logInfo(`Incremental scan: ${changedPaths.length} files changed since ${baseSha.slice(0, 8)}`);
71
78
  if (changedPaths.length === 0) {
72
79
  logSuccess('No code changes since last scan; nothing to do.');
73
- updateFindBugsState(productId, {
80
+ updateFindBugsState(scopeId, {
74
81
  lastScannedCommitSha: headSha,
75
82
  lastScannedAt: new Date().toISOString(),
76
83
  lastError: undefined,
@@ -100,8 +107,12 @@ export async function scanForBugs(options) {
100
107
  issuesCreated: 0,
101
108
  };
102
109
  }
103
- const product = await fetchProductBasics(productId);
104
- const existingIssues = await fetchOpenIssues(productId);
110
+ const product = productId
111
+ ? await fetchProductBasics(productId)
112
+ : await fetchRepositoryBasics(repoId);
113
+ const existingIssues = productId
114
+ ? await fetchOpenIssues(productId)
115
+ : await fetchOpenIssuesByRepo(repoId);
105
116
  logInfo(`Loaded ${existingIssues.length} existing issues for dedup context`);
106
117
  const systemPrompt = createFindBugsSystemPrompt();
107
118
  const userPrompt = createFindBugsUserPrompt({
@@ -155,7 +166,7 @@ export async function scanForBugs(options) {
155
166
  }
156
167
  if (!scanResult) {
157
168
  const msg = 'Audit failed: could not parse a scan_result from the agent';
158
- updateFindBugsState(productId, { lastError: msg });
169
+ updateFindBugsState(scopeId, { lastError: msg });
159
170
  return {
160
171
  status: 'error',
161
172
  message: msg,
@@ -165,13 +176,13 @@ export async function scanForBugs(options) {
165
176
  logInfo(`Audit produced ${bugs.length} candidate bugs. ${summary}`);
166
177
  let created = 0;
167
178
  for (const bug of bugs) {
168
- const issueId = await createIssueForBug(productId, bug);
179
+ const issueId = await createIssueForBug({ productId, repoId }, bug);
169
180
  if (issueId) {
170
181
  created++;
171
182
  logSuccess(`Filed issue ${issueId}: ${bug.title}`);
172
183
  }
173
184
  }
174
- updateFindBugsState(productId, {
185
+ updateFindBugsState(scopeId, {
175
186
  lastScannedCommitSha: headSha,
176
187
  lastScannedAt: new Date().toISOString(),
177
188
  lastError: undefined,
@@ -189,7 +200,7 @@ export async function scanForBugs(options) {
189
200
  catch (error) {
190
201
  const errorMessage = error instanceof Error ? error.message : String(error);
191
202
  logError(`Bug scan failed: ${errorMessage}`);
192
- updateFindBugsState(productId, { lastError: errorMessage });
203
+ updateFindBugsState(scopeId, { lastError: errorMessage });
193
204
  return {
194
205
  status: 'error',
195
206
  message: `Bug scan failed: ${errorMessage}`,
@@ -202,11 +213,13 @@ export async function scanForBugs(options) {
202
213
  lock.release();
203
214
  }
204
215
  }
205
- async function createIssueForBug(productId, bug) {
216
+ async function createIssueForBug(scope, bug) {
206
217
  return createIssue({
207
- productId,
218
+ productId: scope.productId,
219
+ repoId: scope.repoId,
208
220
  title: bug.title,
209
221
  description: formatIssueDescription(bug),
222
+ source: 'quality',
210
223
  });
211
224
  }
212
225
  function formatIssueDescription(bug) {
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Server-side scan baseline, shared across the find-* phases.
3
+ *
4
+ * A baseline pins a commit as the "everything before here is acknowledged"
5
+ * floor for a scope. On a fresh scan (no machine-local scan-state cursor) the
6
+ * find-* scanners use it as the diff base so only findings in code changed
7
+ * since the baseline are reported — the "only new" view DCM-style baselines
8
+ * provide — instead of re-filing the entire pre-existing backlog.
9
+ *
10
+ * Read here via the user's supabase session; written from the desktop app
11
+ * (services/db/quality-baselines.ts). Stored in `quality_scan_baselines`.
12
+ */
13
+ export interface ScanScope {
14
+ productId?: string;
15
+ repoId?: string;
16
+ }
17
+ /**
18
+ * Resolve the diff base for a scan, given the explicit `--full` flag, the
19
+ * machine-local last-scanned cursor, and the server baseline. Pure + exported
20
+ * for testing.
21
+ *
22
+ * - `--full` always wins → undefined (scan everything, ignore baseline).
23
+ * - otherwise prefer the local cursor (normal incremental), falling back to
24
+ * the baseline so a fresh checkout still suppresses the pre-existing backlog.
25
+ */
26
+ export declare function resolveScanBaseSha(opts: {
27
+ full?: boolean;
28
+ lastScannedSha?: string;
29
+ baselineSha?: string | null;
30
+ }): string | undefined;
31
+ /**
32
+ * Fetch the pinned baseline commit for a scope, or null if none is set / no
33
+ * session is available. Never throws — a baseline read failure must not abort
34
+ * a scan (it just degrades to a full scan).
35
+ */
36
+ export declare function getScanBaseline(scope: ScanScope): Promise<string | null>;
37
+ /**
38
+ * Fetch the baseline and resolve the scan's diff base in one call, logging when
39
+ * the baseline floor is applied on a fresh checkout. Shared by every find-*
40
+ * phase so the baseline wiring lives in one place.
41
+ */
42
+ export declare function resolveScanBase(scope: ScanScope, opts: {
43
+ full?: boolean;
44
+ lastScannedSha?: string;
45
+ }): Promise<string | undefined>;