@rigour-labs/core 6.4.0 → 6.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/dist/deep/changed-functions.d.ts +15 -0
  2. package/dist/deep/changed-functions.js +34 -0
  3. package/dist/deep/code-pass.d.ts +10 -0
  4. package/dist/deep/code-pass.js +37 -2
  5. package/dist/deep/code-pass.test.d.ts +1 -0
  6. package/dist/deep/code-pass.test.js +69 -0
  7. package/dist/deep/code-review-prompt.d.ts +1 -1
  8. package/dist/deep/code-review-prompt.js +3 -2
  9. package/dist/deep/diff-tests/calls.d.ts +8 -0
  10. package/dist/deep/diff-tests/calls.js +85 -0
  11. package/dist/deep/diff-tests/calls.test.d.ts +1 -0
  12. package/dist/deep/diff-tests/calls.test.js +33 -0
  13. package/dist/deep/diff-tests/run.d.ts +21 -0
  14. package/dist/deep/diff-tests/run.js +178 -0
  15. package/dist/deep/diff-tests/run.test.d.ts +1 -0
  16. package/dist/deep/diff-tests/run.test.js +58 -0
  17. package/dist/deep/reference-pack.d.ts +19 -0
  18. package/dist/deep/reference-pack.js +101 -0
  19. package/dist/deep/reference-pack.test.d.ts +1 -0
  20. package/dist/deep/reference-pack.test.js +46 -0
  21. package/dist/deep/review-context-export.d.ts +11 -0
  22. package/dist/deep/review-context-export.js +31 -0
  23. package/dist/deep/review-context-export.test.d.ts +1 -0
  24. package/dist/deep/review-context-export.test.js +32 -0
  25. package/dist/deep/self-check.d.ts +21 -0
  26. package/dist/deep/self-check.js +49 -0
  27. package/dist/firewall/trusted-control.d.ts +1 -2
  28. package/dist/gates/deep-analysis.d.ts +5 -0
  29. package/dist/gates/deep-analysis.js +19 -1
  30. package/dist/gates/deep-runner.js +2 -1
  31. package/dist/gates/deep-timeout.test.d.ts +1 -0
  32. package/dist/gates/deep-timeout.test.js +10 -0
  33. package/dist/gates/logic-drift.d.ts +8 -3
  34. package/dist/gates/logic-drift.js +10 -5
  35. package/dist/gates/logic-drift.test.js +7 -0
  36. package/dist/gates/promise-safety.js +3 -1
  37. package/dist/gates/promise-safety.test.d.ts +1 -0
  38. package/dist/gates/promise-safety.test.js +34 -0
  39. package/dist/gates/security-patterns-data.js +2 -1
  40. package/dist/gates/security-patterns.test.js +8 -0
  41. package/dist/gates/semantic-bugs.js +4 -3
  42. package/dist/gates/semantic-bugs.test.js +6 -0
  43. package/dist/hooks/checker.d.ts +1 -2
  44. package/dist/hooks/index.d.ts +2 -0
  45. package/dist/index.d.ts +20 -1
  46. package/dist/index.js +4 -5
  47. package/dist/inference/index.d.ts +1 -1
  48. package/dist/inference/index.js +3 -5
  49. package/dist/inference/local-model.test.d.ts +1 -0
  50. package/dist/inference/local-model.test.js +23 -0
  51. package/dist/inference/sidecar-provider.d.ts +10 -1
  52. package/dist/inference/sidecar-provider.js +28 -6
  53. package/dist/inference/types.d.ts +8 -1
  54. package/dist/inference/types.js +14 -0
  55. package/dist/inference/types.test.d.ts +1 -0
  56. package/dist/inference/types.test.js +19 -0
  57. package/dist/review/diff-test-findings.d.ts +3 -0
  58. package/dist/review/diff-test-findings.js +41 -0
  59. package/dist/review/review.d.ts +4 -2
  60. package/dist/review/review.js +5 -2
  61. package/dist/semantic/benchmark.js +2 -2
  62. package/dist/semantic/benchmark.test.js +5 -1
  63. package/dist/semantic/engine.d.ts +3 -0
  64. package/dist/semantic/engine.js +8 -2
  65. package/dist/semantic/failure-handling.d.ts +12 -0
  66. package/dist/semantic/failure-handling.js +41 -0
  67. package/dist/semantic/intent/parallel-reads.js +2 -30
  68. package/dist/semantic/project-facts.d.ts +25 -0
  69. package/dist/semantic/project-facts.js +104 -0
  70. package/dist/semantic/project-facts.test.d.ts +1 -0
  71. package/dist/semantic/project-facts.test.js +45 -0
  72. package/dist/semantic/rules/ecosystem/deps-codegen-undeclared-import.d.ts +2 -0
  73. package/dist/semantic/rules/ecosystem/deps-codegen-undeclared-import.js +71 -0
  74. package/dist/semantic/rules/ecosystem/deps-internal-symbol.d.ts +2 -0
  75. package/dist/semantic/rules/ecosystem/deps-internal-symbol.js +36 -0
  76. package/dist/semantic/rules/ecosystem/dom-self-query-in-component.d.ts +2 -0
  77. package/dist/semantic/rules/ecosystem/dom-self-query-in-component.js +60 -0
  78. package/dist/semantic/rules/ecosystem/env-bare-browser-global.d.ts +2 -0
  79. package/dist/semantic/rules/ecosystem/env-bare-browser-global.js +57 -0
  80. package/dist/semantic/rules/ecosystem/exports-condition-parity.d.ts +2 -0
  81. package/dist/semantic/rules/ecosystem/exports-condition-parity.js +57 -0
  82. package/dist/semantic/rules/ecosystem/exports-forgotten-type.d.ts +2 -0
  83. package/dist/semantic/rules/ecosystem/exports-forgotten-type.js +128 -0
  84. package/dist/semantic/rules/ecosystem/imports-barrel-cycle.d.ts +2 -0
  85. package/dist/semantic/rules/ecosystem/imports-barrel-cycle.js +78 -0
  86. package/dist/semantic/rules/ecosystem/index.d.ts +15 -0
  87. package/dist/semantic/rules/ecosystem/index.js +22 -0
  88. package/dist/semantic/rules/ecosystem/paths-unnormalized-module-key.d.ts +2 -0
  89. package/dist/semantic/rules/ecosystem/paths-unnormalized-module-key.js +87 -0
  90. package/dist/semantic/rules/ecosystem/react-inline-html-object.d.ts +2 -0
  91. package/dist/semantic/rules/ecosystem/react-inline-html-object.js +30 -0
  92. package/dist/semantic/rules/ecosystem/shared.d.ts +18 -0
  93. package/dist/semantic/rules/ecosystem/shared.js +43 -0
  94. package/dist/semantic/rules/ecosystem/solid-jsx-and-conditional.d.ts +2 -0
  95. package/dist/semantic/rules/ecosystem/solid-jsx-and-conditional.js +36 -0
  96. package/dist/semantic/rules/ecosystem/ts-internal-in-public-signature.d.ts +2 -0
  97. package/dist/semantic/rules/ecosystem/ts-internal-in-public-signature.js +66 -0
  98. package/dist/semantic/rules/ecosystem/vite-rollup-only-hook-field.d.ts +2 -0
  99. package/dist/semantic/rules/ecosystem/vite-rollup-only-hook-field.js +31 -0
  100. package/dist/semantic/rules/ecosystem/vue-static-computed.d.ts +2 -0
  101. package/dist/semantic/rules/ecosystem/vue-static-computed.js +51 -0
  102. package/dist/semantic/sites/call-sites.d.ts +25 -0
  103. package/dist/semantic/sites/call-sites.js +69 -0
  104. package/dist/semantic/sites/call-sites.test.d.ts +1 -0
  105. package/dist/semantic/sites/call-sites.test.js +46 -0
  106. package/dist/semantic/sites/export.d.ts +2 -0
  107. package/dist/semantic/sites/export.js +22 -0
  108. package/dist/semantic/types.d.ts +4 -0
  109. package/dist/services/engineering-knowledge-graph.d.ts +1 -2
  110. package/dist/templates/universal-config.js +3 -2
  111. package/dist/types/index.d.ts +31 -8
  112. package/dist/types/index.js +4 -3
  113. package/dist/utils/diff-removed.test.d.ts +1 -0
  114. package/dist/utils/diff-removed.test.js +29 -0
  115. package/dist/utils/diff.d.ts +12 -0
  116. package/dist/utils/diff.js +26 -0
  117. package/package.json +6 -6
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Reference material for reviewing a changed file: what the change means,
3
+ * beyond the file itself.
4
+ *
5
+ * Most bugs reviewers miss need facts outside the changed lines: a guard the
6
+ * change deleted, what a called function actually does, who calls a changed
7
+ * export, what the PR says it intends. The pack carries, within a character
8
+ * budget and in this priority:
9
+ * 1. the PR description,
10
+ * 2. lines the change removed from this file,
11
+ * 3. definitions of functions the changed code calls (resolved by the type checker),
12
+ * 4. call sites of changed exported functions elsewhere in the repository.
13
+ *
14
+ * It is reference only: findings must still cite a line of the reviewed file.
15
+ * Its source joins the identifier check, so a finding may name a callee.
16
+ */
17
+ import { execFileSync } from 'child_process';
18
+ import path from 'path';
19
+ import ts from 'typescript';
20
+ import { calledFunction, forEachNode, lineOf } from '../semantic/ast.js';
21
+ import { changedFunctions, functionName, isExported } from './changed-functions.js';
22
+ import { loadProjectConfig, programBatches } from '../semantic/program.js';
23
+ const PR_BODY_CHARS = 1500;
24
+ const MAX_CALLEES = 6;
25
+ const CALLEE_LINES = 40;
26
+ const MAX_CALLERS = 5;
27
+ const TS_FILE = /\.(?:[cm]?[jt]s|[jt]sx)$/i;
28
+ export function buildReferencePack(input) {
29
+ const sections = [];
30
+ const sources = [];
31
+ let budget = input.maxChars;
32
+ const add = (title, body, source = body) => {
33
+ const block = `${title}\n${body}`;
34
+ if (!body.trim() || block.length > budget)
35
+ return;
36
+ sections.push(block);
37
+ sources.push(source);
38
+ budget -= block.length + 2;
39
+ };
40
+ if (input.prBody?.trim())
41
+ add('PR DESCRIPTION (what the author intends):', input.prBody.trim().slice(0, PR_BODY_CHARS));
42
+ for (const block of input.removed ?? []) {
43
+ add(`REMOVED by this change, just before line ${block.line} of ${input.file}:`, block.text.map(l => `- ${l}`).join('\n'), block.text.join('\n'));
44
+ }
45
+ const change = changeOf(input);
46
+ if (change) {
47
+ for (const callee of calleeDefinitions(input.cwd, change))
48
+ add(`CALLED: ${callee.label}`, callee.text);
49
+ for (const caller of callersOfChangedExports(input.cwd, input.file, change))
50
+ add(`CALLER of ${caller.name}:`, caller.text);
51
+ }
52
+ return { text: sections.join('\n\n'), source: sources.join('\n'), sections };
53
+ }
54
+ function changeOf(input) {
55
+ const focus = input.focusLines ?? [];
56
+ if (focus.length === 0 || !TS_FILE.test(input.file))
57
+ return undefined;
58
+ const absolute = path.resolve(input.cwd, input.file);
59
+ const [program] = programBatches([absolute], loadProjectConfig(input.cwd).options, 1);
60
+ const sourceFile = program?.getSourceFile(absolute);
61
+ if (!program || !sourceFile)
62
+ return undefined;
63
+ return { sourceFile, checker: program.getTypeChecker(), functions: changedFunctions(sourceFile, focus) };
64
+ }
65
+ /** Definitions, in other files, of functions called from the changed functions. */
66
+ function calleeDefinitions(cwd, change) {
67
+ const seen = new Set();
68
+ const snippets = [];
69
+ for (const fn of change.functions) {
70
+ forEachNode(fn, (node) => {
71
+ if (snippets.length >= MAX_CALLEES || !ts.isCallExpression(node))
72
+ return;
73
+ const target = calledFunction(change.checker, node);
74
+ const where = target?.getSourceFile();
75
+ if (!target || !where || seen.has(target) || where === change.sourceFile || where.isDeclarationFile)
76
+ return;
77
+ seen.add(target);
78
+ const lines = target.getText(where).split('\n');
79
+ const text = lines.slice(0, CALLEE_LINES).join('\n') + (lines.length > CALLEE_LINES ? `\n… [${lines.length - CALLEE_LINES} more lines]` : '');
80
+ snippets.push({ label: `${path.relative(cwd, where.fileName).split(path.sep).join('/')}:${lineOf(target)}`, text });
81
+ });
82
+ }
83
+ return snippets;
84
+ }
85
+ /** Call sites, outside this file, of exported functions the change touched. */
86
+ function callersOfChangedExports(cwd, file, change) {
87
+ const names = change.functions.filter(isExported).map(functionName).filter((n) => !!n);
88
+ return names.map(name => ({ name, text: grepCallers(cwd, file, name) })).filter(c => c.text);
89
+ }
90
+ function grepCallers(cwd, file, name) {
91
+ try {
92
+ const out = execFileSync('git', ['grep', '-n', '-E', '-e', `(^|[^A-Za-z0-9_$])${name}\\(`, '--', '*.ts', '*.tsx', '*.js', '*.jsx', '*.mjs', `:!${file}`], {
93
+ cwd, encoding: 'utf8', maxBuffer: 8 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'],
94
+ });
95
+ return out.split('\n').filter(Boolean).filter(l => !/\.(?:test|spec)\./.test(l)).slice(0, MAX_CALLERS)
96
+ .map(l => l.length > 200 ? `${l.slice(0, 200)}…` : l).join('\n');
97
+ }
98
+ catch {
99
+ return '';
100
+ }
101
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,46 @@
1
+ import { execFileSync } from 'child_process';
2
+ import fs from 'fs';
3
+ import os from 'os';
4
+ import path from 'path';
5
+ import { afterEach, describe, expect, it } from 'vitest';
6
+ import { buildReferencePack } from './reference-pack.js';
7
+ let repo;
8
+ afterEach(() => { if (repo)
9
+ fs.rmSync(repo, { recursive: true, force: true }); repo = undefined; });
10
+ function project() {
11
+ repo = fs.mkdtempSync(path.join(os.tmpdir(), 'reference-pack-'));
12
+ const files = {
13
+ 'src/paths.ts': "export function cleanPath(p: string): string {\n return p.replace(/\\/+/g, '/');\n}\n",
14
+ 'src/routes.ts': "import { cleanPath } from './paths';\nexport function routePath(dir: string, route: string): string {\n return cleanPath(`/${dir}${route}`);\n}\n",
15
+ 'src/generator.ts': "import { routePath } from './routes';\nexport const home = routePath('.', '/');\n",
16
+ };
17
+ for (const [name, text] of Object.entries(files)) {
18
+ fs.mkdirSync(path.dirname(path.join(repo, name)), { recursive: true });
19
+ fs.writeFileSync(path.join(repo, name), text);
20
+ }
21
+ execFileSync('git', ['init', '-q'], { cwd: repo });
22
+ execFileSync('git', ['add', '-A'], { cwd: repo });
23
+ return repo;
24
+ }
25
+ describe('buildReferencePack', () => {
26
+ it('carries the intent, removed lines, callee definitions and callers of a changed export', () => {
27
+ const cwd = project();
28
+ const pack = buildReferencePack({
29
+ cwd, file: 'src/routes.ts', focusLines: [3], maxChars: 10_000,
30
+ prBody: 'Normalize virtual route paths.',
31
+ removed: [{ line: 3, text: [" return `/${dir}${route}`;"] }],
32
+ });
33
+ expect(pack.text).toContain('PR DESCRIPTION (what the author intends):\nNormalize virtual route paths.');
34
+ expect(pack.text).toContain('REMOVED by this change, just before line 3 of src/routes.ts:\n- return `/${dir}${route}`;');
35
+ expect(pack.text).toContain('CALLED: src/paths.ts:1\nexport function cleanPath');
36
+ expect(pack.text).toContain("CALLER of routePath:\nsrc/generator.ts:2:export const home = routePath('.', '/');");
37
+ expect(pack.source).toContain('cleanPath');
38
+ });
39
+ it('stays within its budget, keeping the highest-priority sections', () => {
40
+ const cwd = project();
41
+ const pack = buildReferencePack({ cwd, file: 'src/routes.ts', focusLines: [3], maxChars: 80, prBody: 'Normalize virtual route paths.' });
42
+ expect(pack.text).toContain('PR DESCRIPTION');
43
+ expect(pack.text).not.toContain('CALLED');
44
+ expect(pack.text.length).toBeLessThanOrEqual(80);
45
+ });
46
+ });
@@ -0,0 +1,11 @@
1
+ export interface ReviewContextExport {
2
+ file: string;
3
+ focusLines: number[];
4
+ prompt: string;
5
+ }
6
+ export interface ReviewContextOptions {
7
+ prBody?: string;
8
+ maxSourceChars?: number;
9
+ maxReferenceChars?: number;
10
+ }
11
+ export declare function exportReviewContexts(cwd: string, diff: string, options?: ReviewContextOptions): Promise<ReviewContextExport[]>;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The exact prompt the max tier would review for each changed file of a diff.
3
+ *
4
+ * The driftbench review miner pairs these prompts with what the later fix
5
+ * changed, so a fine-tuned model learns from the input it will get at runtime,
6
+ * built by the same code (numbered source, focus segments, reference pack).
7
+ */
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+ import { changedLinesByFile, parseDiff, removedByFile } from '../utils/diff.js';
11
+ import { buildCodeContext } from './code-context.js';
12
+ import { buildCodeReviewPrompt } from './code-review-prompt.js';
13
+ import { extractFacts } from './fact-extractor.js';
14
+ import { buildReferencePack } from './reference-pack.js';
15
+ export async function exportReviewContexts(cwd, diff, options = {}) {
16
+ const focus = changedLinesByFile(parseDiff(diff));
17
+ const removed = removedByFile(diff);
18
+ const files = Object.keys(focus).filter(f => focus[f].length > 0 && fs.existsSync(path.join(cwd, f)));
19
+ if (files.length === 0)
20
+ return [];
21
+ const facts = await extractFacts(cwd, [], files);
22
+ return facts.map(fileFacts => {
23
+ const content = fs.readFileSync(path.join(cwd, fileFacts.path), 'utf-8');
24
+ const context = buildCodeContext(fileFacts, content, { maxChars: options.maxSourceChars ?? 36_000, focusLines: focus[fileFacts.path] });
25
+ const pack = buildReferencePack({
26
+ cwd, file: fileFacts.path, focusLines: focus[fileFacts.path], removed: removed[fileFacts.path],
27
+ prBody: options.prBody, maxChars: options.maxReferenceChars ?? 12_000,
28
+ });
29
+ return { file: fileFacts.path, focusLines: focus[fileFacts.path], prompt: buildCodeReviewPrompt(context, pack.text) };
30
+ });
31
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,32 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { afterEach, describe, expect, it } from 'vitest';
5
+ import { buildCodeReviewPrompt } from './code-review-prompt.js';
6
+ import { exportReviewContexts } from './review-context-export.js';
7
+ let dir;
8
+ afterEach(() => { if (dir)
9
+ fs.rmSync(dir, { recursive: true, force: true }); dir = undefined; });
10
+ const DIFF = `diff --git a/src/total.ts b/src/total.ts
11
+ --- a/src/total.ts
12
+ +++ b/src/total.ts
13
+ @@ -2,2 +2,2 @@
14
+ let sum = 0;
15
+ - for (let i = 0; i < items.length; i++) sum += items[i];
16
+ + for (let i = 0; i <= items.length; i++) sum += items[i];
17
+ `;
18
+ describe('exportReviewContexts', () => {
19
+ it('emits the review prompt for each changed file, with its focus lines and what the change removed', async () => {
20
+ dir = fs.mkdtempSync(path.join(os.tmpdir(), 'review-context-'));
21
+ fs.mkdirSync(path.join(dir, 'src'));
22
+ fs.writeFileSync(path.join(dir, 'src/total.ts'), 'export function total(items: number[]) {\n let sum = 0;\n for (let i = 0; i <= items.length; i++) sum += items[i];\n return sum;\n}\n');
23
+ const [context, ...rest] = await exportReviewContexts(dir, DIFF, { prBody: 'Sum the basket.' });
24
+ expect(rest).toEqual([]);
25
+ expect(context.file).toBe('src/total.ts');
26
+ expect(context.focusLines).toEqual([3]);
27
+ expect(context.prompt.startsWith(buildCodeReviewPrompt({ file: '', language: '', text: '', ranges: [], source: '' }).slice(0, 60))).toBe(true);
28
+ expect(context.prompt).toContain('3| for (let i = 0; i <= items.length; i++) sum += items[i];');
29
+ expect(context.prompt).toContain('- for (let i = 0; i < items.length; i++) sum += items[i];');
30
+ expect(context.prompt).toContain('Sum the basket.');
31
+ });
32
+ });
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Self-check: the model re-reads each finding against the code and confirms
3
+ * or withdraws it.
4
+ *
5
+ * Two review passes with the reference material in different orders raise
6
+ * recall (a finding one ordering misses, the other catches); asking again,
7
+ * finding by finding, whether it would misbehave at runtime keeps that extra
8
+ * recall from arriving as noise. A check that cannot run keeps the finding:
9
+ * an unavailable model is not evidence against it.
10
+ */
11
+ import type { DeepFinding, InferenceOptions, InferenceProvider } from '../inference/types.js';
12
+ import type { CodeContext } from './code-context.js';
13
+ export interface SelfCheckResult {
14
+ kept: DeepFinding[];
15
+ withdrawn: number;
16
+ failed: number;
17
+ }
18
+ export declare function selfCheck(provider: InferenceProvider, context: CodeContext, reference: string, findings: DeepFinding[], inference: InferenceOptions): Promise<SelfCheckResult>;
19
+ export declare function checkPrompt(context: CodeContext, reference: string, finding: DeepFinding): string;
20
+ /** true, false, or undefined when the reply is not a verdict. */
21
+ export declare function parseVerdict(reply: string): boolean | undefined;
@@ -0,0 +1,49 @@
1
+ const MAX_CHECKED = 8;
2
+ const VERDICT_SCHEMA = {
3
+ type: 'object',
4
+ properties: { real: { type: 'boolean' }, reason: { type: 'string' } },
5
+ required: ['real'],
6
+ };
7
+ export async function selfCheck(provider, context, reference, findings, inference) {
8
+ const result = { kept: [], withdrawn: 0, failed: 0 };
9
+ for (const [index, finding] of findings.entries()) {
10
+ if (index >= MAX_CHECKED) {
11
+ result.kept.push(finding);
12
+ continue;
13
+ }
14
+ try {
15
+ const reply = await provider.analyze(checkPrompt(context, reference, finding), { ...inference, jsonSchema: VERDICT_SCHEMA });
16
+ if (parseVerdict(reply) === false)
17
+ result.withdrawn++;
18
+ else
19
+ result.kept.push(finding);
20
+ }
21
+ catch {
22
+ result.failed++;
23
+ result.kept.push(finding);
24
+ }
25
+ }
26
+ return result;
27
+ }
28
+ export function checkPrompt(context, reference, finding) {
29
+ return [
30
+ 'You reported a defect in a code review. Check it against the code before it is posted.',
31
+ `SOURCE (line numbers on the left):\n${context.text}`,
32
+ reference ? `REFERENCE (not under review):\n${reference}` : '',
33
+ `REPORTED at ${context.file}:${finding.line}: ${finding.description}`,
34
+ 'Is this a real defect: would this code misbehave at runtime or in production as described? '
35
+ + 'Answer false if it is style, speculation about unseen callers, or already handled in the code shown. '
36
+ + 'Respond ONLY with JSON: {"real": true|false, "reason": "..."}.',
37
+ ].filter(Boolean).join('\n\n');
38
+ }
39
+ /** true, false, or undefined when the reply is not a verdict. */
40
+ export function parseVerdict(reply) {
41
+ const json = reply.slice(reply.indexOf('{'), reply.lastIndexOf('}') + 1);
42
+ try {
43
+ const value = JSON.parse(json).real;
44
+ return typeof value === 'boolean' ? value : undefined;
45
+ }
46
+ catch {
47
+ return undefined;
48
+ }
49
+ }
@@ -19,7 +19,7 @@ export declare function getGatewayPolicyHash(cwd: string, config: GatewayControl
19
19
  export declare function validateGatewayConfig(value: unknown): GatewayControlConfig;
20
20
  export declare function saveGatewayConfig(cwd: string, config: GatewayControlConfig, controlRoot?: string): Promise<string>;
21
21
  export declare function loadGatewayConfig(cwd: string, controlRoot?: string): Promise<GatewayControlConfig | null>;
22
- interface GrantInput {
22
+ export interface GrantInput {
23
23
  issuerId: string;
24
24
  subjectId: string;
25
25
  taskId: string;
@@ -42,4 +42,3 @@ export declare function consumeTrustedGrant(cwd: string, input: {
42
42
  reason: string;
43
43
  grant?: CapabilityGrant;
44
44
  }>;
45
- export {};
@@ -47,9 +47,14 @@ export declare class DeepAnalysisGate extends Gate {
47
47
  run(context: GateContext): Promise<Failure[]>;
48
48
  private setupProvider;
49
49
  private reviewCode;
50
+ /** The max and cloud tiers read reference material and review twice; the small models do not. */
51
+ private referenceOptions;
50
52
  private analyzeFacts;
51
53
  private recordPass;
52
54
  private localMemoryFindings;
53
55
  private createCloudAgent;
54
56
  private toFailure;
55
57
  }
58
+ /** Per-call options; unset config falls back to per-provider defaults. */
59
+ /** Per-call inference timeout: a local 7B reads a file with its reference pack far slower than the small tiers. */
60
+ export declare function defaultTimeout(options: DeepOptions): number;
@@ -32,6 +32,9 @@ const CLOUD_SETUP_TIMEOUT_MS = 120_000;
32
32
  */
33
33
  const LOCAL_SOURCE_CHARS = 36_000;
34
34
  const CLOUD_SOURCE_CHARS = 60_000;
35
+ /** Reference material (removed lines, callees, callers, PR intent) for the max and cloud tiers. */
36
+ const LOCAL_REFERENCE_CHARS = 12_000;
37
+ const CLOUD_REFERENCE_CHARS = 24_000;
35
38
  export class DeepAnalysisGate extends Gate {
36
39
  config;
37
40
  provider = null;
@@ -104,11 +107,20 @@ export class DeepAnalysisGate extends Gate {
104
107
  inference: inferenceOptions(this.config),
105
108
  maxSourceChars: isCloud(this.config.options) ? CLOUD_SOURCE_CHARS : LOCAL_SOURCE_CHARS,
106
109
  focusLines: this.config.options.focusLines,
110
+ reference: this.referenceOptions(),
107
111
  onProgress: this.config.onProgress,
108
112
  });
109
113
  this.recordPass(result);
110
114
  return verifyCodeFindings(result.findings, result.contexts);
111
115
  }
116
+ /** The max and cloud tiers read reference material and review twice; the small models do not. */
117
+ referenceOptions() {
118
+ const options = this.config.options;
119
+ const cloud = isCloud(options);
120
+ if (!cloud && !options.max)
121
+ return undefined;
122
+ return { maxChars: cloud ? CLOUD_REFERENCE_CHARS : LOCAL_REFERENCE_CHARS, removed: options.removedLines, prBody: options.prBody };
123
+ }
112
124
  async analyzeFacts(cwd, facts) {
113
125
  this.config.onProgress?.(` Found ${facts.length} files to analyze.`);
114
126
  const memory = await this.localMemoryFindings(cwd, facts);
@@ -163,12 +175,18 @@ function isCloud(options) {
163
175
  return !!options.apiKey && !!options.provider && options.provider !== 'local';
164
176
  }
165
177
  /** Per-call options; unset config falls back to per-provider defaults. */
178
+ /** Per-call inference timeout: a local 7B reads a file with its reference pack far slower than the small tiers. */
179
+ export function defaultTimeout(options) {
180
+ if (isCloud(options))
181
+ return 120_000;
182
+ return options.max ? 240_000 : 60_000;
183
+ }
166
184
  function inferenceOptions(config) {
167
185
  const cloud = isCloud(config.options);
168
186
  return {
169
187
  maxTokens: config.maxTokens ?? (cloud ? 4096 : 1024),
170
188
  temperature: config.temperature ?? 0.1,
171
- timeout: config.timeoutMs ?? (cloud ? 120_000 : 60_000),
189
+ timeout: config.timeoutMs ?? defaultTimeout(config.options),
172
190
  jsonMode: true,
173
191
  };
174
192
  }
@@ -3,6 +3,7 @@
3
3
  * and failures, derived from the deep gate's outcome.
4
4
  */
5
5
  import { DeepAnalysisGate } from './deep-analysis.js';
6
+ import { localTier } from '../inference/types.js';
6
7
  import { Logger } from '../utils/logger.js';
7
8
  /**
8
9
  * Run the deep gate and turn its outcome into summary, stats and failures.
@@ -37,7 +38,7 @@ export async function runDeepAnalysis(config, context, deepOptions) {
37
38
  enabled: true,
38
39
  status: outcome.status,
39
40
  mode: outcome.mode,
40
- tier: isLocal ? (deepOptions.pro ? 'deep' : 'lite') : 'cloud',
41
+ tier: isLocal ? localTier(deepOptions) : 'cloud',
41
42
  model: outcome.model,
42
43
  model_fallback: outcome.modelFallback,
43
44
  total_ms: Date.now() - start,
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { defaultTimeout } from './deep-analysis.js';
3
+ describe('deep inference timeout', () => {
4
+ it('gives the local 7B tier four minutes a call, the small tiers one, and cloud two', () => {
5
+ expect(defaultTimeout({ enabled: true })).toBe(60_000);
6
+ expect(defaultTimeout({ enabled: true, pro: true })).toBe(60_000);
7
+ expect(defaultTimeout({ enabled: true, max: true })).toBe(240_000);
8
+ expect(defaultTimeout({ enabled: true, apiKey: 'k', provider: 'claude' })).toBe(120_000);
9
+ });
10
+ });
@@ -3,9 +3,14 @@
3
3
  *
4
4
  * Detects when AI subtly changes business logic in functions:
5
5
  * - Comparison operator mutations: >= became > (off-by-one)
6
- * - Return statement additions/removals
7
- * - Branch count changes (new if/else added or removed)
8
- * - Call sequence changes (function calls reordered)
6
+ * - Return statement additions/removals (opt-in: track_returns)
7
+ * - Branch count changes, new if/else added or removed (opt-in: track_branches)
8
+ *
9
+ * Return and branch counts are off by default: a change that edits a function
10
+ * usually changes them on purpose, so they fired on almost every edited function
11
+ * without saying what was wrong. Judged against review comments developers acted
12
+ * on, 0 of 7 such findings raised the reviewer's issue. An operator mutation is
13
+ * specific and actionable, so it stays on.
9
14
  *
10
15
  * This is the HARDEST drift to catch because:
11
16
  * - Code still compiles
@@ -3,9 +3,14 @@
3
3
  *
4
4
  * Detects when AI subtly changes business logic in functions:
5
5
  * - Comparison operator mutations: >= became > (off-by-one)
6
- * - Return statement additions/removals
7
- * - Branch count changes (new if/else added or removed)
8
- * - Call sequence changes (function calls reordered)
6
+ * - Return statement additions/removals (opt-in: track_returns)
7
+ * - Branch count changes, new if/else added or removed (opt-in: track_branches)
8
+ *
9
+ * Return and branch counts are off by default: a change that edits a function
10
+ * usually changes them on purpose, so they fired on almost every edited function
11
+ * without saying what was wrong. Judged against review comments developers acted
12
+ * on, 0 of 7 such findings raised the reviewer's issue. An operator mutation is
13
+ * specific and actionable, so it stays on.
9
14
  *
10
15
  * This is the HARDEST drift to catch because:
11
16
  * - Code still compiles
@@ -34,8 +39,8 @@ export class LogicDriftGate extends Gate {
34
39
  enabled: config.enabled ?? true,
35
40
  baseline_path: config.baseline_path ?? '.rigour/logic-baseline.json',
36
41
  track_operators: config.track_operators ?? true,
37
- track_branches: config.track_branches ?? true,
38
- track_returns: config.track_returns ?? true,
42
+ track_branches: config.track_branches ?? false,
43
+ track_returns: config.track_returns ?? false,
39
44
  };
40
45
  }
41
46
  get provenance() { return 'ai-drift'; }
@@ -35,6 +35,13 @@ describe('LogicDriftGate Git baseline', () => {
35
35
  git('merge', '-q', '--ff-only', 'feature');
36
36
  expect(await gate.run({ cwd })).toEqual([]);
37
37
  }, 30_000);
38
+ it('reports return and branch count changes only when opted in', async () => {
39
+ git('switch', '-q', '-c', 'feature');
40
+ fs.writeFileSync(path.join(cwd, 'src/rules.ts'), 'export function eligible(score: number) {\n if (score < 0) return false;\n if (score > 100) return false;\n return score >= 10;\n}\n');
41
+ expect(await new LogicDriftGate().run({ cwd })).toEqual([]);
42
+ const opted = await new LogicDriftGate({ track_returns: true, track_branches: true }).run({ cwd });
43
+ expect(opted.map(f => f.title).sort()).toEqual(['Logic Drift: Branch Change', 'Logic Drift: Return Change']);
44
+ }, 30_000);
38
45
  it('does not use a moving local snapshot when Git has no main reference', async () => {
39
46
  git('switch', '-q', '-c', 'feature');
40
47
  git('branch', '-D', 'main');
@@ -104,7 +104,9 @@ export class PromiseSafetyGate extends Gate {
104
104
  // Also count .then().then().catch() chains — the catch at the end covers all
105
105
  if (j > i && braceDepth < 0)
106
106
  break; // Exited enclosing scope
107
- if (j > i && /^(?:const|let|var|function|class|export|import|if|for|while|return)\b/.test(lookahead.trim()))
107
+ // A statement keyword ends the chain only at the chain's own depth; inside the
108
+ // .then callback's body (depth > 0) `if`/`return` belong to the callback.
109
+ if (j > i && braceDepth <= 0 && /^(?:const|let|var|function|class|export|import|if|for|while|return)\b/.test(lookahead.trim()))
108
110
  break;
109
111
  }
110
112
  if (!hasCatch && !isInsideTryBlock(lines, i) && !/(?:const|let|var)\s+\w+\s*=/.test(line)) {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,34 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
+ import { PromiseSafetyGate } from './promise-safety.js';
6
+ describe('PromiseSafetyGate unhandled .then', () => {
7
+ let cwd;
8
+ beforeEach(() => { cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'promise-safety-')); });
9
+ afterEach(() => { fs.rmSync(cwd, { recursive: true, force: true }); });
10
+ async function thenFindings(code) {
11
+ fs.writeFileSync(path.join(cwd, 'session.ts'), code);
12
+ const failures = await new PromiseSafetyGate().run({ cwd });
13
+ return failures.filter(f => f.details.includes('unhandled-then')).length;
14
+ }
15
+ it('flags a .then chain with no .catch', async () => {
16
+ expect(await thenFindings('export function wake(p: Promise<number>) {\n p.then(n => {\n if (n > 1) log(n);\n });\n}\ndeclare function log(n: number): void;\n')).toBe(1);
17
+ });
18
+ it('finds the .catch that closes a chain whose callback body has if and return statements', async () => {
19
+ const code = [
20
+ 'export function wake(accepted: Promise<{ action: string }>) {',
21
+ ' void accepted',
22
+ ' .then(result => {',
23
+ " if (result.action === 'wake') {",
24
+ ' return run();',
25
+ ' }',
26
+ ' return undefined;',
27
+ ' })',
28
+ ' .catch(() => {});',
29
+ '}',
30
+ 'declare function run(): Promise<void>;',
31
+ ].join('\n');
32
+ expect(await thenFindings(code)).toBe(0);
33
+ });
34
+ });
@@ -67,7 +67,8 @@ export const VULNERABILITY_PATTERNS = [
67
67
  // Hardcoded Secrets
68
68
  {
69
69
  type: 'hardcoded_secrets',
70
- regex: /(?:password|secret|api_key|apikey|auth_token|access_token|private_key)\s*[:=]\s*['"][^'"]{8,}['"]/gi,
70
+ // A secret literal has no whitespace: `hidePassword: 'Hide password'` is a UI label, not a credential.
71
+ regex: /(?:password|secret|api_key|apikey|auth_token|access_token|private_key)\s*[:=]\s*['"][^'"\s]{8,}['"]/gi,
71
72
  severity: 'critical',
72
73
  description: 'Hardcoded secret detected in code',
73
74
  cwe: 'CWE-798',
@@ -85,6 +85,14 @@ describe('SecurityPatternsGate', () => {
85
85
  const vulns = await checkSecurityPatterns(filePath);
86
86
  expect(vulns.some(v => v.type === 'hardcoded_secrets')).toBe(true);
87
87
  });
88
+ it('does not take a UI label that mentions a password for a secret', async () => {
89
+ const filePath = path.join(testDir, 'be.ts');
90
+ fs.writeFileSync(filePath, `
91
+ export default { authForm: { hidePassword: 'Схаваць пароль', showPassword: 'Show the password' } };
92
+ `);
93
+ const vulns = await checkSecurityPatterns(filePath);
94
+ expect(vulns.some(v => v.type === 'hardcoded_secrets')).toBe(false);
95
+ });
88
96
  it('should detect password assignments', async () => {
89
97
  const filePath = path.join(testDir, 'auth.ts');
90
98
  fs.writeFileSync(filePath, `
@@ -8,7 +8,7 @@
8
8
  import { Gate } from './base.js';
9
9
  import { FileScanner } from '../utils/scanner.js';
10
10
  import { Logger } from '../utils/logger.js';
11
- import { analyzeFiles, BUILT_IN_RULES } from '../semantic/engine.js';
11
+ import { ALL_RULES, analyzeFiles, BUILT_IN_RULES } from '../semantic/engine.js';
12
12
  import { compileLearnedRule, LEARNED_PREFIX } from '../semantic/learn/compile.js';
13
13
  import { loadLearnedRules } from '../semantic/learn/store.js';
14
14
  const SOURCE_PATTERNS = ['**/*.{ts,tsx,js,jsx,mjs,cjs,mts,cts}'];
@@ -36,9 +36,10 @@ export class SemanticBugsGate extends Gate {
36
36
  }
37
37
  /** Built-in rules plus the repository's learned rules (`.rigour/rules/`), filtered by config. */
38
38
  selectRules(cwd) {
39
- const all = [...BUILT_IN_RULES, ...loadLearnedRules(cwd).map(compileLearnedRule)];
39
+ const learned = loadLearnedRules(cwd).map(compileLearnedRule);
40
40
  const wanted = this.config.rules;
41
- return wanted?.length ? all.filter(rule => wanted.includes(rule.id)) : all;
41
+ // Named rules may include opt-in candidates; unnamed, only the default set runs.
42
+ return wanted?.length ? [...ALL_RULES, ...learned].filter(rule => wanted.includes(rule.id)) : [...BUILT_IN_RULES, ...learned];
42
43
  }
43
44
  toFailure(finding) {
44
45
  const failure = this.createFailure(`${finding.message}\n${finding.evidence.map(e => ` at ${e}`).join('\n')}`, [finding.file], finding.hint, `[${finding.rule}] ${finding.message.split('. ')[0]}`.slice(0, 120), finding.line, undefined, finding.severity);
@@ -18,6 +18,12 @@ describe('SemanticBugsGate', () => {
18
18
  fs.mkdirSync(path.dirname(path.join(cwd, rel)), { recursive: true });
19
19
  fs.writeFileSync(path.join(cwd, rel), body);
20
20
  }
21
+ it('runs an unvalidated candidate rule only when it is named', async () => {
22
+ write('src/scroll.ts', "export function setup(fn: () => void) {\n window.addEventListener('a', fn);\n addEventListener('b', fn);\n}\n");
23
+ expect(await new SemanticBugsGate().run({ cwd })).toEqual([]);
24
+ const named = await new SemanticBugsGate({ rules: ['env/bare-browser-global'] }).run({ cwd });
25
+ expect(named.map(f => [f.category, f.line])).toEqual([['env/bare-browser-global', 3]]);
26
+ });
21
27
  it('reports a proven finding with its evidence, rule and verified metadata', async () => {
22
28
  write('src/notify.ts', LEAKY);
23
29
  const [failure, ...rest] = await new SemanticBugsGate().run({ cwd });
@@ -8,7 +8,7 @@
8
8
  *
9
9
  */
10
10
  import type { HookCheckerResult } from './types.js';
11
- interface CheckerOptions {
11
+ export interface CheckerOptions {
12
12
  cwd: string;
13
13
  files: string[];
14
14
  timeout_ms?: number;
@@ -21,4 +21,3 @@ interface CheckerOptions {
21
21
  * Returns structured JSON for hook consumers.
22
22
  */
23
23
  export declare function runHookChecker(options: CheckerOptions): Promise<HookCheckerResult>;
24
- export {};
@@ -3,7 +3,9 @@
3
3
  *
4
4
  */
5
5
  export { runHookChecker } from './checker.js';
6
+ export type { CheckerOptions } from './checker.js';
6
7
  export { generateHookFiles } from './templates.js';
8
+ export type { GeneratedHookFile } from './templates.js';
7
9
  export type { HookTool, HookConfig, HookCheckerResult } from './types.js';
8
10
  export { DEFAULT_HOOK_CONFIG, FAST_GATE_IDS } from './types.js';
9
11
  export { scanInputForCredentials, formatDLPAlert, createDLPAuditEntry } from './input-validator.js';