@rigour-labs/core 6.3.1 → 6.4.1
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/README.md +1 -1
- package/dist/deep/changed-functions.d.ts +15 -0
- package/dist/deep/changed-functions.js +34 -0
- package/dist/deep/code-pass.d.ts +10 -0
- package/dist/deep/code-pass.js +37 -2
- package/dist/deep/code-pass.test.d.ts +1 -0
- package/dist/deep/code-pass.test.js +69 -0
- package/dist/deep/code-review-prompt.d.ts +1 -1
- package/dist/deep/code-review-prompt.js +3 -2
- package/dist/deep/diff-tests/calls.d.ts +8 -0
- package/dist/deep/diff-tests/calls.js +85 -0
- package/dist/deep/diff-tests/calls.test.d.ts +1 -0
- package/dist/deep/diff-tests/calls.test.js +33 -0
- package/dist/deep/diff-tests/run.d.ts +21 -0
- package/dist/deep/diff-tests/run.js +178 -0
- package/dist/deep/diff-tests/run.test.d.ts +1 -0
- package/dist/deep/diff-tests/run.test.js +58 -0
- package/dist/deep/reference-pack.d.ts +19 -0
- package/dist/deep/reference-pack.js +101 -0
- package/dist/deep/reference-pack.test.d.ts +1 -0
- package/dist/deep/reference-pack.test.js +46 -0
- package/dist/deep/review-context-export.d.ts +11 -0
- package/dist/deep/review-context-export.js +31 -0
- package/dist/deep/review-context-export.test.d.ts +1 -0
- package/dist/deep/review-context-export.test.js +32 -0
- package/dist/deep/self-check.d.ts +21 -0
- package/dist/deep/self-check.js +49 -0
- package/dist/firewall/trusted-control.d.ts +1 -2
- package/dist/gates/deep-analysis.d.ts +2 -0
- package/dist/gates/deep-analysis.js +12 -0
- package/dist/gates/deep-runner.js +2 -1
- package/dist/gates/hallucinated-imports/js-resolver.d.ts +7 -0
- package/dist/gates/hallucinated-imports/js-resolver.js +17 -1
- package/dist/gates/hallucinated-imports-sveltekit.test.js +24 -0
- package/dist/gates/semantic-bugs.js +4 -3
- package/dist/gates/semantic-bugs.test.js +11 -5
- package/dist/hooks/checker.d.ts +1 -2
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/stop-review.d.ts +25 -0
- package/dist/hooks/stop-review.js +31 -0
- package/dist/index.d.ts +26 -2
- package/dist/index.js +10 -6
- package/dist/inference/index.d.ts +1 -1
- package/dist/inference/index.js +3 -5
- package/dist/inference/local-model.test.d.ts +1 -0
- package/dist/inference/local-model.test.js +23 -0
- package/dist/inference/sidecar-provider.d.ts +10 -1
- package/dist/inference/sidecar-provider.js +28 -6
- package/dist/inference/types.d.ts +8 -1
- package/dist/inference/types.js +14 -0
- package/dist/inference/types.test.d.ts +1 -0
- package/dist/inference/types.test.js +19 -0
- package/dist/review/agent-fixes.d.ts +18 -0
- package/dist/review/agent-fixes.js +118 -0
- package/dist/review/agent-fixes.test.d.ts +1 -0
- package/dist/review/agent-fixes.test.js +32 -0
- package/dist/review/changed-lines.d.ts +19 -0
- package/dist/review/changed-lines.js +22 -0
- package/dist/review/diff-test-findings.d.ts +3 -0
- package/dist/review/diff-test-findings.js +41 -0
- package/dist/review/effectiveness.d.ts +34 -0
- package/dist/review/effectiveness.js +114 -0
- package/dist/review/effectiveness.test.d.ts +1 -0
- package/dist/review/effectiveness.test.js +42 -0
- package/dist/review/git-diff.d.ts +9 -0
- package/dist/review/git-diff.js +42 -0
- package/dist/review/review.d.ts +38 -0
- package/dist/review/review.js +52 -0
- package/dist/review/review.test.d.ts +1 -0
- package/dist/review/review.test.js +87 -0
- package/dist/semantic/benchmark.js +2 -2
- package/dist/semantic/benchmark.test.js +5 -1
- package/dist/semantic/engine.d.ts +3 -0
- package/dist/semantic/engine.js +8 -2
- package/dist/semantic/failure-handling.d.ts +12 -0
- package/dist/semantic/failure-handling.js +41 -0
- package/dist/semantic/intent/parallel-reads.js +2 -30
- package/dist/semantic/learn/learn.d.ts +2 -0
- package/dist/semantic/learn/learn.js +18 -0
- package/dist/semantic/project-facts.d.ts +25 -0
- package/dist/semantic/project-facts.js +104 -0
- package/dist/semantic/project-facts.test.d.ts +1 -0
- package/dist/semantic/project-facts.test.js +45 -0
- package/dist/semantic/rules/ecosystem/deps-codegen-undeclared-import.d.ts +2 -0
- package/dist/semantic/rules/ecosystem/deps-codegen-undeclared-import.js +71 -0
- package/dist/semantic/rules/ecosystem/deps-internal-symbol.d.ts +2 -0
- package/dist/semantic/rules/ecosystem/deps-internal-symbol.js +36 -0
- package/dist/semantic/rules/ecosystem/dom-self-query-in-component.d.ts +2 -0
- package/dist/semantic/rules/ecosystem/dom-self-query-in-component.js +60 -0
- package/dist/semantic/rules/ecosystem/env-bare-browser-global.d.ts +2 -0
- package/dist/semantic/rules/ecosystem/env-bare-browser-global.js +57 -0
- package/dist/semantic/rules/ecosystem/exports-condition-parity.d.ts +2 -0
- package/dist/semantic/rules/ecosystem/exports-condition-parity.js +57 -0
- package/dist/semantic/rules/ecosystem/exports-forgotten-type.d.ts +2 -0
- package/dist/semantic/rules/ecosystem/exports-forgotten-type.js +128 -0
- package/dist/semantic/rules/ecosystem/imports-barrel-cycle.d.ts +2 -0
- package/dist/semantic/rules/ecosystem/imports-barrel-cycle.js +78 -0
- package/dist/semantic/rules/ecosystem/index.d.ts +15 -0
- package/dist/semantic/rules/ecosystem/index.js +22 -0
- package/dist/semantic/rules/ecosystem/paths-unnormalized-module-key.d.ts +2 -0
- package/dist/semantic/rules/ecosystem/paths-unnormalized-module-key.js +87 -0
- package/dist/semantic/rules/ecosystem/react-inline-html-object.d.ts +2 -0
- package/dist/semantic/rules/ecosystem/react-inline-html-object.js +30 -0
- package/dist/semantic/rules/ecosystem/shared.d.ts +18 -0
- package/dist/semantic/rules/ecosystem/shared.js +43 -0
- package/dist/semantic/rules/ecosystem/solid-jsx-and-conditional.d.ts +2 -0
- package/dist/semantic/rules/ecosystem/solid-jsx-and-conditional.js +36 -0
- package/dist/semantic/rules/ecosystem/ts-internal-in-public-signature.d.ts +2 -0
- package/dist/semantic/rules/ecosystem/ts-internal-in-public-signature.js +66 -0
- package/dist/semantic/rules/ecosystem/vite-rollup-only-hook-field.d.ts +2 -0
- package/dist/semantic/rules/ecosystem/vite-rollup-only-hook-field.js +31 -0
- package/dist/semantic/rules/ecosystem/vue-static-computed.d.ts +2 -0
- package/dist/semantic/rules/ecosystem/vue-static-computed.js +51 -0
- package/dist/semantic/sites/call-sites.d.ts +25 -0
- package/dist/semantic/sites/call-sites.js +69 -0
- package/dist/semantic/sites/call-sites.test.d.ts +1 -0
- package/dist/semantic/sites/call-sites.test.js +46 -0
- package/dist/semantic/sites/export.d.ts +2 -0
- package/dist/semantic/sites/export.js +22 -0
- package/dist/semantic/types.d.ts +4 -0
- package/dist/services/engineering-knowledge-graph.d.ts +1 -2
- package/dist/templates/universal-config.js +2 -1
- package/dist/types/index.d.ts +31 -8
- package/dist/types/index.js +4 -2
- package/dist/utils/diff-removed.test.d.ts +1 -0
- package/dist/utils/diff-removed.test.js +29 -0
- package/dist/utils/diff.d.ts +12 -0
- package/dist/utils/diff.js +26 -0
- package/package.json +6 -6
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Is the review loop working? Computed from the local event log
|
|
3
|
+
* (`.rigour/events.jsonl`, written by the MCP server and the stop hook), so
|
|
4
|
+
* nothing leaves the machine.
|
|
5
|
+
*
|
|
6
|
+
* - a finding is resolved when a later review no longer reports it (same rule, same file);
|
|
7
|
+
* - a blocked stop is followed through when the same session later stops cleanly;
|
|
8
|
+
* - a self-review is a rigour_review call made before an attempt to stop.
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { randomUUID } from 'crypto';
|
|
13
|
+
const EVENTS = path.join('.rigour', 'events.jsonl');
|
|
14
|
+
export function readAgentEvents(cwd) {
|
|
15
|
+
let text = '';
|
|
16
|
+
try {
|
|
17
|
+
text = fs.readFileSync(path.join(cwd, EVENTS), 'utf8');
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
return text.split('\n').filter(Boolean).flatMap(line => {
|
|
23
|
+
try {
|
|
24
|
+
return [JSON.parse(line)];
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/** Append an event in the same format the MCP server writes; never throws. */
|
|
32
|
+
export function appendAgentEvent(cwd, event) {
|
|
33
|
+
try {
|
|
34
|
+
fs.mkdirSync(path.join(cwd, '.rigour'), { recursive: true });
|
|
35
|
+
fs.appendFileSync(path.join(cwd, EVENTS), JSON.stringify({ id: randomUUID(), timestamp: new Date().toISOString(), ...event }) + '\n');
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// The event log is best-effort; it must never break a hook or a tool call.
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** `rule:file` keys, so a finding that moved lines is still the same finding. */
|
|
42
|
+
export function findingKeys(findings) {
|
|
43
|
+
return findings.map(f => `${f.id ?? '?'}:${f.file ?? '?'}`);
|
|
44
|
+
}
|
|
45
|
+
export function computeEffectiveness(events) {
|
|
46
|
+
const stats = {
|
|
47
|
+
toolCalls: {}, reviews: 0, failedReviews: 0, findingsReported: 0, findingsResolved: 0,
|
|
48
|
+
stopChecks: 0, stopBlocks: 0, blockedStopsFollowedThrough: 0, stopsWithSelfReview: 0,
|
|
49
|
+
};
|
|
50
|
+
const open = new Set();
|
|
51
|
+
const blockedSessions = new Set();
|
|
52
|
+
let reviewedSinceStop = false;
|
|
53
|
+
for (const event of events) {
|
|
54
|
+
if (event.type === 'tool_call' && event.tool) {
|
|
55
|
+
stats.toolCalls[event.tool] = (stats.toolCalls[event.tool] ?? 0) + 1;
|
|
56
|
+
if (event.tool === 'rigour_review')
|
|
57
|
+
reviewedSinceStop = true;
|
|
58
|
+
}
|
|
59
|
+
else if (event.type === 'tool_response' && event.tool === 'rigour_review') {
|
|
60
|
+
countReview(stats, open, reviewKeys(event));
|
|
61
|
+
}
|
|
62
|
+
else if (event.type === 'stop_review') {
|
|
63
|
+
countStop(stats, blockedSessions, event, reviewedSinceStop);
|
|
64
|
+
reviewedSinceStop = false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return stats;
|
|
68
|
+
}
|
|
69
|
+
function countReview(stats, open, keys) {
|
|
70
|
+
if (keys === null)
|
|
71
|
+
return;
|
|
72
|
+
stats.reviews++;
|
|
73
|
+
if (keys.length)
|
|
74
|
+
stats.failedReviews++;
|
|
75
|
+
const now = new Set(keys);
|
|
76
|
+
for (const key of [...open]) {
|
|
77
|
+
if (!now.has(key)) {
|
|
78
|
+
open.delete(key);
|
|
79
|
+
stats.findingsResolved++;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
for (const key of now) {
|
|
83
|
+
if (!open.has(key)) {
|
|
84
|
+
open.add(key);
|
|
85
|
+
stats.findingsReported++;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function countStop(stats, blockedSessions, event, selfReviewed) {
|
|
90
|
+
const session = event.session ?? 'default';
|
|
91
|
+
stats.stopChecks++;
|
|
92
|
+
if (selfReviewed)
|
|
93
|
+
stats.stopsWithSelfReview++;
|
|
94
|
+
if (event.blocked) {
|
|
95
|
+
stats.stopBlocks++;
|
|
96
|
+
blockedSessions.add(session);
|
|
97
|
+
}
|
|
98
|
+
else if (blockedSessions.delete(session)) {
|
|
99
|
+
stats.blockedStopsFollowedThrough++;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** Finding keys from a rigour_review response, or null when it is not a review result. */
|
|
103
|
+
function reviewKeys(event) {
|
|
104
|
+
const text = event.content?.find(c => c.type === 'text')?.text;
|
|
105
|
+
if (!text)
|
|
106
|
+
return null;
|
|
107
|
+
try {
|
|
108
|
+
const body = JSON.parse(text);
|
|
109
|
+
return Array.isArray(body.failures) ? findingKeys(body.failures) : null;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import { appendAgentEvent, computeEffectiveness, readAgentEvents } from './effectiveness.js';
|
|
6
|
+
const review = (failures) => [
|
|
7
|
+
{ type: 'tool_call', tool: 'rigour_review' },
|
|
8
|
+
{ type: 'tool_response', tool: 'rigour_review', content: [{ type: 'text', text: JSON.stringify({ status: failures.length ? 'FAIL' : 'PASS', failures }) }] },
|
|
9
|
+
];
|
|
10
|
+
describe('computeEffectiveness', () => {
|
|
11
|
+
it('counts reviews, findings resolved by later reviews, and tool calls', () => {
|
|
12
|
+
const events = [
|
|
13
|
+
{ type: 'tool_call', tool: 'rigour_recall' },
|
|
14
|
+
...review([{ id: 'semantic-bugs', file: 'a.ts' }, { id: 'promise-safety', file: 'b.ts' }]),
|
|
15
|
+
...review([{ id: 'promise-safety', file: 'b.ts' }]),
|
|
16
|
+
...review([]),
|
|
17
|
+
];
|
|
18
|
+
expect(computeEffectiveness(events)).toMatchObject({
|
|
19
|
+
reviews: 3, failedReviews: 2, findingsReported: 2, findingsResolved: 2,
|
|
20
|
+
toolCalls: { rigour_recall: 1, rigour_review: 3 },
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
it('follows a blocked stop through to a clean stop in the same session, and counts self-reviews', () => {
|
|
24
|
+
const events = [
|
|
25
|
+
{ type: 'stop_review', session: 's1', blocked: true },
|
|
26
|
+
...review([]),
|
|
27
|
+
{ type: 'stop_review', session: 's1', blocked: false },
|
|
28
|
+
{ type: 'stop_review', session: 's2', blocked: false },
|
|
29
|
+
];
|
|
30
|
+
expect(computeEffectiveness(events)).toMatchObject({
|
|
31
|
+
stopChecks: 3, stopBlocks: 1, blockedStopsFollowedThrough: 1, stopsWithSelfReview: 1,
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
it('reads the log it writes and skips malformed lines', () => {
|
|
35
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'effectiveness-'));
|
|
36
|
+
appendAgentEvent(dir, { type: 'stop_review', blocked: true });
|
|
37
|
+
fs.appendFileSync(path.join(dir, '.rigour', 'events.jsonl'), 'not json\n');
|
|
38
|
+
expect(readAgentEvents(dir).map(e => e.type)).toEqual(['stop_review']);
|
|
39
|
+
expect(readAgentEvents(path.join(dir, 'missing'))).toEqual([]);
|
|
40
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The diff to review, taken from git so callers do not have to build one.
|
|
3
|
+
*
|
|
4
|
+
* - working: uncommitted changes against HEAD, staged or not, plus new files
|
|
5
|
+
* git does not track yet (what an agent has just written).
|
|
6
|
+
* - base: everything on this branch since it left `base` (the merge-base),
|
|
7
|
+
* including uncommitted changes (what a pull request contains).
|
|
8
|
+
*/
|
|
9
|
+
import { spawnSync } from 'child_process';
|
|
10
|
+
export class GitDiffError extends Error {
|
|
11
|
+
}
|
|
12
|
+
export function diffFromGit(cwd, source = { mode: 'working' }) {
|
|
13
|
+
if (source.mode === 'base') {
|
|
14
|
+
const mergeBase = git(cwd, ['merge-base', source.base, 'HEAD']).trim();
|
|
15
|
+
return git(cwd, ['diff', '--no-color', '--no-ext-diff', mergeBase]);
|
|
16
|
+
}
|
|
17
|
+
const tracked = hasHead(cwd) ? git(cwd, ['diff', '--no-color', '--no-ext-diff', 'HEAD']) : '';
|
|
18
|
+
return tracked + untrackedFiles(cwd).map(file => newFileDiff(cwd, file)).join('');
|
|
19
|
+
}
|
|
20
|
+
function untrackedFiles(cwd) {
|
|
21
|
+
return git(cwd, ['ls-files', '--others', '--exclude-standard', '-z']).split('\0').filter(Boolean);
|
|
22
|
+
}
|
|
23
|
+
/** `git diff --no-index` exits 1 when the files differ, which for a new file is always. */
|
|
24
|
+
function newFileDiff(cwd, file) {
|
|
25
|
+
const result = spawnSync('git', ['diff', '--no-color', '--no-ext-diff', '--no-index', '--', '/dev/null', file], {
|
|
26
|
+
cwd, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024,
|
|
27
|
+
});
|
|
28
|
+
if (result.status !== 0 && result.status !== 1)
|
|
29
|
+
throw new GitDiffError(`git diff for new file ${file} failed: ${result.stderr.trim()}`);
|
|
30
|
+
return result.stdout;
|
|
31
|
+
}
|
|
32
|
+
function hasHead(cwd) {
|
|
33
|
+
return spawnSync('git', ['rev-parse', '--verify', '--quiet', 'HEAD'], { cwd }).status === 0;
|
|
34
|
+
}
|
|
35
|
+
function git(cwd, args) {
|
|
36
|
+
const result = spawnSync('git', args, { cwd, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
37
|
+
if (result.error)
|
|
38
|
+
throw new GitDiffError(`git is not available: ${result.error.message}`);
|
|
39
|
+
if (result.status !== 0)
|
|
40
|
+
throw new GitDiffError(`git ${args.join(' ')} failed: ${result.stderr.trim()}`);
|
|
41
|
+
return result.stdout;
|
|
42
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Config, DeepOptions, Failure, Report } from '../types/index.js';
|
|
2
|
+
import { type DiffSource } from './git-diff.js';
|
|
3
|
+
export interface ReviewInput {
|
|
4
|
+
cwd: string;
|
|
5
|
+
config: Config;
|
|
6
|
+
/** A unified diff; when omitted, it is taken from git (`source`). */
|
|
7
|
+
diff?: string;
|
|
8
|
+
source?: DiffSource;
|
|
9
|
+
/** Review exactly these files instead of the ones the diff touches. */
|
|
10
|
+
files?: string[];
|
|
11
|
+
/** Run changed functions before and after the change (needs deep, on the max or a cloud tier). */
|
|
12
|
+
diffTests?: boolean;
|
|
13
|
+
/** Deep analysis; `focusLines` and `removedLines` are filled from the diff. */
|
|
14
|
+
deep?: Omit<DeepOptions, 'focusLines' | 'removedLines'>;
|
|
15
|
+
}
|
|
16
|
+
export interface ReviewResult {
|
|
17
|
+
status: 'PASS' | 'FAIL' | 'ERROR';
|
|
18
|
+
findings: Failure[];
|
|
19
|
+
fileFindings: Failure[];
|
|
20
|
+
unlocated: number;
|
|
21
|
+
excludedOutsideChangedLines: number;
|
|
22
|
+
changedLines: Record<string, Set<number>>;
|
|
23
|
+
report: Report | null;
|
|
24
|
+
deepError?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface ReviewFinding {
|
|
27
|
+
id: string;
|
|
28
|
+
gate: string;
|
|
29
|
+
severity: string;
|
|
30
|
+
provenance: string;
|
|
31
|
+
message: string;
|
|
32
|
+
file: string;
|
|
33
|
+
line: number | null;
|
|
34
|
+
suggestion?: string;
|
|
35
|
+
}
|
|
36
|
+
export declare function reviewChange(input: ReviewInput): Promise<ReviewResult>;
|
|
37
|
+
/** The JSON shape of a finding, shared by the CLI's --json and the MCP tool. */
|
|
38
|
+
export declare function toReviewFinding(failure: Failure): ReviewFinding;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* reviewChange: the one review engine behind `rigour review`, the MCP
|
|
3
|
+
* `rigour_review` tool and the GitHub integration.
|
|
4
|
+
*
|
|
5
|
+
* Runs the configured gates on the files a change touches and keeps what
|
|
6
|
+
* belongs to the change (see changed-lines.ts). A deep run that was asked for
|
|
7
|
+
* but could not analyze anything makes the result ERROR, never a clean PASS.
|
|
8
|
+
*/
|
|
9
|
+
import { GateRunner } from '../gates/runner.js';
|
|
10
|
+
import { changedLinesByFile, parseDiff, removedByFile } from '../utils/diff.js';
|
|
11
|
+
import { normalizeScopePatterns } from '../utils/scope.js';
|
|
12
|
+
import { deepAnalysisError } from '../utils/deep-status.js';
|
|
13
|
+
import { splitByChangedLines } from './changed-lines.js';
|
|
14
|
+
import { diffFromGit } from './git-diff.js';
|
|
15
|
+
import { diffTestFailures } from './diff-test-findings.js';
|
|
16
|
+
export async function reviewChange(input) {
|
|
17
|
+
const diff = input.diff ?? diffFromGit(input.cwd, input.source);
|
|
18
|
+
const changedLines = parseDiff(diff);
|
|
19
|
+
const targets = input.files?.length ? input.files : Object.keys(changedLines);
|
|
20
|
+
if (targets.length === 0) {
|
|
21
|
+
return { status: 'PASS', findings: [], fileFindings: [], unlocated: 0, excludedOutsideChangedLines: 0, changedLines, report: null };
|
|
22
|
+
}
|
|
23
|
+
const deep = input.deep ? { ...input.deep, focusLines: changedLinesByFile(changedLines), removedLines: removedByFile(diff) } : undefined;
|
|
24
|
+
const report = await new GateRunner(input.config).run(input.cwd, await normalizeScopePatterns(input.cwd, targets), deep);
|
|
25
|
+
if (input.diffTests && deep)
|
|
26
|
+
report.failures.push(...await diffTestFailures(input.cwd, input.source, deep));
|
|
27
|
+
const split = splitByChangedLines(report.failures, changedLines);
|
|
28
|
+
const deepError = deepAnalysisError(report);
|
|
29
|
+
return {
|
|
30
|
+
status: deepError ? 'ERROR' : split.findings.length > 0 ? 'FAIL' : 'PASS',
|
|
31
|
+
findings: split.findings,
|
|
32
|
+
fileFindings: split.fileFindings,
|
|
33
|
+
unlocated: split.unlocated,
|
|
34
|
+
excludedOutsideChangedLines: split.outside,
|
|
35
|
+
changedLines,
|
|
36
|
+
report,
|
|
37
|
+
...(deepError ? { deepError } : {}),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** The JSON shape of a finding, shared by the CLI's --json and the MCP tool. */
|
|
41
|
+
export function toReviewFinding(failure) {
|
|
42
|
+
return {
|
|
43
|
+
id: failure.id,
|
|
44
|
+
gate: failure.title,
|
|
45
|
+
severity: failure.severity || 'medium',
|
|
46
|
+
provenance: failure.provenance || 'traditional',
|
|
47
|
+
message: failure.details,
|
|
48
|
+
file: failure.files?.[0] || '',
|
|
49
|
+
line: failure.line ?? null,
|
|
50
|
+
...(failure.hint ? { suggestion: failure.hint } : {}),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
6
|
+
import { ConfigSchema } from '../types/index.js';
|
|
7
|
+
import { parseDiff } from '../utils/diff.js';
|
|
8
|
+
import { splitByChangedLines } from './changed-lines.js';
|
|
9
|
+
import { diffFromGit } from './git-diff.js';
|
|
10
|
+
import { reviewChange, toReviewFinding } from './review.js';
|
|
11
|
+
const LEAKY = [
|
|
12
|
+
'export async function notify(endpoint: string, signature: string) {',
|
|
13
|
+
" return fetch(endpoint, { method: 'POST', headers: { 'x-hook-signature': signature } });",
|
|
14
|
+
'}',
|
|
15
|
+
'',
|
|
16
|
+
].join('\n');
|
|
17
|
+
function failure(file, line) {
|
|
18
|
+
return { id: 'x', title: 'X', details: 'd', files: file ? [file] : [], ...(line !== undefined ? { line } : {}) };
|
|
19
|
+
}
|
|
20
|
+
describe('splitByChangedLines', () => {
|
|
21
|
+
it('separates changed-line, file-level, unlocated and outside findings', () => {
|
|
22
|
+
const changed = { 'a.ts': new Set([3]) };
|
|
23
|
+
const split = splitByChangedLines([failure('a.ts', 3), failure('a.ts', 9), failure('a.ts'), failure('b.ts'), failure(undefined)], changed);
|
|
24
|
+
expect(split.findings.map(f => f.line)).toEqual([3]);
|
|
25
|
+
expect(split.fileFindings).toHaveLength(1);
|
|
26
|
+
expect(split.unlocated).toBe(1);
|
|
27
|
+
expect(split.outside).toBe(2);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
describe('git-backed review', () => {
|
|
31
|
+
let repo;
|
|
32
|
+
const git = (...args) => execFileSync('git', ['-C', repo, ...args], { encoding: 'utf8' });
|
|
33
|
+
const write = (rel, body) => {
|
|
34
|
+
fs.mkdirSync(path.dirname(path.join(repo, rel)), { recursive: true });
|
|
35
|
+
fs.writeFileSync(path.join(repo, rel), body);
|
|
36
|
+
};
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
repo = fs.mkdtempSync(path.join(os.tmpdir(), 'review-core-'));
|
|
39
|
+
git('init', '-q', '-b', 'main');
|
|
40
|
+
git('config', 'user.email', 't@example.com');
|
|
41
|
+
git('config', 'user.name', 't');
|
|
42
|
+
git('config', 'commit.gpgsign', 'false');
|
|
43
|
+
});
|
|
44
|
+
afterEach(() => { fs.rmSync(repo, { recursive: true, force: true }); });
|
|
45
|
+
it('works in a repository with no commits yet', () => {
|
|
46
|
+
write('src/new.ts', 'export const a = 1;\n');
|
|
47
|
+
expect(parseDiff(diffFromGit(repo))).toEqual({ 'src/new.ts': new Set([1]) });
|
|
48
|
+
});
|
|
49
|
+
it('takes uncommitted edits and untracked files from the working tree', () => {
|
|
50
|
+
write('src/a.ts', 'export const a = 1;\nexport const b = 2;\n');
|
|
51
|
+
git('add', '-A');
|
|
52
|
+
git('commit', '-qm', 'init');
|
|
53
|
+
write('src/a.ts', 'export const a = 1;\nexport const b = 3;\n');
|
|
54
|
+
write('src/new.ts', 'export const c = 1;\nexport const d = 2;\n');
|
|
55
|
+
expect(parseDiff(diffFromGit(repo))).toEqual({ 'src/a.ts': new Set([2]), 'src/new.ts': new Set([1, 2]) });
|
|
56
|
+
});
|
|
57
|
+
it('takes a branch against its merge-base, committed and uncommitted', () => {
|
|
58
|
+
write('src/a.ts', 'export const a = 1;\n');
|
|
59
|
+
git('add', '-A');
|
|
60
|
+
git('commit', '-qm', 'init');
|
|
61
|
+
git('checkout', '-q', '-b', 'feature');
|
|
62
|
+
write('src/b.ts', 'export const b = 1;\n');
|
|
63
|
+
git('add', '-A');
|
|
64
|
+
git('commit', '-qm', 'feature');
|
|
65
|
+
write('src/a.ts', 'export const a = 1;\nexport const z = 2;\n');
|
|
66
|
+
expect(parseDiff(diffFromGit(repo, { mode: 'base', base: 'main' }))).toEqual({ 'src/a.ts': new Set([2]), 'src/b.ts': new Set([1]) });
|
|
67
|
+
});
|
|
68
|
+
it('fails a change that introduces a bug and ignores the same bug in untouched code', async () => {
|
|
69
|
+
write('src/old.ts', LEAKY);
|
|
70
|
+
git('add', '-A');
|
|
71
|
+
git('commit', '-qm', 'init');
|
|
72
|
+
write('src/notify.ts', LEAKY);
|
|
73
|
+
const config = ConfigSchema.parse({ version: 1, gates: { semantic_bugs: { enabled: true } } });
|
|
74
|
+
const result = await reviewChange({ cwd: repo, config });
|
|
75
|
+
const semantic = result.findings.filter(f => f.id === 'semantic-bugs');
|
|
76
|
+
expect(result.status).toBe('FAIL');
|
|
77
|
+
expect(semantic.map(f => [f.files?.[0], f.line])).toEqual([['src/notify.ts', 2]]);
|
|
78
|
+
expect(toReviewFinding(semantic[0])).toMatchObject({ id: 'semantic-bugs', file: 'src/notify.ts', line: 2, severity: 'high' });
|
|
79
|
+
});
|
|
80
|
+
it('passes when nothing changed', async () => {
|
|
81
|
+
write('src/a.ts', 'export const a = 1;\n');
|
|
82
|
+
git('add', '-A');
|
|
83
|
+
git('commit', '-qm', 'init');
|
|
84
|
+
const result = await reviewChange({ cwd: repo, config: ConfigSchema.parse({ version: 1 }) });
|
|
85
|
+
expect(result).toMatchObject({ status: 'PASS', findings: [], report: null });
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import fs from 'fs';
|
|
8
8
|
import path from 'path';
|
|
9
|
-
import { analyzeFiles } from './engine.js';
|
|
9
|
+
import { ALL_RULES, analyzeFiles } from './engine.js';
|
|
10
10
|
export function loadCases(root) {
|
|
11
11
|
return fs.readdirSync(root, { withFileTypes: true })
|
|
12
12
|
.filter(entry => entry.isDirectory() && fs.existsSync(path.join(root, entry.name, 'case.json')))
|
|
@@ -27,5 +27,5 @@ function findingsIn(dir) {
|
|
|
27
27
|
if (!fs.existsSync(dir))
|
|
28
28
|
return [];
|
|
29
29
|
const files = fs.readdirSync(dir).filter(f => /\.[cm]?[jt]sx?$/.test(f));
|
|
30
|
-
return analyzeFiles(dir, files).map(f => `${f.rule} ${f.file}:${f.line}`);
|
|
30
|
+
return analyzeFiles(dir, files, { rules: ALL_RULES }).map(f => `${f.rule} ${f.file}:${f.line}`);
|
|
31
31
|
}
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { CANDIDATE_RULES, ECOSYSTEM_RULES } from './rules/ecosystem/index.js';
|
|
3
4
|
import { loadCases, runCase } from './benchmark.js';
|
|
4
5
|
const ROOT = path.resolve(__dirname, '../../../../benchmarks/semantic');
|
|
5
6
|
describe('semantic benchmark', () => {
|
|
6
7
|
const cases = loadCases(ROOT);
|
|
7
8
|
it('has cases for every built-in rule, including negatives', () => {
|
|
8
9
|
const rules = new Set(cases.map(c => c.rule));
|
|
9
|
-
expect(rules).toEqual(new Set([
|
|
10
|
+
expect(rules).toEqual(new Set([
|
|
11
|
+
'in-memory-aggregation', 'credential-redirect', 'degraded-response-cached',
|
|
12
|
+
...[...ECOSYSTEM_RULES, ...CANDIDATE_RULES].map(rule => rule.id), null,
|
|
13
|
+
]));
|
|
10
14
|
});
|
|
11
15
|
for (const bench of cases) {
|
|
12
16
|
it(`${bench.name}: ${bench.note}`, () => {
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { SemanticFinding, SemanticRule } from './types.js';
|
|
2
|
+
/** Rules that run by default. */
|
|
2
3
|
export declare const BUILT_IN_RULES: SemanticRule[];
|
|
4
|
+
/** Every rule Rigour ships, candidates included: what can be named to run. */
|
|
5
|
+
export declare const ALL_RULES: SemanticRule[];
|
|
3
6
|
export interface EngineOptions {
|
|
4
7
|
rules?: SemanticRule[];
|
|
5
8
|
batchSize?: number;
|
package/dist/semantic/engine.js
CHANGED
|
@@ -3,11 +3,16 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { loadProjectConfig, programBatches } from './program.js';
|
|
6
|
+
import { ProjectFacts } from './project-facts.js';
|
|
6
7
|
import { Summaries } from './summaries.js';
|
|
7
8
|
import { inMemoryAggregation } from './rules/in-memory-aggregation.js';
|
|
8
9
|
import { credentialRedirect } from './rules/credential-redirect.js';
|
|
9
10
|
import { degradedResponseCached } from './rules/degraded-response-cached.js';
|
|
10
|
-
|
|
11
|
+
import { CANDIDATE_RULES, ECOSYSTEM_RULES } from './rules/ecosystem/index.js';
|
|
12
|
+
/** Rules that run by default. */
|
|
13
|
+
export const BUILT_IN_RULES = [inMemoryAggregation, credentialRedirect, degradedResponseCached, ...ECOSYSTEM_RULES];
|
|
14
|
+
/** Every rule Rigour ships, candidates included: what can be named to run. */
|
|
15
|
+
export const ALL_RULES = [...BUILT_IN_RULES, ...CANDIDATE_RULES];
|
|
11
16
|
/** Analyze `files` (cwd-relative or absolute). Declaration files are skipped. */
|
|
12
17
|
export function analyzeFiles(cwd, files, opts = {}) {
|
|
13
18
|
const rules = opts.rules ?? BUILT_IN_RULES;
|
|
@@ -19,6 +24,7 @@ export function analyzeFiles(cwd, files, opts = {}) {
|
|
|
19
24
|
const { options } = loadProjectConfig(cwd);
|
|
20
25
|
const rootSet = new Set(roots.map(r => path.normalize(r)));
|
|
21
26
|
const findings = [];
|
|
27
|
+
const project = new ProjectFacts(cwd);
|
|
22
28
|
for (const program of programBatches(roots, options, opts.batchSize)) {
|
|
23
29
|
const checker = program.getTypeChecker();
|
|
24
30
|
const summaries = new Summaries(checker);
|
|
@@ -26,7 +32,7 @@ export function analyzeFiles(cwd, files, opts = {}) {
|
|
|
26
32
|
if (!rootSet.has(path.normalize(sourceFile.fileName)))
|
|
27
33
|
continue;
|
|
28
34
|
for (const rule of rules) {
|
|
29
|
-
findings.push(...rule.check({ cwd, checker, sourceFile, summaries }));
|
|
35
|
+
findings.push(...rule.check({ cwd, program, checker, sourceFile, summaries, project }));
|
|
30
36
|
}
|
|
31
37
|
rootSet.delete(path.normalize(sourceFile.fileName));
|
|
32
38
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Does a call handle its own failure? Shared by the intent checks and the
|
|
3
|
+
* training-site export, so both see a call the same way.
|
|
4
|
+
*/
|
|
5
|
+
import ts from 'typescript';
|
|
6
|
+
import { type FunctionLike } from './ast.js';
|
|
7
|
+
export declare function isPromiseAll(call: ts.CallExpression): boolean;
|
|
8
|
+
export declare function isPromiseAllSettled(call: ts.CallExpression): boolean;
|
|
9
|
+
/** Inside a `try` (with a catch) of the same function: the rejection is handled there. */
|
|
10
|
+
export declare function isInsideTry(node: ts.Node, fn: FunctionLike): boolean;
|
|
11
|
+
/** `x().catch(...)`, `x().then(ok, fail)`, or a callee whose body catches and returns. */
|
|
12
|
+
export declare function handlesOwnFailure(checker: ts.TypeChecker, call: ts.CallExpression): boolean;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Does a call handle its own failure? Shared by the intent checks and the
|
|
3
|
+
* training-site export, so both see a call the same way.
|
|
4
|
+
*/
|
|
5
|
+
import ts from 'typescript';
|
|
6
|
+
import { calledFunction } from './ast.js';
|
|
7
|
+
export function isPromiseAll(call) {
|
|
8
|
+
return isPromiseMethod(call, 'all');
|
|
9
|
+
}
|
|
10
|
+
export function isPromiseAllSettled(call) {
|
|
11
|
+
return isPromiseMethod(call, 'allSettled');
|
|
12
|
+
}
|
|
13
|
+
function isPromiseMethod(call, name) {
|
|
14
|
+
const callee = call.expression;
|
|
15
|
+
return ts.isPropertyAccessExpression(callee) && callee.name.text === name
|
|
16
|
+
&& ts.isIdentifier(callee.expression) && callee.expression.text === 'Promise';
|
|
17
|
+
}
|
|
18
|
+
/** Inside a `try` (with a catch) of the same function: the rejection is handled there. */
|
|
19
|
+
export function isInsideTry(node, fn) {
|
|
20
|
+
for (let child = node, parent = node.parent; parent && parent !== fn; child = parent, parent = parent.parent) {
|
|
21
|
+
if (ts.isTryStatement(parent) && parent.tryBlock === child && parent.catchClause)
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
/** `x().catch(...)`, `x().then(ok, fail)`, or a callee whose body catches and returns. */
|
|
27
|
+
export function handlesOwnFailure(checker, call) {
|
|
28
|
+
const callee = call.expression;
|
|
29
|
+
if (ts.isPropertyAccessExpression(callee)) {
|
|
30
|
+
if (callee.name.text === 'catch' || callee.name.text === 'finally')
|
|
31
|
+
return true;
|
|
32
|
+
if (callee.name.text === 'then' && call.arguments.length >= 2)
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
const target = calledFunction(checker, call);
|
|
36
|
+
const body = target?.body;
|
|
37
|
+
return !!body && ts.isBlock(body) && body.statements.some(s => ts.isTryStatement(s) && !!s.catchClause && catchReturns(s.catchClause));
|
|
38
|
+
}
|
|
39
|
+
function catchReturns(clause) {
|
|
40
|
+
return clause.block.statements.some(s => ts.isReturnStatement(s));
|
|
41
|
+
}
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* which the code cannot say; see intent-check.ts.
|
|
8
8
|
*/
|
|
9
9
|
import ts from 'typescript';
|
|
10
|
-
import {
|
|
10
|
+
import { enclosingFunction, forEachNode, lineOf, relativeFile, unwrap } from '../ast.js';
|
|
11
|
+
import { handlesOwnFailure, isInsideTry, isPromiseAll } from '../failure-handling.js';
|
|
11
12
|
import { promiseAllItems } from '../origins.js';
|
|
12
13
|
import { functionKeyOf } from '../learn/identity.js';
|
|
13
14
|
export function findParallelReads(cwd, checker, sourceFile) {
|
|
@@ -28,32 +29,3 @@ export function findParallelReads(cwd, checker, sourceFile) {
|
|
|
28
29
|
});
|
|
29
30
|
return sites;
|
|
30
31
|
}
|
|
31
|
-
function isPromiseAll(call) {
|
|
32
|
-
const callee = call.expression;
|
|
33
|
-
return ts.isPropertyAccessExpression(callee) && callee.name.text === 'all'
|
|
34
|
-
&& ts.isIdentifier(callee.expression) && callee.expression.text === 'Promise';
|
|
35
|
-
}
|
|
36
|
-
/** Inside a `try` (with a catch) of the same function: the rejection is handled there. */
|
|
37
|
-
function isInsideTry(node, fn) {
|
|
38
|
-
for (let child = node, parent = node.parent; parent && parent !== fn; child = parent, parent = parent.parent) {
|
|
39
|
-
if (ts.isTryStatement(parent) && parent.tryBlock === child && parent.catchClause)
|
|
40
|
-
return true;
|
|
41
|
-
}
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
44
|
-
/** `x().catch(...)`, `x().then(ok, fail)`, or a callee whose body catches and returns. */
|
|
45
|
-
function handlesOwnFailure(checker, call) {
|
|
46
|
-
const callee = call.expression;
|
|
47
|
-
if (ts.isPropertyAccessExpression(callee)) {
|
|
48
|
-
if (callee.name.text === 'catch' || callee.name.text === 'finally')
|
|
49
|
-
return true;
|
|
50
|
-
if (callee.name.text === 'then' && call.arguments.length >= 2)
|
|
51
|
-
return true;
|
|
52
|
-
}
|
|
53
|
-
const target = calledFunction(checker, call);
|
|
54
|
-
const body = target?.body;
|
|
55
|
-
return !!body && ts.isBlock(body) && body.statements.some(s => ts.isTryStatement(s) && !!s.catchClause && catchReturns(s.catchClause));
|
|
56
|
-
}
|
|
57
|
-
function catchReturns(clause) {
|
|
58
|
-
return clause.block.statements.some(s => ts.isReturnStatement(s));
|
|
59
|
-
}
|
|
@@ -23,3 +23,5 @@ export interface LearnReport {
|
|
|
23
23
|
}>;
|
|
24
24
|
}
|
|
25
25
|
export declare function learnFromFix(input: LearnInput): Promise<LearnReport>;
|
|
26
|
+
/** Learn from one file's before and after (e.g. an agent's fix), validated against the repository at `repoDir`. */
|
|
27
|
+
export declare function learnFromFileChange(repoDir: string, file: string, before: string, after: string, maxHits?: number): Promise<LearnReport>;
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* reported with the reason the most specific one failed.
|
|
9
9
|
*/
|
|
10
10
|
import crypto from 'crypto';
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import os from 'os';
|
|
11
13
|
import path from 'path';
|
|
12
14
|
import { globby } from 'globby';
|
|
13
15
|
import { analyzeFiles } from '../engine.js';
|
|
@@ -47,6 +49,22 @@ export async function learnFromFix(input) {
|
|
|
47
49
|
}
|
|
48
50
|
return report;
|
|
49
51
|
}
|
|
52
|
+
/** Learn from one file's before and after (e.g. an agent's fix), validated against the repository at `repoDir`. */
|
|
53
|
+
export async function learnFromFileChange(repoDir, file, before, after, maxHits) {
|
|
54
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'rigour-learn-'));
|
|
55
|
+
try {
|
|
56
|
+
const place = (tree, content) => {
|
|
57
|
+
const target = path.join(root, tree, file);
|
|
58
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
59
|
+
fs.writeFileSync(target, content);
|
|
60
|
+
return path.join(root, tree);
|
|
61
|
+
};
|
|
62
|
+
return await learnFromFix({ beforeDir: place('before', before), afterDir: place('after', after), repoDir, files: [file], maxHits });
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
50
68
|
function openTree(cwd, file) {
|
|
51
69
|
const root = path.join(cwd, file);
|
|
52
70
|
const [program] = [...programBatches([root], loadProjectConfig(cwd).options)];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
export declare class ProjectFacts {
|
|
3
|
+
private readonly cwd;
|
|
4
|
+
private readonly packages;
|
|
5
|
+
private readonly options;
|
|
6
|
+
constructor(cwd: string);
|
|
7
|
+
/** Declared dependency names and version ranges of the package that owns `file`. */
|
|
8
|
+
dependencies(file: string): Map<string, string>;
|
|
9
|
+
/** Directory of the package.json that owns `file`. */
|
|
10
|
+
packageDir(file: string): string | undefined;
|
|
11
|
+
/** The `name` of the package that owns `file`. */
|
|
12
|
+
packageName(file: string): string | undefined;
|
|
13
|
+
declares(file: string, ...names: string[]): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Highest major version a declared dependency may run on, across every field
|
|
16
|
+
* and `||` alternative (`>=18.0.0 || >=19.0.0` -> 19). A package that supports
|
|
17
|
+
* a major must work on it. Undefined when no range names a version (`catalog:`, `workspace:*`).
|
|
18
|
+
*/
|
|
19
|
+
majorVersion(file: string, name: string): number | undefined;
|
|
20
|
+
/** Compiler options of the nearest tsconfig.json above `file`, extends resolved. */
|
|
21
|
+
compilerOptions(file: string): ts.CompilerOptions;
|
|
22
|
+
private dirsUp;
|
|
23
|
+
private nearestPackage;
|
|
24
|
+
private readPackage;
|
|
25
|
+
}
|