ghagga 3.2.0 → 3.3.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/index-cmd.d.ts +49 -0
- package/dist/commands/index-cmd.d.ts.map +1 -0
- package/dist/commands/index-cmd.js +367 -0
- package/dist/commands/index-cmd.js.map +1 -0
- package/dist/commands/index-cmd.test.d.ts +17 -0
- package/dist/commands/index-cmd.test.d.ts.map +1 -0
- package/dist/commands/index-cmd.test.js +541 -0
- package/dist/commands/index-cmd.test.js.map +1 -0
- package/dist/commands/indexer-registry.d.ts +93 -0
- package/dist/commands/indexer-registry.d.ts.map +1 -0
- package/dist/commands/indexer-registry.js +342 -0
- package/dist/commands/indexer-registry.js.map +1 -0
- package/dist/commands/indexer-registry.test.d.ts +10 -0
- package/dist/commands/indexer-registry.test.d.ts.map +1 -0
- package/dist/commands/indexer-registry.test.js +538 -0
- package/dist/commands/indexer-registry.test.js.map +1 -0
- package/dist/commands/review-blast-radius.test.d.ts +13 -0
- package/dist/commands/review-blast-radius.test.d.ts.map +1 -0
- package/dist/commands/review-blast-radius.test.js +179 -0
- package/dist/commands/review-blast-radius.test.js.map +1 -0
- package/dist/commands/review.d.ts +112 -1
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/commands/review.js +264 -2
- package/dist/commands/review.js.map +1 -1
- package/dist/commands/review.test.js +290 -0
- package/dist/commands/review.test.js.map +1 -1
- package/dist/commands/triage.d.ts +14 -0
- package/dist/commands/triage.d.ts.map +1 -0
- package/dist/commands/triage.js +153 -0
- package/dist/commands/triage.js.map +1 -0
- package/dist/commands/triage.test.d.ts +8 -0
- package/dist/commands/triage.test.d.ts.map +1 -0
- package/dist/commands/triage.test.js +184 -0
- package/dist/commands/triage.test.js.map +1 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the blast-radius pure seams extracted from `reviewCommand`
|
|
3
|
+
* (design v2 D9): `resolveBlastRadiusEnabled`, `checkGraphStaleness`,
|
|
4
|
+
* `computeUncoveredLanguages`.
|
|
5
|
+
*
|
|
6
|
+
* `resolveBlastRadiusEnabled` touches the real filesystem (existsSync) — a
|
|
7
|
+
* REAL temp dir (mkdtempSync) is used, NOT a mocked `node:fs`, so the actual
|
|
8
|
+
* repoPath-relative resolution is exercised (CRITICAL-2 regression guard).
|
|
9
|
+
* This file deliberately does NOT `vi.mock('node:fs')` (unlike review.test.ts)
|
|
10
|
+
* to keep that guarantee honest.
|
|
11
|
+
*/
|
|
12
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { tmpdir } from 'node:os';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { GRAPH_VERSION } from 'ghagga-core';
|
|
16
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
17
|
+
import { checkGraphStaleness, computeUncoveredLanguages, resolveBlastRadiusEnabled, } from './review.js';
|
|
18
|
+
// ─── resolveBlastRadiusEnabled ───────────────────────────────────
|
|
19
|
+
describe('resolveBlastRadiusEnabled', () => {
|
|
20
|
+
let repoPath;
|
|
21
|
+
let otherCwd;
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
repoPath = mkdtempSync(join(tmpdir(), 'ghagga-blast-radius-repo-'));
|
|
24
|
+
otherCwd = mkdtempSync(join(tmpdir(), 'ghagga-blast-radius-cwd-'));
|
|
25
|
+
});
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
rmSync(repoPath, { recursive: true, force: true });
|
|
28
|
+
rmSync(otherCwd, { recursive: true, force: true });
|
|
29
|
+
});
|
|
30
|
+
function withGraph() {
|
|
31
|
+
mkdirSync(join(repoPath, '.ghagga'), { recursive: true });
|
|
32
|
+
writeFileSync(join(repoPath, '.ghagga', 'graph.json'), '{}');
|
|
33
|
+
}
|
|
34
|
+
it('auto-enables when .ghagga/graph.json exists under repoPath', () => {
|
|
35
|
+
withGraph();
|
|
36
|
+
const result = resolveBlastRadiusEnabled(repoPath, {}, {});
|
|
37
|
+
expect(result).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
it('stays off when no graph.json exists', () => {
|
|
40
|
+
const result = resolveBlastRadiusEnabled(repoPath, {}, {});
|
|
41
|
+
expect(result).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
it('resolves against repoPath, NOT process.cwd() (CRITICAL-2)', () => {
|
|
44
|
+
withGraph();
|
|
45
|
+
const originalCwd = process.cwd();
|
|
46
|
+
try {
|
|
47
|
+
process.chdir(otherCwd);
|
|
48
|
+
// The graph lives under repoPath, not the (different) current cwd —
|
|
49
|
+
// the v1 bug used resolve('.') and would have returned false here.
|
|
50
|
+
const result = resolveBlastRadiusEnabled(repoPath, {}, {});
|
|
51
|
+
expect(result).toBe(true);
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
process.chdir(originalCwd);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
it('--no-blast-radius (blastRadius: false) wins over an existing graph', () => {
|
|
58
|
+
withGraph();
|
|
59
|
+
const result = resolveBlastRadiusEnabled(repoPath, { blastRadius: false }, {});
|
|
60
|
+
expect(result).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
it('.ghagga.json enableBlastRadius: false wins over an existing graph', () => {
|
|
63
|
+
withGraph();
|
|
64
|
+
const result = resolveBlastRadiusEnabled(repoPath, {}, { enableBlastRadius: false });
|
|
65
|
+
expect(result).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
it('.ghagga.json enableBlastRadius: true wins even without a graph', () => {
|
|
68
|
+
const result = resolveBlastRadiusEnabled(repoPath, {}, { enableBlastRadius: true });
|
|
69
|
+
expect(result).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
it('explicit --no-blast-radius overrides .ghagga.json enableBlastRadius: true', () => {
|
|
72
|
+
const result = resolveBlastRadiusEnabled(repoPath, { blastRadius: false }, { enableBlastRadius: true });
|
|
73
|
+
expect(result).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
// ─── checkGraphStaleness ──────────────────────────────────────────
|
|
77
|
+
describe('checkGraphStaleness', () => {
|
|
78
|
+
function makeMetadata(overrides = {}) {
|
|
79
|
+
return {
|
|
80
|
+
lastIndexedCommit: 'abc123',
|
|
81
|
+
lastIndexedAt: new Date().toISOString(),
|
|
82
|
+
schemaVersion: GRAPH_VERSION,
|
|
83
|
+
fileCount: 1,
|
|
84
|
+
languages: ['typescript'],
|
|
85
|
+
indexDurationMs: 10,
|
|
86
|
+
...overrides,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
it('returns no warnings when metadata is fresh and HEAD matches', () => {
|
|
90
|
+
const metadata = makeMetadata({ lastIndexedCommit: 'abc123' });
|
|
91
|
+
expect(checkGraphStaleness(metadata, 'abc123')).toEqual([]);
|
|
92
|
+
});
|
|
93
|
+
it('warns on HEAD mismatch', () => {
|
|
94
|
+
const metadata = makeMetadata({ lastIndexedCommit: 'abc123' });
|
|
95
|
+
const warnings = checkGraphStaleness(metadata, 'def456');
|
|
96
|
+
expect(warnings.some((w) => w.kind === 'head-mismatch')).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
it('skips the HEAD-mismatch check when currentHead is empty (non-git/detached)', () => {
|
|
99
|
+
const metadata = makeMetadata({ lastIndexedCommit: 'abc123' });
|
|
100
|
+
const warnings = checkGraphStaleness(metadata, '');
|
|
101
|
+
expect(warnings.some((w) => w.kind === 'head-mismatch')).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
it('warns on age > 7 days', () => {
|
|
104
|
+
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
105
|
+
const metadata = makeMetadata({ lastIndexedCommit: 'abc123', lastIndexedAt: thirtyDaysAgo });
|
|
106
|
+
const warnings = checkGraphStaleness(metadata, 'abc123');
|
|
107
|
+
expect(warnings.some((w) => w.kind === 'stale-age')).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
it('warns distinctly when metadata is null (B-005)', () => {
|
|
110
|
+
const warnings = checkGraphStaleness(null, 'abc123');
|
|
111
|
+
expect(warnings).toHaveLength(1);
|
|
112
|
+
expect(warnings[0]?.kind).toBe('no-metadata');
|
|
113
|
+
});
|
|
114
|
+
it('warns on graphVersion mismatch (B-003)', () => {
|
|
115
|
+
const metadata = makeMetadata({ lastIndexedCommit: 'abc123', graphVersion: GRAPH_VERSION + 1 });
|
|
116
|
+
const warnings = checkGraphStaleness(metadata, 'abc123');
|
|
117
|
+
expect(warnings.some((w) => w.kind === 'version-mismatch')).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
it('does not warn on graphVersion match', () => {
|
|
120
|
+
const metadata = makeMetadata({ lastIndexedCommit: 'abc123', graphVersion: GRAPH_VERSION });
|
|
121
|
+
const warnings = checkGraphStaleness(metadata, 'abc123');
|
|
122
|
+
expect(warnings.some((w) => w.kind === 'version-mismatch')).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
// ─── computeUncoveredLanguages ────────────────────────────────────
|
|
126
|
+
describe('computeUncoveredLanguages', () => {
|
|
127
|
+
function makeGraph(nodeLanguages) {
|
|
128
|
+
return {
|
|
129
|
+
version: GRAPH_VERSION,
|
|
130
|
+
rootDir: '.',
|
|
131
|
+
nodes: Object.fromEntries(Object.entries(nodeLanguages).map(([path, language]) => [
|
|
132
|
+
path,
|
|
133
|
+
{
|
|
134
|
+
hash: 'h',
|
|
135
|
+
// biome-ignore lint/suspicious/noExplicitAny: fixture language is dynamic
|
|
136
|
+
language: language,
|
|
137
|
+
imports: [],
|
|
138
|
+
exports: [],
|
|
139
|
+
calls: [],
|
|
140
|
+
isTest: false,
|
|
141
|
+
},
|
|
142
|
+
])),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
it('flags a changed file whose language has zero graph coverage', () => {
|
|
146
|
+
const graph = makeGraph({ 'src/main.go': 'go' });
|
|
147
|
+
const uncovered = computeUncoveredLanguages(graph, ['app.py']);
|
|
148
|
+
expect(uncovered).toContain('python');
|
|
149
|
+
});
|
|
150
|
+
it('does not flag a changed file whose language IS covered', () => {
|
|
151
|
+
const graph = makeGraph({ 'src/main.go': 'go' });
|
|
152
|
+
const uncovered = computeUncoveredLanguages(graph, ['src/other.go']);
|
|
153
|
+
expect(uncovered).not.toContain('go');
|
|
154
|
+
});
|
|
155
|
+
it('regex-fallback graph (languages populated from real nodes) is NOT all-flagged (CRITICAL-1)', () => {
|
|
156
|
+
// A regex-fallback-built graph still has real per-file `language` on
|
|
157
|
+
// every node (unlike dispatch's `indexedLanguages`, which is empty for
|
|
158
|
+
// the regex path) — so coverage must be computed from graph CONTENTS.
|
|
159
|
+
const graph = makeGraph({ 'src/a.ts': 'typescript', 'src/b.py': 'python' });
|
|
160
|
+
const uncovered = computeUncoveredLanguages(graph, ['src/a.ts', 'src/b.py']);
|
|
161
|
+
expect(uncovered).toEqual([]);
|
|
162
|
+
});
|
|
163
|
+
it('flags a subpackage-only language not present anywhere in the graph', () => {
|
|
164
|
+
const graph = makeGraph({ 'src/main.go': 'go' });
|
|
165
|
+
const uncovered = computeUncoveredLanguages(graph, [
|
|
166
|
+
'packages/api/requirements.txt',
|
|
167
|
+
'src/main.go',
|
|
168
|
+
]);
|
|
169
|
+
// requirements.txt has no detectable language extension — must not crash
|
|
170
|
+
// and must not be spuriously flagged as a language.
|
|
171
|
+
expect(uncovered).toEqual([]);
|
|
172
|
+
});
|
|
173
|
+
it('flags multiple distinct uncovered languages without duplicates', () => {
|
|
174
|
+
const graph = makeGraph({ 'src/main.go': 'go' });
|
|
175
|
+
const uncovered = computeUncoveredLanguages(graph, ['a.py', 'b.py', 'c.rs']);
|
|
176
|
+
expect(uncovered.sort()).toEqual(['python', 'rust']);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
//# sourceMappingURL=review-blast-radius.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"review-blast-radius.test.js","sourceRoot":"","sources":["../../src/commands/review-blast-radius.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* environment, and optional .ghagga.json file, then runs the
|
|
6
6
|
* core review pipeline and formats the output.
|
|
7
7
|
*/
|
|
8
|
-
import type { LLMProvider, ReviewMode } from 'ghagga-core';
|
|
8
|
+
import type { DependencyGraph, GraphMetadata, LLMProvider, ReviewMode } from 'ghagga-core';
|
|
9
9
|
export interface ReviewOptions {
|
|
10
10
|
mode: ReviewMode;
|
|
11
11
|
provider: LLMProvider;
|
|
@@ -53,6 +53,117 @@ export interface ReviewOptions {
|
|
|
53
53
|
lenses?: string;
|
|
54
54
|
/** Path to custom lens definitions directory (from --lens-dir flag). */
|
|
55
55
|
lensDir?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Explicit blast-radius override from `--no-blast-radius`. `false` forces
|
|
58
|
+
* blast-radius off regardless of graph presence / config; `undefined`
|
|
59
|
+
* defers to `.ghagga.json`/auto-detection (see `resolveBlastRadiusEnabled`).
|
|
60
|
+
*/
|
|
61
|
+
blastRadius?: boolean;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* A single entry in the `.ghagga.json` `providerChain` array — routes ONE
|
|
65
|
+
* multi-voice reviewer (consensus vote / fan-out lens) to a specific
|
|
66
|
+
* cross-engine target via mcp-llm-bridge. Mirrors `ghagga-core`'s
|
|
67
|
+
* `ProviderChainEntry`, minus `apiKey`: the gateway token is resolved at
|
|
68
|
+
* runtime (CLI `--api-key` / stored login token), never stored in plaintext
|
|
69
|
+
* in the config file.
|
|
70
|
+
*/
|
|
71
|
+
interface GhaggaConfigProviderChainEntry {
|
|
72
|
+
/** Only 'gateway' entries are meaningful for cross-engine routing. */
|
|
73
|
+
provider: 'gateway';
|
|
74
|
+
/** Model identifier passed to the bridge (e.g. "claude-sonnet-4-6"). */
|
|
75
|
+
model: string;
|
|
76
|
+
/**
|
|
77
|
+
* Bridge-side provider id to route to (e.g. 'cli-claude', 'codex-cli').
|
|
78
|
+
* Short-circuits the bridge's model-based routing.
|
|
79
|
+
*/
|
|
80
|
+
targetProvider?: string;
|
|
81
|
+
/** Gateway base URL. Defaults to the first entry's / flat gateway config when omitted. */
|
|
82
|
+
gatewayUrl?: string;
|
|
83
|
+
/** OpenCode model in `provider/model` format (cli-bridge only — unused for gateway). */
|
|
84
|
+
cliModel?: string;
|
|
85
|
+
}
|
|
86
|
+
interface GhaggaConfig {
|
|
87
|
+
mode?: string;
|
|
88
|
+
provider?: string;
|
|
89
|
+
model?: string;
|
|
90
|
+
enableSemgrep?: boolean;
|
|
91
|
+
enableTrivy?: boolean;
|
|
92
|
+
enableCpd?: boolean;
|
|
93
|
+
customRules?: string[];
|
|
94
|
+
ignorePatterns?: string[];
|
|
95
|
+
reviewLevel?: string;
|
|
96
|
+
disabledTools?: string[];
|
|
97
|
+
enabledTools?: string[];
|
|
98
|
+
lenses?: string[];
|
|
99
|
+
/**
|
|
100
|
+
* Per-voice cross-engine provider chain (BL-CLI-PROVIDER-CHAIN). When set,
|
|
101
|
+
* consensus (3-vote) and fan-out (N-lens) modes round-robin each voice
|
|
102
|
+
* across these gateway entries instead of hammering a single engine.
|
|
103
|
+
*/
|
|
104
|
+
providerChain?: GhaggaConfigProviderChainEntry[];
|
|
105
|
+
/**
|
|
106
|
+
* Enable blast-radius filtering (design v2 D7). `true`/`false` override
|
|
107
|
+
* auto-detection (`.ghagga/graph.json` presence); omit to auto-detect.
|
|
108
|
+
* `--no-blast-radius` (ReviewOptions.blastRadius === false) always wins
|
|
109
|
+
* over this config key.
|
|
110
|
+
*/
|
|
111
|
+
enableBlastRadius?: boolean;
|
|
56
112
|
}
|
|
57
113
|
export declare function reviewCommand(targetPath: string, options: ReviewOptions): Promise<void>;
|
|
114
|
+
/** One staleness/coverage signal for `checkGraphStaleness` — WARN, never block. */
|
|
115
|
+
export interface StalenessWarning {
|
|
116
|
+
kind: 'head-mismatch' | 'stale-age' | 'no-metadata' | 'version-mismatch';
|
|
117
|
+
message: string;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Resolve whether blast-radius should be enabled for this review run.
|
|
121
|
+
*
|
|
122
|
+
* Precedence (highest to lowest):
|
|
123
|
+
* 1. `--no-blast-radius` (options.blastRadius === false) → OFF
|
|
124
|
+
* 2. `.ghagga.json` `enableBlastRadius` (explicit true/false) → that value
|
|
125
|
+
* 3. Auto-ON when `<repoPath>/.ghagga/graph.json` exists
|
|
126
|
+
* 4. OFF (no graph, no override)
|
|
127
|
+
*
|
|
128
|
+
* CRITICAL-2 (JD B-001): the existence check is resolved against `repoPath`
|
|
129
|
+
* — the review target — NOT `process.cwd()`, so `ghagga review <other-dir>`
|
|
130
|
+
* from an unrelated cwd still auto-detects correctly.
|
|
131
|
+
*/
|
|
132
|
+
export declare function resolveBlastRadiusEnabled(repoPath: string, options: Pick<ReviewOptions, 'blastRadius'>, fileConfig: Pick<GhaggaConfig, 'enableBlastRadius'>): boolean;
|
|
133
|
+
/**
|
|
134
|
+
* Detect staleness/coverage-verification signals for the loaded graph.
|
|
135
|
+
* ALWAYS warn-and-continue (design v2 D2) — never blocks the review.
|
|
136
|
+
*
|
|
137
|
+
* - HEAD mismatch: `metadata.lastIndexedCommit` non-empty and differs from
|
|
138
|
+
* `currentHead`. An EMPTY `currentHead` (non-git/detached checkout) skips
|
|
139
|
+
* this specific check — there's nothing meaningful to compare against.
|
|
140
|
+
* - Age: delegates to `isGraphStale` (>`GRAPH_STALE_DAYS` days).
|
|
141
|
+
* - Metadata absent (B-005): `metadata === null` — graph.json loaded but no
|
|
142
|
+
* sibling metadata.json (e.g. pre-feature `ghagga index` run). Returns
|
|
143
|
+
* EARLY with only this warning — there's nothing else to check.
|
|
144
|
+
* - Version mismatch (B-003): `metadata.graphVersion` set and differs from
|
|
145
|
+
* the current `GRAPH_VERSION` (equivalent to comparing against the loaded
|
|
146
|
+
* graph's `version`, which `validateGraph` already pins to `GRAPH_VERSION`).
|
|
147
|
+
*/
|
|
148
|
+
export declare function checkGraphStaleness(metadata: GraphMetadata | null, currentHead: string): StalenessWarning[];
|
|
149
|
+
/**
|
|
150
|
+
* Compute the set of languages present among `changedFiles` that have ZERO
|
|
151
|
+
* coverage in the loaded graph — i.e. files whose blast-radius dependent
|
|
152
|
+
* count is unreliable (shows zero, not "no dependents").
|
|
153
|
+
*
|
|
154
|
+
* Coverage is derived from the graph's ACTUAL node contents (design v2 D8,
|
|
155
|
+
* JD CRITICAL-1), not from indexer-dispatch bookkeeping:
|
|
156
|
+
* - The regex-fallback path sets `indexedLanguages: []` even though every
|
|
157
|
+
* node has a real `language` — keying off dispatch state would flag
|
|
158
|
+
* every changed file as uncovered.
|
|
159
|
+
* - `detectPresentLanguages` is root-only — a language that only exists in
|
|
160
|
+
* a subpackage (e.g. `packages/api/requirements.txt`) is never detected
|
|
161
|
+
* or skipped, so dispatch state gives no signal at all.
|
|
162
|
+
*
|
|
163
|
+
* Files whose language cannot be detected (`detectLanguage` returns
|
|
164
|
+
* `undefined` — unknown extension) are silently excluded; there is no
|
|
165
|
+
* language identity to report as uncovered.
|
|
166
|
+
*/
|
|
167
|
+
export declare function computeUncoveredLanguages(graph: DependencyGraph, changedFiles: string[]): string[];
|
|
168
|
+
export {};
|
|
58
169
|
//# sourceMappingURL=review.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"review.d.ts","sourceRoot":"","sources":["../../src/commands/review.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"review.d.ts","sourceRoot":"","sources":["../../src/commands/review.ts"],"names":[],"mappings":"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"}
|
package/dist/commands/review.js
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { execSync } from 'node:child_process';
|
|
9
9
|
import { existsSync, readFileSync } from 'node:fs';
|
|
10
|
+
import { readFile } from 'node:fs/promises';
|
|
10
11
|
import { join, resolve } from 'node:path';
|
|
11
|
-
import { buildSarif, DEFAULT_SETTINGS, EngramMemoryStorage, formatReviewComment, initializeDefaultTools, REVIEW_COMMENT_MARKER, reviewPipeline, SqliteMemoryStorage, toolRegistry, } from 'ghagga-core';
|
|
12
|
+
import { buildSarif, DEFAULT_SETTINGS, detectLanguage, EngramMemoryStorage, FilesystemGraphLoader, formatReviewComment, GRAPH_VERSION, initializeDefaultTools, isGraphStale, NullGraphLoader, PreloadedGraphLoader, parseDiffFiles, REVIEW_COMMENT_MARKER, reviewPipeline, SqliteMemoryStorage, toolRegistry, } from 'ghagga-core';
|
|
12
13
|
import { GitHubForgeAdapter, GitLabForgeAdapter, StaticTokenProvider, } from 'ghagga-forge';
|
|
13
14
|
import { createCliGitHubClientPort, resolveGitHubRepoId } from '../lib/cli-github-client-port.js';
|
|
14
15
|
import { createCliGitLabClientPort, resolveGitLabApiBase, resolveGitLabProjectId, } from '../lib/cli-gitlab-client-port.js';
|
|
@@ -94,6 +95,89 @@ export async function reviewCommand(targetPath, options) {
|
|
|
94
95
|
const fileConfig = loadConfigFile(repoPath, options.config);
|
|
95
96
|
// Step 3: Merge settings (CLI options take priority over config file)
|
|
96
97
|
const settings = mergeSettings(options, fileConfig);
|
|
98
|
+
// Step 3.4: Resolve blast-radius auto-ON (design v2 D7) — resolved
|
|
99
|
+
// against repoPath (the review target), not process.cwd() (CRITICAL-2).
|
|
100
|
+
settings.enableBlastRadius = resolveBlastRadiusEnabled(repoPath, options, fileConfig);
|
|
101
|
+
// Step 3.4.5: Wire the local FilesystemGraphLoader + fs fileReader when
|
|
102
|
+
// blast-radius is enabled (design v2 D3/D4), and surface staleness
|
|
103
|
+
// (D2) + partial-language-coverage (D8) as advisory warnings — never
|
|
104
|
+
// blocking. Graph-absent is a silent no-op: applyBlastRadius itself
|
|
105
|
+
// already emits its own "no graph available" pipeline event.
|
|
106
|
+
//
|
|
107
|
+
// The graph is loaded exactly ONCE here (for staleness/coverage). The
|
|
108
|
+
// already-loaded result is then wrapped in a PreloadedGraphLoader (or
|
|
109
|
+
// NullGraphLoader when absent/malformed/oversize) so the pipeline's own
|
|
110
|
+
// `applyBlastRadius` step — which unconditionally calls
|
|
111
|
+
// `graphLoader.load()` — reads it from memory instead of re-reading
|
|
112
|
+
// graph.json (up to 20MB) and re-emitting the oversize/malformed warning
|
|
113
|
+
// a second time (R4-001).
|
|
114
|
+
let graphLoader;
|
|
115
|
+
let fileReader;
|
|
116
|
+
// Populated below (reusing the SAME `git rev-parse HEAD` already
|
|
117
|
+
// computed for staleness checking) and threaded into `ReviewInput` as
|
|
118
|
+
// `currentHead` — the Pillar 0 freshness precondition for
|
|
119
|
+
// symbol-precise blast-radius narrowing (scip-symbol-exclusion D0).
|
|
120
|
+
let currentHead;
|
|
121
|
+
if (settings.enableBlastRadius) {
|
|
122
|
+
const filesystemGraphLoader = new FilesystemGraphLoader(repoPath, {
|
|
123
|
+
onOversize: (bytes, max) => {
|
|
124
|
+
tui.log.warn(`⚠️ Dependency graph exceeds ${max} bytes (actual: ${bytes}) — ignoring, blast-radius disabled for this run.`);
|
|
125
|
+
},
|
|
126
|
+
onMalformed: (reason) => {
|
|
127
|
+
tui.log.warn(`⚠️ Dependency graph is stale/corrupt — ignoring (${reason}).`);
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
fileReader = async (filePath) => {
|
|
131
|
+
try {
|
|
132
|
+
return await readFile(join(repoPath, filePath), 'utf-8');
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
try {
|
|
139
|
+
const graph = await filesystemGraphLoader.load();
|
|
140
|
+
if (graph) {
|
|
141
|
+
const metadata = await filesystemGraphLoader.loadMetadata();
|
|
142
|
+
try {
|
|
143
|
+
currentHead = execSync('git rev-parse HEAD', {
|
|
144
|
+
cwd: repoPath,
|
|
145
|
+
encoding: 'utf-8',
|
|
146
|
+
}).trim();
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
currentHead = '';
|
|
150
|
+
}
|
|
151
|
+
for (const warning of checkGraphStaleness(metadata, currentHead)) {
|
|
152
|
+
tui.log.warn(`⚠️ ${warning.message}`);
|
|
153
|
+
}
|
|
154
|
+
const changedFiles = parseDiffFiles(diff).map((f) => f.path);
|
|
155
|
+
const uncoveredLanguages = computeUncoveredLanguages(graph, changedFiles);
|
|
156
|
+
if (uncoveredLanguages.length > 0) {
|
|
157
|
+
tui.log.warn(`⚠️ Partial graph coverage: no indexed dependents for ${uncoveredLanguages.join(', ')} — ` +
|
|
158
|
+
'blast-radius will show 0 dependents for these files, which may be unreliable rather than accurate.');
|
|
159
|
+
}
|
|
160
|
+
// Hand the pipeline the graph we already loaded — no re-read.
|
|
161
|
+
graphLoader = new PreloadedGraphLoader(graph, metadata);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
// Absent/malformed/oversize — filesystemGraphLoader already fired
|
|
165
|
+
// its onOversize/onMalformed warning exactly once above. Hand the
|
|
166
|
+
// pipeline a loader that always resolves to null so it degrades
|
|
167
|
+
// without touching the filesystem again.
|
|
168
|
+
graphLoader = new NullGraphLoader();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
173
|
+
tui.log.warn(`⚠️ Blast-radius staleness/coverage check failed (continuing): ${message}`);
|
|
174
|
+
graphLoader = new NullGraphLoader();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Step 3.5: Build the per-voice cross-engine provider chain, if configured
|
|
178
|
+
// (BL-CLI-PROVIDER-CHAIN). Undefined when absent — regression-safe, the
|
|
179
|
+
// pipeline falls back to the flat provider/model exactly as before.
|
|
180
|
+
const providerChain = buildProviderChain(fileConfig, options.apiKey);
|
|
97
181
|
// Step 4: Show progress
|
|
98
182
|
if (!options.outputFormat) {
|
|
99
183
|
tui.intro('🤖 GHAGGA Code Review');
|
|
@@ -197,6 +281,18 @@ export async function reviewCommand(targetPath, options) {
|
|
|
197
281
|
enhance: options.enhance,
|
|
198
282
|
// --quick: disable AI review, use static analysis only
|
|
199
283
|
...(options.quick ? { aiReviewEnabled: false } : {}),
|
|
284
|
+
// Per-voice cross-engine routing (BL-CLI-PROVIDER-CHAIN) — undefined
|
|
285
|
+
// when `.ghagga.json` has no providerChain, unchanged flat-provider path.
|
|
286
|
+
...(providerChain ? { providerChain } : {}),
|
|
287
|
+
// Blast-radius (design v2) — undefined when disabled/no graph, matching
|
|
288
|
+
// the pre-existing `applyBlastRadius` gate (`enableBlastRadius && graphLoader`).
|
|
289
|
+
...(graphLoader ? { graphLoader } : {}),
|
|
290
|
+
...(fileReader ? { fileReader } : {}),
|
|
291
|
+
// Pillar 0 freshness precondition (scip-symbol-exclusion D0) — reuses
|
|
292
|
+
// the SAME `git rev-parse HEAD` already computed above for staleness
|
|
293
|
+
// checking. Undefined when blast-radius is disabled or HEAD couldn't
|
|
294
|
+
// be resolved (non-git repo) — narrowing degrades to a no-op.
|
|
295
|
+
...(currentHead ? { currentHead } : {}),
|
|
200
296
|
});
|
|
201
297
|
// Step 5.5: Persist memory to disk
|
|
202
298
|
await memoryStorage?.close();
|
|
@@ -544,6 +640,39 @@ function getGitDiff(repoPath) {
|
|
|
544
640
|
}
|
|
545
641
|
}
|
|
546
642
|
// ─── Config File ────────────────────────────────────────────────
|
|
643
|
+
/**
|
|
644
|
+
* Validate the shape of a `.ghagga.json` `providerChain` array. Throws a
|
|
645
|
+
* descriptive `Error` on the first invalid entry — provider misrouting is
|
|
646
|
+
* high-stakes (silently reviewing with the wrong engine), so this fails
|
|
647
|
+
* loudly rather than the soft-warn-and-continue behavior used for a
|
|
648
|
+
* generally malformed config file.
|
|
649
|
+
*/
|
|
650
|
+
function validateProviderChain(chain) {
|
|
651
|
+
if (!Array.isArray(chain)) {
|
|
652
|
+
throw new Error('providerChain must be an array');
|
|
653
|
+
}
|
|
654
|
+
chain.forEach((rawEntry, index) => {
|
|
655
|
+
if (typeof rawEntry !== 'object' || rawEntry === null) {
|
|
656
|
+
throw new Error(`providerChain[${index}] must be an object`);
|
|
657
|
+
}
|
|
658
|
+
const entry = rawEntry;
|
|
659
|
+
if (entry.provider !== 'gateway') {
|
|
660
|
+
throw new Error(`providerChain[${index}].provider must be "gateway" (got ${JSON.stringify(entry.provider)})`);
|
|
661
|
+
}
|
|
662
|
+
if (typeof entry.model !== 'string' || entry.model.length === 0) {
|
|
663
|
+
throw new Error(`providerChain[${index}].model must be a non-empty string`);
|
|
664
|
+
}
|
|
665
|
+
if (entry.targetProvider !== undefined && typeof entry.targetProvider !== 'string') {
|
|
666
|
+
throw new Error(`providerChain[${index}].targetProvider must be a string`);
|
|
667
|
+
}
|
|
668
|
+
if (entry.gatewayUrl !== undefined && typeof entry.gatewayUrl !== 'string') {
|
|
669
|
+
throw new Error(`providerChain[${index}].gatewayUrl must be a string`);
|
|
670
|
+
}
|
|
671
|
+
if (entry.cliModel !== undefined && typeof entry.cliModel !== 'string') {
|
|
672
|
+
throw new Error(`providerChain[${index}].cliModel must be a string`);
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
}
|
|
547
676
|
/**
|
|
548
677
|
* Load and parse an optional .ghagga.json config file.
|
|
549
678
|
*/
|
|
@@ -552,15 +681,148 @@ function loadConfigFile(repoPath, configPath) {
|
|
|
552
681
|
if (!existsSync(filePath)) {
|
|
553
682
|
return {};
|
|
554
683
|
}
|
|
684
|
+
let parsed;
|
|
555
685
|
try {
|
|
556
686
|
const raw = readFileSync(filePath, 'utf-8');
|
|
557
|
-
|
|
687
|
+
parsed = JSON.parse(raw);
|
|
558
688
|
}
|
|
559
689
|
catch (error) {
|
|
560
690
|
const message = error instanceof Error ? error.message : String(error);
|
|
561
691
|
tui.log.warn(`⚠️ Could not parse config file: ${message}`);
|
|
562
692
|
return {};
|
|
563
693
|
}
|
|
694
|
+
if (parsed.providerChain !== undefined) {
|
|
695
|
+
try {
|
|
696
|
+
validateProviderChain(parsed.providerChain);
|
|
697
|
+
}
|
|
698
|
+
catch (error) {
|
|
699
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
700
|
+
tui.log.error(`❌ Invalid .ghagga.json providerChain: ${message}`);
|
|
701
|
+
process.exit(1);
|
|
702
|
+
return {};
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
return parsed;
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Map a validated `.ghagga.json` providerChain into `ghagga-core`'s
|
|
709
|
+
* `ProviderChainEntry[]` for `ReviewInput.providerChain` — the piece that
|
|
710
|
+
* closes the CLI's gap in per-voice cross-engine routing (consensus/fan-out
|
|
711
|
+
* modes round-robin each voice across these gateway entries, see
|
|
712
|
+
* `resolveGenerateTextFns` in `packages/core/src/pipeline/providers.ts`).
|
|
713
|
+
*
|
|
714
|
+
* `apiKey` is NEVER read from the config file (never stored in plaintext) —
|
|
715
|
+
* it is filled at runtime from the resolved gateway token (`options.apiKey`,
|
|
716
|
+
* itself CLI flag > env var > stored login token, see `apps/cli/src/index.ts`).
|
|
717
|
+
*/
|
|
718
|
+
function buildProviderChain(fileConfig, apiKey) {
|
|
719
|
+
if (!fileConfig.providerChain || fileConfig.providerChain.length === 0) {
|
|
720
|
+
return undefined;
|
|
721
|
+
}
|
|
722
|
+
return fileConfig.providerChain.map((entry) => ({
|
|
723
|
+
provider: entry.provider,
|
|
724
|
+
model: entry.model,
|
|
725
|
+
apiKey: apiKey ?? '',
|
|
726
|
+
...(entry.targetProvider !== undefined ? { targetProvider: entry.targetProvider } : {}),
|
|
727
|
+
...(entry.gatewayUrl !== undefined ? { gatewayUrl: entry.gatewayUrl } : {}),
|
|
728
|
+
...(entry.cliModel !== undefined ? { cliModel: entry.cliModel } : {}),
|
|
729
|
+
}));
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Resolve whether blast-radius should be enabled for this review run.
|
|
733
|
+
*
|
|
734
|
+
* Precedence (highest to lowest):
|
|
735
|
+
* 1. `--no-blast-radius` (options.blastRadius === false) → OFF
|
|
736
|
+
* 2. `.ghagga.json` `enableBlastRadius` (explicit true/false) → that value
|
|
737
|
+
* 3. Auto-ON when `<repoPath>/.ghagga/graph.json` exists
|
|
738
|
+
* 4. OFF (no graph, no override)
|
|
739
|
+
*
|
|
740
|
+
* CRITICAL-2 (JD B-001): the existence check is resolved against `repoPath`
|
|
741
|
+
* — the review target — NOT `process.cwd()`, so `ghagga review <other-dir>`
|
|
742
|
+
* from an unrelated cwd still auto-detects correctly.
|
|
743
|
+
*/
|
|
744
|
+
export function resolveBlastRadiusEnabled(repoPath, options, fileConfig) {
|
|
745
|
+
if (options.blastRadius === false)
|
|
746
|
+
return false;
|
|
747
|
+
if (fileConfig.enableBlastRadius === false)
|
|
748
|
+
return false;
|
|
749
|
+
if (fileConfig.enableBlastRadius === true)
|
|
750
|
+
return true;
|
|
751
|
+
return existsSync(join(repoPath, '.ghagga', 'graph.json'));
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Detect staleness/coverage-verification signals for the loaded graph.
|
|
755
|
+
* ALWAYS warn-and-continue (design v2 D2) — never blocks the review.
|
|
756
|
+
*
|
|
757
|
+
* - HEAD mismatch: `metadata.lastIndexedCommit` non-empty and differs from
|
|
758
|
+
* `currentHead`. An EMPTY `currentHead` (non-git/detached checkout) skips
|
|
759
|
+
* this specific check — there's nothing meaningful to compare against.
|
|
760
|
+
* - Age: delegates to `isGraphStale` (>`GRAPH_STALE_DAYS` days).
|
|
761
|
+
* - Metadata absent (B-005): `metadata === null` — graph.json loaded but no
|
|
762
|
+
* sibling metadata.json (e.g. pre-feature `ghagga index` run). Returns
|
|
763
|
+
* EARLY with only this warning — there's nothing else to check.
|
|
764
|
+
* - Version mismatch (B-003): `metadata.graphVersion` set and differs from
|
|
765
|
+
* the current `GRAPH_VERSION` (equivalent to comparing against the loaded
|
|
766
|
+
* graph's `version`, which `validateGraph` already pins to `GRAPH_VERSION`).
|
|
767
|
+
*/
|
|
768
|
+
export function checkGraphStaleness(metadata, currentHead) {
|
|
769
|
+
if (!metadata) {
|
|
770
|
+
return [
|
|
771
|
+
{
|
|
772
|
+
kind: 'no-metadata',
|
|
773
|
+
message: 'Graph loaded but no metadata found — staleness cannot be verified; re-run `ghagga index`.',
|
|
774
|
+
},
|
|
775
|
+
];
|
|
776
|
+
}
|
|
777
|
+
const warnings = [];
|
|
778
|
+
if (currentHead && metadata.lastIndexedCommit && metadata.lastIndexedCommit !== currentHead) {
|
|
779
|
+
warnings.push({
|
|
780
|
+
kind: 'head-mismatch',
|
|
781
|
+
message: `Dependency graph indexed at ${metadata.lastIndexedCommit}, HEAD is ${currentHead} — run \`ghagga index\` to refresh.`,
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
if (isGraphStale(metadata)) {
|
|
785
|
+
warnings.push({
|
|
786
|
+
kind: 'stale-age',
|
|
787
|
+
message: `Dependency graph is stale (last indexed: ${metadata.lastIndexedAt}) — run \`ghagga index\` to refresh.`,
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
if (metadata.graphVersion !== undefined && metadata.graphVersion !== GRAPH_VERSION) {
|
|
791
|
+
warnings.push({
|
|
792
|
+
kind: 'version-mismatch',
|
|
793
|
+
message: `Graph metadata schema version (${metadata.graphVersion}) does not match the current graph version (${GRAPH_VERSION}) — run \`ghagga index\` to refresh.`,
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
return warnings;
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Compute the set of languages present among `changedFiles` that have ZERO
|
|
800
|
+
* coverage in the loaded graph — i.e. files whose blast-radius dependent
|
|
801
|
+
* count is unreliable (shows zero, not "no dependents").
|
|
802
|
+
*
|
|
803
|
+
* Coverage is derived from the graph's ACTUAL node contents (design v2 D8,
|
|
804
|
+
* JD CRITICAL-1), not from indexer-dispatch bookkeeping:
|
|
805
|
+
* - The regex-fallback path sets `indexedLanguages: []` even though every
|
|
806
|
+
* node has a real `language` — keying off dispatch state would flag
|
|
807
|
+
* every changed file as uncovered.
|
|
808
|
+
* - `detectPresentLanguages` is root-only — a language that only exists in
|
|
809
|
+
* a subpackage (e.g. `packages/api/requirements.txt`) is never detected
|
|
810
|
+
* or skipped, so dispatch state gives no signal at all.
|
|
811
|
+
*
|
|
812
|
+
* Files whose language cannot be detected (`detectLanguage` returns
|
|
813
|
+
* `undefined` — unknown extension) are silently excluded; there is no
|
|
814
|
+
* language identity to report as uncovered.
|
|
815
|
+
*/
|
|
816
|
+
export function computeUncoveredLanguages(graph, changedFiles) {
|
|
817
|
+
const covered = new Set(Object.values(graph.nodes).map((node) => node.language));
|
|
818
|
+
const uncovered = new Set();
|
|
819
|
+
for (const filePath of changedFiles) {
|
|
820
|
+
const language = detectLanguage(filePath);
|
|
821
|
+
if (language && !covered.has(language)) {
|
|
822
|
+
uncovered.add(language);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
return [...uncovered];
|
|
564
826
|
}
|
|
565
827
|
// ─── Settings Merge ─────────────────────────────────────────────
|
|
566
828
|
/**
|