@rigour-labs/core 6.4.4 → 6.5.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/dist/deep/agent-review.d.ts +19 -0
- package/dist/deep/agent-review.js +19 -0
- package/dist/deep/agent-review.test.d.ts +1 -0
- package/dist/deep/agent-review.test.js +53 -0
- package/dist/deep/code-pass.d.ts +10 -0
- package/dist/deep/code-pass.js +64 -33
- package/dist/deep/code-pass.test.js +53 -12
- package/dist/deep/code-verifier.d.ts +3 -1
- package/dist/deep/code-verifier.js +11 -11
- package/dist/deep/code-verifier.test.js +8 -0
- package/dist/deep/pr-diff.d.ts +7 -0
- package/dist/deep/pr-diff.js +47 -0
- package/dist/deep/pr-review.d.ts +33 -0
- package/dist/deep/pr-review.js +81 -0
- package/dist/deep/pr-review.test.d.ts +1 -0
- package/dist/deep/pr-review.test.js +80 -0
- package/dist/deep/related-changes.d.ts +13 -0
- package/dist/deep/related-changes.js +56 -0
- package/dist/deep/related-changes.test.d.ts +1 -0
- package/dist/deep/related-changes.test.js +33 -0
- package/dist/deep/review-tools.d.ts +16 -0
- package/dist/deep/review-tools.js +111 -0
- package/dist/deep/review-tools.test.d.ts +1 -0
- package/dist/deep/review-tools.test.js +39 -0
- package/dist/deep/risk.d.ts +41 -0
- package/dist/deep/risk.js +135 -0
- package/dist/deep/risk.test.d.ts +1 -0
- package/dist/deep/risk.test.js +63 -0
- package/dist/deep/router.d.ts +27 -0
- package/dist/deep/router.js +15 -0
- package/dist/deep/self-check.d.ts +14 -9
- package/dist/deep/self-check.js +41 -32
- package/dist/deep/tool-loop.d.ts +19 -0
- package/dist/deep/tool-loop.js +23 -0
- package/dist/gates/deep-analysis.d.ts +27 -0
- package/dist/gates/deep-analysis.js +73 -2
- package/dist/gates/deep-runner.js +14 -0
- package/dist/gates/frontend-secret-exposure.d.ts +2 -0
- package/dist/gates/frontend-secret-exposure.js +8 -0
- package/dist/gates/frontend-secret-exposure.test.js +14 -0
- package/dist/gates/hallucinated-imports/framework-modules-nuxt.test.d.ts +1 -0
- package/dist/gates/hallucinated-imports/framework-modules-nuxt.test.js +27 -0
- package/dist/gates/hallucinated-imports/framework-modules.d.ts +12 -3
- package/dist/gates/hallucinated-imports/framework-modules.js +32 -6
- package/dist/gates/hallucinated-imports/index.js +2 -1
- package/dist/gates/hallucinated-imports/js-resolver-types.test.d.ts +1 -0
- package/dist/gates/hallucinated-imports/js-resolver-types.test.js +14 -0
- package/dist/gates/hallucinated-imports/js-resolver.d.ts +6 -1
- package/dist/gates/hallucinated-imports/js-resolver.js +25 -3
- package/dist/gates/phantom-apis.js +2 -2
- package/dist/gates/phantom-apis.test.js +10 -0
- package/dist/gates/promise-safety.js +1 -1
- package/dist/gates/test-quality-matchers.js +3 -0
- package/dist/gates/test-quality.test.js +13 -0
- package/dist/hooks/checker.test.js +9 -0
- package/dist/hooks/stop-review.d.ts +2 -0
- package/dist/hooks/stop-review.js +19 -3
- package/dist/hooks/templates.d.ts +2 -0
- package/dist/hooks/templates.js +16 -2
- package/dist/hooks/templates.test.d.ts +1 -0
- package/dist/hooks/templates.test.js +16 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +13 -0
- package/dist/inference/cloud-provider.d.ts +12 -1
- package/dist/inference/cloud-provider.js +137 -2
- package/dist/inference/cloud-provider.test.d.ts +1 -0
- package/dist/inference/cloud-provider.test.js +118 -0
- package/dist/inference/pricing.d.ts +15 -0
- package/dist/inference/pricing.js +13 -0
- package/dist/inference/types.d.ts +42 -0
- package/dist/review/changed-function-spans.d.ts +4 -0
- package/dist/review/changed-function-spans.js +48 -0
- package/dist/review/changed-lines.d.ts +9 -3
- package/dist/review/changed-lines.js +19 -2
- package/dist/review/deep-runs.d.ts +22 -0
- package/dist/review/deep-runs.js +59 -0
- package/dist/review/deep-runs.test.d.ts +1 -0
- package/dist/review/deep-runs.test.js +23 -0
- package/dist/review/generated-files.d.ts +3 -0
- package/dist/review/generated-files.js +35 -0
- package/dist/review/generated-files.test.d.ts +1 -0
- package/dist/review/generated-files.test.js +25 -0
- package/dist/review/ledger.d.ts +27 -0
- package/dist/review/ledger.js +72 -0
- package/dist/review/quiet.d.ts +16 -0
- package/dist/review/quiet.js +71 -0
- package/dist/review/quiet.test.d.ts +1 -0
- package/dist/review/quiet.test.js +27 -0
- package/dist/review/review-ack.d.ts +16 -0
- package/dist/review/review-ack.js +25 -0
- package/dist/review/review-task.d.ts +40 -0
- package/dist/review/review-task.js +50 -0
- package/dist/review/review-task.test.d.ts +1 -0
- package/dist/review/review-task.test.js +80 -0
- package/dist/review/review.d.ts +12 -1
- package/dist/review/review.js +16 -6
- package/dist/review/review.test.js +25 -0
- package/dist/review-learning/acted-on.d.ts +25 -0
- package/dist/review-learning/acted-on.js +69 -0
- package/dist/review-learning/learn-from-reviews.d.ts +34 -0
- package/dist/review-learning/learn-from-reviews.js +65 -0
- package/dist/review-learning/lessons.d.ts +52 -0
- package/dist/review-learning/lessons.js +149 -0
- package/dist/review-learning/repo-rules.d.ts +16 -0
- package/dist/review-learning/repo-rules.js +84 -0
- package/dist/review-learning/repo-rules.test.d.ts +1 -0
- package/dist/review-learning/repo-rules.test.js +39 -0
- package/dist/review-learning/review-learning.test.d.ts +1 -0
- package/dist/review-learning/review-learning.test.js +104 -0
- package/dist/review-learning/team-lessons.d.ts +4 -0
- package/dist/review-learning/team-lessons.js +24 -0
- package/dist/semantic/rules/credential-redirect.js +4 -1
- package/dist/settings.js +4 -1
- package/dist/telemetry/telemetry.d.ts +28 -0
- package/dist/telemetry/telemetry.js +152 -0
- package/dist/telemetry/telemetry.test.d.ts +1 -0
- package/dist/telemetry/telemetry.test.js +59 -0
- package/dist/telemetry/token.d.ts +1 -0
- package/dist/telemetry/token.js +1 -0
- package/dist/templates/universal-config.js +7 -3
- package/dist/types/index.d.ts +276 -10
- package/dist/types/index.js +38 -5
- package/package.json +6 -6
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anonymous, opt-in usage telemetry: how Rigour is used and where it is wrong,
|
|
3
|
+
* so the next release fixes what people actually hit. Every field is listed in
|
|
4
|
+
* TELEMETRY.md.
|
|
5
|
+
*
|
|
6
|
+
* Never sent: code, file or repository names, paths, git remotes, finding
|
|
7
|
+
* text, emails, keys, IP addresses (Mixpanel is told not to record them). The
|
|
8
|
+
* only identity is a random install id. Nothing is sent unless the person said
|
|
9
|
+
* yes, a token is built in (release builds only), and neither DO_NOT_TRACK nor
|
|
10
|
+
* RIGOUR_TELEMETRY=0 is set. CI never sends unless RIGOUR_TELEMETRY=1. A send
|
|
11
|
+
* that fails or is slow is dropped: telemetry never fails or slows a command.
|
|
12
|
+
*
|
|
13
|
+
* Two shapes: an event per CLI command or review (`trackUsage`), and, for the
|
|
14
|
+
* hot paths an agent drives on every edit (hooks, MCP tools), local counters
|
|
15
|
+
* (`countUsage`, no network) sent as one `daily_usage` event a day.
|
|
16
|
+
*/
|
|
17
|
+
import crypto from 'crypto';
|
|
18
|
+
import fs from 'fs';
|
|
19
|
+
import os from 'os';
|
|
20
|
+
import path from 'path';
|
|
21
|
+
import { MIXPANEL_TOKEN } from './token.js';
|
|
22
|
+
const ENDPOINT = 'https://api.mixpanel.com/track?ip=0';
|
|
23
|
+
const TIMEOUT_MS = 2000;
|
|
24
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
25
|
+
export function telemetryToken(env = process.env) {
|
|
26
|
+
return env.RIGOUR_MIXPANEL_TOKEN?.trim() || MIXPANEL_TOKEN;
|
|
27
|
+
}
|
|
28
|
+
function file(home, name) {
|
|
29
|
+
return path.join(home, '.rigour', name);
|
|
30
|
+
}
|
|
31
|
+
export function readTelemetryState(home = os.homedir()) {
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(fs.readFileSync(file(home, 'telemetry.json'), 'utf8'));
|
|
34
|
+
if (typeof parsed?.installId === 'string')
|
|
35
|
+
return { installId: parsed.installId, enabled: typeof parsed.enabled === 'boolean' ? parsed.enabled : undefined };
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// Not asked yet.
|
|
39
|
+
}
|
|
40
|
+
return { installId: crypto.randomUUID() };
|
|
41
|
+
}
|
|
42
|
+
export function setTelemetryEnabled(enabled, home = os.homedir()) {
|
|
43
|
+
const state = { ...readTelemetryState(home), enabled };
|
|
44
|
+
writeJson(file(home, 'telemetry.json'), state);
|
|
45
|
+
return state;
|
|
46
|
+
}
|
|
47
|
+
function isCi(env) {
|
|
48
|
+
return !!(env.CI || env.GITHUB_ACTIONS || env.BUILDKITE || env.GITLAB_CI || env.CIRCLECI);
|
|
49
|
+
}
|
|
50
|
+
/** Opted out by the environment, whatever the stored answer. */
|
|
51
|
+
function vetoed(env) {
|
|
52
|
+
return (!!env.DO_NOT_TRACK && env.DO_NOT_TRACK !== '0') || env.RIGOUR_TELEMETRY === '0' || !telemetryToken(env);
|
|
53
|
+
}
|
|
54
|
+
export function isTelemetryEnabled(deps = {}) {
|
|
55
|
+
const env = deps.env ?? process.env;
|
|
56
|
+
if (vetoed(env))
|
|
57
|
+
return false;
|
|
58
|
+
if (env.RIGOUR_TELEMETRY === '1')
|
|
59
|
+
return true;
|
|
60
|
+
if (isCi(env))
|
|
61
|
+
return false;
|
|
62
|
+
return readTelemetryState(deps.home).enabled === true;
|
|
63
|
+
}
|
|
64
|
+
/** Ask only once, only a person at a terminal, only when sending would be possible at all. */
|
|
65
|
+
export function shouldAskTelemetry(isTty, deps = {}) {
|
|
66
|
+
const env = deps.env ?? process.env;
|
|
67
|
+
return isTty && !isCi(env) && !vetoed(env) && env.RIGOUR_TELEMETRY === undefined && readTelemetryState(deps.home).enabled === undefined;
|
|
68
|
+
}
|
|
69
|
+
export async function trackUsage(event, properties, deps = {}) {
|
|
70
|
+
if (!isTelemetryEnabled(deps))
|
|
71
|
+
return;
|
|
72
|
+
const env = deps.env ?? process.env;
|
|
73
|
+
const body = [{
|
|
74
|
+
event,
|
|
75
|
+
properties: {
|
|
76
|
+
token: telemetryToken(env),
|
|
77
|
+
distinct_id: readTelemetryState(deps.home).installId,
|
|
78
|
+
time: Math.floor((deps.now ?? Date.now()) / 1000),
|
|
79
|
+
$insert_id: crypto.randomUUID(),
|
|
80
|
+
version: deps.version,
|
|
81
|
+
os: process.platform,
|
|
82
|
+
node_major: Number(process.versions.node.split('.')[0]),
|
|
83
|
+
ci: isCi(env),
|
|
84
|
+
...properties,
|
|
85
|
+
},
|
|
86
|
+
}];
|
|
87
|
+
try {
|
|
88
|
+
await (deps.fetch ?? fetch)(ENDPOINT, {
|
|
89
|
+
method: 'POST',
|
|
90
|
+
headers: { 'Content-Type': 'application/json', Accept: 'text/plain' },
|
|
91
|
+
body: JSON.stringify(body),
|
|
92
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// Dropped: telemetry never affects the command.
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** Count something an agent does on a hot path (a hook check, an MCP tool call). Local only; no network. */
|
|
100
|
+
export function countUsage(name, by = 1, deps = {}) {
|
|
101
|
+
if (!isTelemetryEnabled(deps))
|
|
102
|
+
return;
|
|
103
|
+
try {
|
|
104
|
+
const home = deps.home ?? os.homedir();
|
|
105
|
+
const counters = readCounters(home, deps.now ?? Date.now());
|
|
106
|
+
counters.counts[name] = (counters.counts[name] ?? 0) + by;
|
|
107
|
+
writeJson(file(home, 'telemetry-counters.json'), counters);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// Counting never affects the agent.
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** Send the day's counters as one `daily_usage` event once they are a day old, then start a new day. */
|
|
114
|
+
export async function flushDailyUsage(deps = {}) {
|
|
115
|
+
if (!isTelemetryEnabled(deps))
|
|
116
|
+
return false;
|
|
117
|
+
const home = deps.home ?? os.homedir();
|
|
118
|
+
const now = deps.now ?? Date.now();
|
|
119
|
+
const counters = readCounters(home, now);
|
|
120
|
+
if (now - counters.since < DAY_MS || Object.keys(counters.counts).length === 0)
|
|
121
|
+
return false;
|
|
122
|
+
writeJson(file(home, 'telemetry-counters.json'), { since: now, counts: {} });
|
|
123
|
+
await trackUsage('daily_usage', { ...counters.counts }, deps);
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
function readCounters(home, now) {
|
|
127
|
+
try {
|
|
128
|
+
const parsed = JSON.parse(fs.readFileSync(file(home, 'telemetry-counters.json'), 'utf8'));
|
|
129
|
+
if (typeof parsed?.since === 'number' && parsed.counts && typeof parsed.counts === 'object')
|
|
130
|
+
return parsed;
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// First count.
|
|
134
|
+
}
|
|
135
|
+
return { since: now, counts: {} };
|
|
136
|
+
}
|
|
137
|
+
function writeJson(target, value) {
|
|
138
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
139
|
+
fs.writeFileSync(target, JSON.stringify(value, null, 2) + '\n');
|
|
140
|
+
}
|
|
141
|
+
/** A duration as a coarse bucket, so timing never identifies a run. */
|
|
142
|
+
export function durationBucket(ms) {
|
|
143
|
+
if (ms < 1000)
|
|
144
|
+
return '<1s';
|
|
145
|
+
if (ms < 5000)
|
|
146
|
+
return '1-5s';
|
|
147
|
+
if (ms < 30000)
|
|
148
|
+
return '5-30s';
|
|
149
|
+
if (ms < 120000)
|
|
150
|
+
return '30s-2m';
|
|
151
|
+
return '>2m';
|
|
152
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { countUsage, durationBucket, flushDailyUsage, isTelemetryEnabled, readTelemetryState, setTelemetryEnabled, shouldAskTelemetry, trackUsage } from './telemetry.js';
|
|
6
|
+
let home;
|
|
7
|
+
const env = { RIGOUR_MIXPANEL_TOKEN: 'tok' };
|
|
8
|
+
beforeEach(() => { home = fs.mkdtempSync(path.join(os.tmpdir(), 'telemetry-')); });
|
|
9
|
+
afterEach(() => { fs.rmSync(home, { recursive: true, force: true }); });
|
|
10
|
+
describe('telemetry consent', () => {
|
|
11
|
+
it('sends nothing until the person says yes, and the environment always wins', () => {
|
|
12
|
+
expect(isTelemetryEnabled({ env, home })).toBe(false);
|
|
13
|
+
expect(shouldAskTelemetry(true, { env, home })).toBe(true);
|
|
14
|
+
setTelemetryEnabled(true, home);
|
|
15
|
+
expect(isTelemetryEnabled({ env, home })).toBe(true);
|
|
16
|
+
expect(shouldAskTelemetry(true, { env, home })).toBe(false); // asked once
|
|
17
|
+
expect(isTelemetryEnabled({ env: { ...env, DO_NOT_TRACK: '1' }, home })).toBe(false);
|
|
18
|
+
expect(isTelemetryEnabled({ env: { ...env, RIGOUR_TELEMETRY: '0' }, home })).toBe(false);
|
|
19
|
+
expect(isTelemetryEnabled({ env: { ...env, CI: 'true' }, home })).toBe(false);
|
|
20
|
+
expect(isTelemetryEnabled({ env: {}, home })).toBe(false); // a build without a token never sends
|
|
21
|
+
});
|
|
22
|
+
it('never asks in CI or without a terminal, and keeps one random install id', () => {
|
|
23
|
+
expect(shouldAskTelemetry(false, { env, home })).toBe(false);
|
|
24
|
+
expect(shouldAskTelemetry(true, { env: { ...env, GITHUB_ACTIONS: 'true' }, home })).toBe(false);
|
|
25
|
+
const first = setTelemetryEnabled(false, home).installId;
|
|
26
|
+
expect(readTelemetryState(home).installId).toBe(first);
|
|
27
|
+
expect(first).toMatch(/^[0-9a-f-]{36}$/);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
describe('telemetry sending', () => {
|
|
31
|
+
it('sends one event with the install id and no IP, and swallows a failing network', async () => {
|
|
32
|
+
setTelemetryEnabled(true, home);
|
|
33
|
+
const fetch = vi.fn().mockResolvedValue({ ok: true });
|
|
34
|
+
await trackUsage('review_completed', { status: 'PASS', findings_by_gate: { 'semantic-bugs': 1 } }, { env, home, fetch, version: '6.5.0' });
|
|
35
|
+
const [url, init] = fetch.mock.calls[0];
|
|
36
|
+
expect(url).toBe('https://api.mixpanel.com/track?ip=0');
|
|
37
|
+
const [event] = JSON.parse(init.body);
|
|
38
|
+
expect(event.event).toBe('review_completed');
|
|
39
|
+
expect(event.properties).toMatchObject({ token: 'tok', distinct_id: readTelemetryState(home).installId, version: '6.5.0', status: 'PASS' });
|
|
40
|
+
expect(JSON.stringify(event)).not.toContain(home);
|
|
41
|
+
await expect(trackUsage('x', {}, { env, home, fetch: vi.fn().mockRejectedValue(new Error('offline')) })).resolves.toBeUndefined();
|
|
42
|
+
});
|
|
43
|
+
it('counts hot-path activity locally and sends it as one daily event', async () => {
|
|
44
|
+
setTelemetryEnabled(true, home);
|
|
45
|
+
const fetch = vi.fn().mockResolvedValue({ ok: true });
|
|
46
|
+
const day = 24 * 60 * 60 * 1000;
|
|
47
|
+
countUsage('hook_check', 1, { env, home, now: 0 });
|
|
48
|
+
countUsage('hook_check', 1, { env, home, now: 1000 });
|
|
49
|
+
countUsage('mcp:rigour_review', 1, { env, home, now: 2000 });
|
|
50
|
+
expect(await flushDailyUsage({ env, home, fetch, now: 5000 })).toBe(false); // not a day yet
|
|
51
|
+
expect(await flushDailyUsage({ env, home, fetch, now: day + 1 })).toBe(true);
|
|
52
|
+
const [event] = JSON.parse(fetch.mock.calls[0][1].body);
|
|
53
|
+
expect(event).toMatchObject({ event: 'daily_usage', properties: { hook_check: 2, 'mcp:rigour_review': 1 } });
|
|
54
|
+
expect(await flushDailyUsage({ env, home, fetch, now: 2 * day + 2 })).toBe(false); // counters reset
|
|
55
|
+
});
|
|
56
|
+
it('buckets durations so timing never identifies a run', () => {
|
|
57
|
+
expect([500, 3000, 20000, 60000, 600000].map(durationBucket)).toEqual(['<1s', '1-5s', '5-30s', '30s-2m', '>2m']);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const MIXPANEL_TOKEN = "53ab28669fd4bf80e2dbe5605eabacc5";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const MIXPANEL_TOKEN = '53ab28669fd4bf80e2dbe5605eabacc5';
|
|
@@ -154,7 +154,7 @@ export const UNIVERSAL_CONFIG = {
|
|
|
154
154
|
promise_safety: {
|
|
155
155
|
enabled: true,
|
|
156
156
|
check_unhandled_then: true,
|
|
157
|
-
check_unsafe_parse:
|
|
157
|
+
check_unsafe_parse: false,
|
|
158
158
|
check_async_without_await: true,
|
|
159
159
|
check_unsafe_fetch: true,
|
|
160
160
|
ignore_patterns: [],
|
|
@@ -191,8 +191,8 @@ export const UNIVERSAL_CONFIG = {
|
|
|
191
191
|
},
|
|
192
192
|
governance: {
|
|
193
193
|
enabled: true,
|
|
194
|
-
enforce_memory:
|
|
195
|
-
enforce_skills:
|
|
194
|
+
enforce_memory: false,
|
|
195
|
+
enforce_skills: false,
|
|
196
196
|
block_native_memory: true,
|
|
197
197
|
protected_memory_paths: [
|
|
198
198
|
'CLAUDE.md', '.claude/CLAUDE.md',
|
|
@@ -275,11 +275,15 @@ export const UNIVERSAL_CONFIG = {
|
|
|
275
275
|
fast_gates: ['hallucinated-imports', 'phantom-apis', 'deprecated-apis', 'promise-safety', 'security-patterns', 'file-size'],
|
|
276
276
|
timeout_ms: 5000,
|
|
277
277
|
block_on_failure: false,
|
|
278
|
+
require_review_ack: false,
|
|
278
279
|
dlp: true,
|
|
279
280
|
},
|
|
280
281
|
output: {
|
|
281
282
|
report_path: 'rigour-report.json',
|
|
282
283
|
},
|
|
284
|
+
review: {
|
|
285
|
+
include_heuristics: false,
|
|
286
|
+
},
|
|
283
287
|
planned: [],
|
|
284
288
|
ignore: [],
|
|
285
289
|
};
|