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.
- package/dist/commands/api-docs/index.d.ts +18 -0
- package/dist/commands/api-docs/index.js +41 -0
- 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/commands/quality-benchmark/index.d.ts +5 -0
- package/dist/commands/quality-benchmark/index.js +28 -0
- package/dist/index.js +38 -6
- package/dist/phases/api-docs/index.d.ts +47 -0
- package/dist/phases/api-docs/index.js +254 -0
- package/dist/phases/api-docs/mcp-server.d.ts +25 -0
- package/dist/phases/api-docs/mcp-server.js +82 -0
- package/dist/phases/api-docs/prompts.d.ts +16 -0
- package/dist/phases/api-docs/prompts.js +65 -0
- package/dist/phases/api-docs/types.d.ts +22 -0
- package/dist/phases/api-docs/types.js +10 -0
- package/dist/phases/find-architecture/index.d.ts +4 -1
- package/dist/phases/find-architecture/index.js +46 -26
- package/dist/phases/find-architecture/prompts.d.ts +2 -1
- package/dist/phases/find-architecture/prompts.js +3 -2
- package/dist/phases/find-bugs/index.d.ts +4 -1
- package/dist/phases/find-bugs/index.js +32 -19
- package/dist/phases/find-shared/baseline.d.ts +45 -0
- package/dist/phases/find-shared/baseline.js +56 -0
- package/dist/phases/find-shared/custom-rules.d.ts +39 -0
- package/dist/phases/find-shared/custom-rules.js +75 -0
- package/dist/phases/find-shared/detect-context.d.ts +40 -0
- package/dist/phases/find-shared/detect-context.js +247 -0
- package/dist/phases/find-shared/mcp.d.ts +24 -3
- package/dist/phases/find-shared/mcp.js +41 -4
- package/dist/phases/find-shared/rule-config.d.ts +37 -0
- package/dist/phases/find-shared/rule-config.js +67 -0
- package/dist/phases/find-shared/rule-packs.d.ts +65 -0
- package/dist/phases/find-shared/rule-packs.js +124 -0
- package/dist/phases/find-shared/scoped-read.d.ts +12 -0
- package/dist/phases/find-shared/scoped-read.js +33 -0
- package/dist/phases/find-smells/index.d.ts +4 -1
- package/dist/phases/find-smells/index.js +43 -23
- package/dist/phases/find-smells/prompts.d.ts +2 -1
- package/dist/phases/find-smells/prompts.js +4 -3
- package/dist/phases/quality-benchmark/gate.d.ts +50 -0
- package/dist/phases/quality-benchmark/gate.js +91 -0
- package/dist/phases/quality-benchmark/index.js +15 -1
- package/dist/phases/quality-benchmark/parsers.d.ts +23 -0
- package/dist/phases/quality-benchmark/parsers.js +210 -0
- package/dist/phases/quality-benchmark/rubric.md +37 -0
- package/dist/phases/quality-benchmark/tool-catalog.js +58 -1
- package/dist/phases/quality-benchmark/types.d.ts +8 -1
- 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
|
-
|
|
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.
|
|
31
|
-
*
|
|
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.
|
|
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,
|
|
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:
|
|
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[]>;
|