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,56 @@
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
+ import { logInfo } from '../../utils/logger.js';
14
+ import { readScopedRow } from './scoped-read.js';
15
+ /**
16
+ * Resolve the diff base for a scan, given the explicit `--full` flag, the
17
+ * machine-local last-scanned cursor, and the server baseline. Pure + exported
18
+ * for testing.
19
+ *
20
+ * - `--full` always wins → undefined (scan everything, ignore baseline).
21
+ * - otherwise prefer the local cursor (normal incremental), falling back to
22
+ * the baseline so a fresh checkout still suppresses the pre-existing backlog.
23
+ */
24
+ export function resolveScanBaseSha(opts) {
25
+ if (opts.full) {
26
+ return undefined;
27
+ }
28
+ // `||` (not `??`) so an empty-string cursor falls through to the baseline.
29
+ return opts.lastScannedSha || opts.baselineSha || undefined;
30
+ }
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 async function getScanBaseline(scope) {
37
+ const row = await readScopedRow('quality_scan_baselines', 'baseline_commit_sha', scope);
38
+ return row?.baseline_commit_sha ?? null;
39
+ }
40
+ /**
41
+ * Fetch the baseline and resolve the scan's diff base in one call, logging when
42
+ * the baseline floor is applied on a fresh checkout. Shared by every find-*
43
+ * phase so the baseline wiring lives in one place.
44
+ */
45
+ export async function resolveScanBase(scope, opts) {
46
+ const baselineSha = await getScanBaseline(scope);
47
+ const baseSha = resolveScanBaseSha({
48
+ full: opts.full,
49
+ lastScannedSha: opts.lastScannedSha,
50
+ baselineSha,
51
+ });
52
+ if (!opts.full && !opts.lastScannedSha && baselineSha) {
53
+ logInfo(`No local scan history; using server baseline ${baselineSha.slice(0, 8)} — reporting only findings new since the baseline.`);
54
+ }
55
+ return baseSha;
56
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Per-scope user-authored custom rules, shared across the find-* phases.
3
+ *
4
+ * Where the built-in rule packs (rule-packs.ts) ship stack-specific guidance,
5
+ * custom rules let a team write their *own* checks in natural language — the
6
+ * configurable-rules / CQLinq capability NDepend-style tools expose, without
7
+ * the query language. Each enabled rule's guidance is appended to the relevant
8
+ * scanner's system prompt so the agent flags violations like any other finding.
9
+ *
10
+ * Read here from the user's supabase session; authored from the desktop app
11
+ * (services/db/quality-custom-rules.ts). Stored in `quality_custom_rules`.
12
+ */
13
+ import type { ScanScope } from './baseline.js';
14
+ import type { FindPhaseKind } from './rule-packs.js';
15
+ /** Phases that consume injected custom-rule guidance today. */
16
+ export type CustomRulePhase = Extract<FindPhaseKind, 'smells' | 'architecture'>;
17
+ export interface CustomRule {
18
+ label: string;
19
+ guidance: string;
20
+ }
21
+ /**
22
+ * Fetch the enabled custom rules for a scope + phase, oldest first. Never
23
+ * throws — a read failure (or no session) must not abort a scan; it just means
24
+ * no custom rules are injected.
25
+ */
26
+ export declare function getCustomRules(scope: ScanScope, phase: CustomRulePhase): Promise<CustomRule[]>;
27
+ /**
28
+ * Render custom rules into a markdown block for appending to a find-* system
29
+ * prompt. Returns '' when there are none, so callers can concatenate
30
+ * unconditionally (same contract as renderRulePacks). Pure + exported for
31
+ * testing.
32
+ */
33
+ export declare function renderCustomRules(rules: CustomRule[]): string;
34
+ /**
35
+ * Fetch + render the scope's custom rules for a phase in one call, logging how
36
+ * many were applied. Shared by the rule-consuming phases so the wiring lives in
37
+ * one place. Returns the markdown block ('' when none apply).
38
+ */
39
+ export declare function resolveCustomRules(scope: ScanScope, phase: CustomRulePhase): Promise<string>;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Per-scope user-authored custom rules, shared across the find-* phases.
3
+ *
4
+ * Where the built-in rule packs (rule-packs.ts) ship stack-specific guidance,
5
+ * custom rules let a team write their *own* checks in natural language — the
6
+ * configurable-rules / CQLinq capability NDepend-style tools expose, without
7
+ * the query language. Each enabled rule's guidance is appended to the relevant
8
+ * scanner's system prompt so the agent flags violations like any other finding.
9
+ *
10
+ * Read here from the user's supabase session; authored from the desktop app
11
+ * (services/db/quality-custom-rules.ts). Stored in `quality_custom_rules`.
12
+ */
13
+ import { getSupabase, hasSupabaseSession } from '../../supabase/client.js';
14
+ import { logInfo, logWarning } from '../../utils/logger.js';
15
+ /**
16
+ * Fetch the enabled custom rules for a scope + phase, oldest first. Never
17
+ * throws — a read failure (or no session) must not abort a scan; it just means
18
+ * no custom rules are injected.
19
+ */
20
+ export async function getCustomRules(scope, phase) {
21
+ if (!hasSupabaseSession()) {
22
+ return [];
23
+ }
24
+ try {
25
+ const query = getSupabase()
26
+ .from('quality_custom_rules')
27
+ .select('label, guidance')
28
+ .eq('phase', phase)
29
+ .eq('enabled', true);
30
+ const scoped = scope.productId
31
+ ? query.eq('product_id', scope.productId)
32
+ : query.eq('repository_id', scope.repoId);
33
+ const { data, error } = await scoped.order('created_at', {
34
+ ascending: true,
35
+ });
36
+ if (error) {
37
+ logWarning(`Could not read quality_custom_rules: ${error.message}`);
38
+ return [];
39
+ }
40
+ return data ?? [];
41
+ }
42
+ catch (err) {
43
+ logWarning(`Could not read quality_custom_rules: ${err instanceof Error ? err.message : String(err)}`);
44
+ return [];
45
+ }
46
+ }
47
+ /**
48
+ * Render custom rules into a markdown block for appending to a find-* system
49
+ * prompt. Returns '' when there are none, so callers can concatenate
50
+ * unconditionally (same contract as renderRulePacks). Pure + exported for
51
+ * testing.
52
+ */
53
+ export function renderCustomRules(rules) {
54
+ if (rules.length === 0) {
55
+ return '';
56
+ }
57
+ const sections = rules
58
+ .map((r) => `### ${r.label}\n${r.guidance}`)
59
+ .join('\n\n');
60
+ return `\n\n**Custom rules** — this team has defined the following project-specific checks. Treat each as an additional category, applying the same severity rubric and output format as the generic categories above:\n\n${sections}`;
61
+ }
62
+ /**
63
+ * Fetch + render the scope's custom rules for a phase in one call, logging how
64
+ * many were applied. Shared by the rule-consuming phases so the wiring lives in
65
+ * one place. Returns the markdown block ('' when none apply).
66
+ */
67
+ export async function resolveCustomRules(scope, phase) {
68
+ const rules = await getCustomRules(scope, phase);
69
+ if (rules.length > 0) {
70
+ logInfo(`Applying ${rules.length} custom ${phase} rule(s): ${rules
71
+ .map((r) => r.label)
72
+ .join(', ')}`);
73
+ }
74
+ return renderCustomRules(rules);
75
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Deterministic project-context detection, shared across the find-* phases.
3
+ *
4
+ * The find-* auditors are LLM-driven and already language-agnostic for the
5
+ * generic smell/bug/architecture categories — the agent reads whatever source
6
+ * is in the repo. This module adds the missing piece needed for *targeted*,
7
+ * stack-specific rules: a cheap, no-LLM detection of which languages and
8
+ * frameworks a repository actually contains, so the right rule packs (see
9
+ * `rule-packs.ts`) can be selected and injected into the system prompt.
10
+ *
11
+ * It mirrors the role that Phase 1 detection plays for `quality-benchmark`,
12
+ * but stays deterministic (manifest-file inspection only) so a scan never pays
13
+ * an extra model round-trip just to figure out the stack.
14
+ *
15
+ * Polyglot repos are first-class: a monorepo with `apps/mobile/pubspec.yaml`
16
+ * and `services/api/go.mod` resolves to `{ languages: ['dart','go'],
17
+ * frameworks: ['flutter'] }`, and every matching rule pack is selected.
18
+ *
19
+ * Detection is best-effort and must never throw — a malformed manifest simply
20
+ * contributes nothing.
21
+ */
22
+ /**
23
+ * Detected stack for a repository. Values are lowercased so callers can do
24
+ * case-insensitive set membership without re-normalising — same convention as
25
+ * `selectToolsForContext` in the quality-benchmark tool catalog.
26
+ */
27
+ export interface ProjectContext {
28
+ /** Languages present, e.g. ['dart', 'go', 'ts']. */
29
+ languages: string[];
30
+ /** Frameworks present, e.g. ['flutter', 'react']. */
31
+ frameworks: string[];
32
+ /** Marker filenames that were found, e.g. ['pubspec.yaml', 'package.json']. */
33
+ files_present: string[];
34
+ }
35
+ /**
36
+ * Detect the languages and frameworks present in a repository checkout.
37
+ *
38
+ * @param repoRoot absolute path to the repo checkout (cwd of the auditor).
39
+ */
40
+ export declare function detectProjectContext(repoRoot: string): ProjectContext;
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Deterministic project-context detection, shared across the find-* phases.
3
+ *
4
+ * The find-* auditors are LLM-driven and already language-agnostic for the
5
+ * generic smell/bug/architecture categories — the agent reads whatever source
6
+ * is in the repo. This module adds the missing piece needed for *targeted*,
7
+ * stack-specific rules: a cheap, no-LLM detection of which languages and
8
+ * frameworks a repository actually contains, so the right rule packs (see
9
+ * `rule-packs.ts`) can be selected and injected into the system prompt.
10
+ *
11
+ * It mirrors the role that Phase 1 detection plays for `quality-benchmark`,
12
+ * but stays deterministic (manifest-file inspection only) so a scan never pays
13
+ * an extra model round-trip just to figure out the stack.
14
+ *
15
+ * Polyglot repos are first-class: a monorepo with `apps/mobile/pubspec.yaml`
16
+ * and `services/api/go.mod` resolves to `{ languages: ['dart','go'],
17
+ * frameworks: ['flutter'] }`, and every matching rule pack is selected.
18
+ *
19
+ * Detection is best-effort and must never throw — a malformed manifest simply
20
+ * contributes nothing.
21
+ */
22
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
23
+ import { join } from 'node:path';
24
+ /** Directories never worth walking — vendored, generated, or VCS metadata. */
25
+ const SKIP_DIRS = new Set([
26
+ 'node_modules',
27
+ '.git',
28
+ 'dist',
29
+ 'build',
30
+ 'out',
31
+ '.next',
32
+ '.dart_tool',
33
+ 'target',
34
+ 'vendor',
35
+ 'Pods',
36
+ '.gradle',
37
+ '__pycache__',
38
+ '.venv',
39
+ 'venv',
40
+ ]);
41
+ /**
42
+ * How deep to walk looking for manifest files. Root + a couple of levels
43
+ * catches the common monorepo layouts (`apps/<x>/`, `packages/<x>/`,
44
+ * `services/<x>/`) without descending into the whole tree.
45
+ */
46
+ const MAX_DEPTH = 3;
47
+ /**
48
+ * Walk the repo (bounded depth, skipping vendored dirs) and collect the
49
+ * absolute paths of every manifest file we know how to read. Defensive: any
50
+ * unreadable directory is skipped silently.
51
+ */
52
+ function collectManifests(repoRoot) {
53
+ // marker filename → list of absolute paths where it was found
54
+ const found = new Map();
55
+ const record = (marker, absPath) => {
56
+ const list = found.get(marker) ?? [];
57
+ list.push(absPath);
58
+ found.set(marker, list);
59
+ };
60
+ const walk = (dir, depth) => {
61
+ if (depth > MAX_DEPTH) {
62
+ return;
63
+ }
64
+ let entries;
65
+ try {
66
+ entries = readdirSync(dir);
67
+ }
68
+ catch {
69
+ return;
70
+ }
71
+ for (const entry of entries) {
72
+ const abs = join(dir, entry);
73
+ let isDir = false;
74
+ try {
75
+ isDir = statSync(abs).isDirectory();
76
+ }
77
+ catch {
78
+ continue;
79
+ }
80
+ if (isDir) {
81
+ if (!SKIP_DIRS.has(entry) && !entry.startsWith('.')) {
82
+ walk(abs, depth + 1);
83
+ }
84
+ continue;
85
+ }
86
+ // File: record exact-name and extension-based markers.
87
+ if (MARKER_FILES.has(entry)) {
88
+ record(entry, abs);
89
+ }
90
+ else if (entry.endsWith('.csproj')) {
91
+ record('*.csproj', abs);
92
+ }
93
+ else if (entry.endsWith('.gemspec')) {
94
+ record('*.gemspec', abs);
95
+ }
96
+ }
97
+ };
98
+ walk(repoRoot, 0);
99
+ return found;
100
+ }
101
+ /** Exact-name manifest files we look for. */
102
+ const MARKER_FILES = new Set([
103
+ 'pubspec.yaml',
104
+ 'package.json',
105
+ 'tsconfig.json',
106
+ 'go.mod',
107
+ 'Cargo.toml',
108
+ 'pyproject.toml',
109
+ 'requirements.txt',
110
+ 'setup.py',
111
+ 'Pipfile',
112
+ 'Gemfile',
113
+ 'pom.xml',
114
+ 'build.gradle',
115
+ 'build.gradle.kts',
116
+ ]);
117
+ function safeRead(paths) {
118
+ if (!paths || paths.length === 0) {
119
+ return '';
120
+ }
121
+ let out = '';
122
+ for (const p of paths) {
123
+ try {
124
+ out += `\n${readFileSync(p, 'utf-8')}`;
125
+ }
126
+ catch {
127
+ // unreadable manifest contributes nothing
128
+ }
129
+ }
130
+ return out;
131
+ }
132
+ /** Add every framework whose marker regex matches `haystack`. */
133
+ function addFrameworks(frameworks, haystack, markers) {
134
+ for (const [re, name] of markers) {
135
+ if (re.test(haystack)) {
136
+ frameworks.add(name);
137
+ }
138
+ }
139
+ }
140
+ function detectDart({ manifests, languages, frameworks }) {
141
+ if (!manifests.has('pubspec.yaml')) {
142
+ return;
143
+ }
144
+ languages.add('dart');
145
+ const pubspec = safeRead(manifests.get('pubspec.yaml'));
146
+ // Flutter projects declare `flutter:` as a dependency / SDK constraint.
147
+ if (/\bflutter\s*:/.test(pubspec) || /sdk:\s*flutter/.test(pubspec)) {
148
+ frameworks.add('flutter');
149
+ }
150
+ }
151
+ const JS_FRAMEWORK_MARKERS = [
152
+ [/"react-native"\s*:/, 'react-native'],
153
+ [/"react"\s*:/, 'react'],
154
+ [/"next"\s*:/, 'next'],
155
+ [/"vue"\s*:/, 'vue'],
156
+ [/"@angular\/core"\s*:/, 'angular'],
157
+ [/"svelte"\s*:/, 'svelte'],
158
+ [/"express"\s*:/, 'express'],
159
+ [/"@nestjs\/core"\s*:/, 'nest'],
160
+ ];
161
+ function detectJsTs({ manifests, languages, frameworks }) {
162
+ if (!manifests.has('package.json')) {
163
+ return;
164
+ }
165
+ languages.add('js');
166
+ const pkg = safeRead(manifests.get('package.json'));
167
+ if (manifests.has('tsconfig.json') || /"typescript"\s*:/.test(pkg)) {
168
+ languages.add('ts');
169
+ }
170
+ addFrameworks(frameworks, pkg, JS_FRAMEWORK_MARKERS);
171
+ }
172
+ const PY_FRAMEWORK_MARKERS = [
173
+ [/\bdjango\b/i, 'django'],
174
+ [/\bfastapi\b/i, 'fastapi'],
175
+ [/\bflask\b/i, 'flask'],
176
+ ];
177
+ function detectPython({ manifests, languages, frameworks }) {
178
+ const hasPy = manifests.has('pyproject.toml') ||
179
+ manifests.has('requirements.txt') ||
180
+ manifests.has('setup.py') ||
181
+ manifests.has('Pipfile');
182
+ if (!hasPy) {
183
+ return;
184
+ }
185
+ languages.add('py');
186
+ const py = safeRead(manifests.get('pyproject.toml')) +
187
+ safeRead(manifests.get('requirements.txt')) +
188
+ safeRead(manifests.get('Pipfile'));
189
+ addFrameworks(frameworks, py, PY_FRAMEWORK_MARKERS);
190
+ }
191
+ function detectRuby({ manifests, languages, frameworks }) {
192
+ if (!manifests.has('Gemfile') && !manifests.has('*.gemspec')) {
193
+ return;
194
+ }
195
+ languages.add('ruby');
196
+ if (/\brails\b/i.test(safeRead(manifests.get('Gemfile')))) {
197
+ frameworks.add('rails');
198
+ }
199
+ }
200
+ function detectJvm({ manifests, languages }) {
201
+ const hasJvm = manifests.has('pom.xml') ||
202
+ manifests.has('build.gradle') ||
203
+ manifests.has('build.gradle.kts');
204
+ if (!hasJvm) {
205
+ return;
206
+ }
207
+ languages.add('java');
208
+ const gradle = safeRead(manifests.get('build.gradle')) +
209
+ safeRead(manifests.get('build.gradle.kts'));
210
+ if (manifests.has('build.gradle.kts') || /\bkotlin\b/i.test(gradle)) {
211
+ languages.add('kotlin');
212
+ }
213
+ }
214
+ /** Single-marker languages with no framework inference. */
215
+ const SIMPLE_LANGUAGES = [
216
+ ['go.mod', 'go'],
217
+ ['Cargo.toml', 'rust'],
218
+ ['*.csproj', 'cs'],
219
+ ];
220
+ /**
221
+ * Detect the languages and frameworks present in a repository checkout.
222
+ *
223
+ * @param repoRoot absolute path to the repo checkout (cwd of the auditor).
224
+ */
225
+ export function detectProjectContext(repoRoot) {
226
+ const manifests = collectManifests(repoRoot);
227
+ const acc = {
228
+ manifests,
229
+ languages: new Set(),
230
+ frameworks: new Set(),
231
+ };
232
+ for (const [marker, lang] of SIMPLE_LANGUAGES) {
233
+ if (manifests.has(marker)) {
234
+ acc.languages.add(lang);
235
+ }
236
+ }
237
+ detectDart(acc);
238
+ detectJsTs(acc);
239
+ detectPython(acc);
240
+ detectRuby(acc);
241
+ detectJvm(acc);
242
+ return {
243
+ languages: [...acc.languages],
244
+ frameworks: [...acc.frameworks],
245
+ files_present: [...manifests.keys()],
246
+ };
247
+ }
@@ -12,22 +12,43 @@ 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
- productId: string;
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;
41
+ /**
42
+ * Origin tag written to `issues.source`. Omit to accept the DB default
43
+ * ('manual'). The find-* scanners pass 'quality' so automated findings can be
44
+ * told apart from hand-written and externally-synced issues.
45
+ */
46
+ source?: string;
28
47
  }
29
48
  /**
30
- * File a new issue via MCP. Returns the new issue id, or null if MCP returned
31
- * an error / unexpected shape (already logged).
49
+ * File a new issue via MCP. The issue is scoped to a product OR a single
50
+ * repository exactly one of `productId` / `repoId` should be set. Returns the
51
+ * new issue id, or null if MCP returned an error / unexpected shape (already
52
+ * logged).
32
53
  */
33
54
  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,19 +80,25 @@ export function isTerminalStatus(status) {
51
80
  status === 'completed');
52
81
  }
53
82
  /**
54
- * File a new issue via MCP. Returns the new issue id, or null if MCP returned
55
- * an error / unexpected shape (already logged).
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: input.productId,
97
+ product_id: productId,
98
+ repository_id: repositoryId,
65
99
  name: input.title,
66
100
  description: input.description,
101
+ ...(input.source ? { source: input.source } : {}),
67
102
  })
68
103
  .select('id')
69
104
  .single();
@@ -77,9 +112,11 @@ export async function createIssue(input) {
77
112
  }
78
113
  }
79
114
  const result = (await callMcpEndpoint('issues/create', {
80
- product_id: input.productId,
115
+ ...(productId ? { product_id: productId } : {}),
116
+ ...(repositoryId ? { repository_id: repositoryId } : {}),
81
117
  name: input.title,
82
118
  description: input.description,
119
+ ...(input.source ? { source: input.source } : {}),
83
120
  }));
84
121
  return result.issue?.id || result.id || null;
85
122
  }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Per-scope rule-pack configuration, shared across the find-* phases.
3
+ *
4
+ * Lets a team tune which stack-aware rule packs the scanners apply via a preset
5
+ * plus explicit per-pack toggles — the configurable-rules capability DCM-style
6
+ * tools expose. Read here from the user's supabase session; edited from the
7
+ * desktop app (services/db/quality-rule-configs.ts). Stored in
8
+ * `quality_rule_configs`.
9
+ */
10
+ import type { ScanScope } from './baseline.js';
11
+ import { type RulePack } from './rule-packs.js';
12
+ export type RulePreset = 'minimal' | 'recommended' | 'strict';
13
+ export interface RuleConfig {
14
+ preset: RulePreset;
15
+ disabledPacks: string[];
16
+ }
17
+ /**
18
+ * Apply a rule config to the packs detection selected. Pure + exported for
19
+ * testing.
20
+ *
21
+ * - no config → unchanged (default is 'recommended' = all detected packs).
22
+ * - 'minimal' → no packs (generic categories only).
23
+ * - otherwise → detected packs minus the explicitly disabled ones.
24
+ */
25
+ export declare function applyRuleConfig(packs: RulePack[], config: RuleConfig | null): RulePack[];
26
+ /**
27
+ * Fetch the rule config for a scope, or null if none is set / no session is
28
+ * available. Never throws — a read failure must not abort a scan (it just
29
+ * degrades to the default 'recommended' behaviour).
30
+ */
31
+ export declare function getRuleConfig(scope: ScanScope): Promise<RuleConfig | null>;
32
+ /**
33
+ * Detect the repo's stack, apply the scope's rule config, and return the rule
34
+ * packs to inject — logging the detected stack and chosen packs. Shared by the
35
+ * pack-consuming phases (find-smells, find-architecture).
36
+ */
37
+ export declare function resolveStackRulePacks(repoRoot: string, scope: ScanScope): Promise<RulePack[]>;