@rigour-labs/core 6.2.5 → 6.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/discovery-signals.d.ts +27 -0
- package/dist/discovery-signals.js +96 -0
- package/dist/discovery.d.ts +1 -2
- package/dist/discovery.js +36 -49
- package/dist/discovery.test.js +84 -71
- package/dist/gates/deep-analysis.d.ts +2 -0
- package/dist/gates/deep-analysis.js +5 -1
- package/dist/gates/deep-intent.d.ts +3 -0
- package/dist/gates/deep-intent.js +57 -0
- package/dist/gates/deep-intent.test.d.ts +1 -0
- package/dist/gates/deep-intent.test.js +51 -0
- package/dist/gates/deep-runner.js +1 -0
- package/dist/gates/deprecated-apis-ast.d.ts +8 -0
- package/dist/gates/deprecated-apis-ast.js +25 -0
- package/dist/gates/deprecated-apis-rules-node.js +2 -8
- package/dist/gates/deprecated-apis.js +5 -1
- package/dist/gates/deprecated-apis.test.js +30 -0
- package/dist/gates/frontend-secret-exposure.js +5 -1
- package/dist/gates/frontend-secret-exposure.test.js +20 -0
- package/dist/gates/hallucinated-imports/framework-modules.d.ts +14 -0
- package/dist/gates/hallucinated-imports/framework-modules.js +75 -0
- package/dist/gates/hallucinated-imports/index.js +14 -6
- package/dist/gates/hallucinated-imports/js-resolver.d.ts +16 -12
- package/dist/gates/hallucinated-imports/js-resolver.js +65 -153
- package/dist/gates/hallucinated-imports/ts-path-target.js +16 -0
- package/dist/gates/hallucinated-imports/tsconfig-paths.d.ts +16 -0
- package/dist/gates/hallucinated-imports/tsconfig-paths.js +120 -0
- package/dist/gates/hallucinated-imports-sveltekit.test.d.ts +1 -0
- package/dist/gates/hallucinated-imports-sveltekit.test.js +108 -0
- package/dist/gates/runner.js +5 -0
- package/dist/gates/semantic-bugs.d.ts +23 -0
- package/dist/gates/semantic-bugs.js +48 -0
- package/dist/gates/semantic-bugs.test.d.ts +1 -0
- package/dist/gates/semantic-bugs.test.js +70 -0
- package/dist/gates/side-effect-analysis/index.js +11 -1
- package/dist/gates/side-effect-analysis.test.d.ts +1 -0
- package/dist/gates/side-effect-analysis.test.js +162 -0
- package/dist/gates/side-effect-helpers/pattern-detection.js +35 -7
- package/dist/gates/side-effect-helpers/resource-tracking.js +17 -10
- package/dist/gates/side-effect-helpers/scope-analysis.d.ts +6 -0
- package/dist/gates/side-effect-helpers/scope-analysis.js +20 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +4 -1
- package/dist/inference/sidecar-provider.js +12 -1
- package/dist/inference/types.d.ts +2 -0
- package/dist/semantic/ast.d.ts +29 -0
- package/dist/semantic/ast.js +122 -0
- package/dist/semantic/benchmark.d.ts +16 -0
- package/dist/semantic/benchmark.js +31 -0
- package/dist/semantic/benchmark.test.d.ts +1 -0
- package/dist/semantic/benchmark.test.js +18 -0
- package/dist/semantic/engine.d.ts +8 -0
- package/dist/semantic/engine.js +45 -0
- package/dist/semantic/intent/benchmark.d.ts +31 -0
- package/dist/semantic/intent/benchmark.js +74 -0
- package/dist/semantic/intent/intent-check.d.ts +43 -0
- package/dist/semantic/intent/intent-check.js +72 -0
- package/dist/semantic/intent/intent.test.d.ts +1 -0
- package/dist/semantic/intent/intent.test.js +46 -0
- package/dist/semantic/intent/parallel-reads.d.ts +23 -0
- package/dist/semantic/intent/parallel-reads.js +59 -0
- package/dist/semantic/learn/benchmark.d.ts +20 -0
- package/dist/semantic/learn/benchmark.js +71 -0
- package/dist/semantic/learn/compile.d.ts +4 -0
- package/dist/semantic/learn/compile.js +56 -0
- package/dist/semantic/learn/conditions.d.ts +22 -0
- package/dist/semantic/learn/conditions.js +93 -0
- package/dist/semantic/learn/extract.d.ts +30 -0
- package/dist/semantic/learn/extract.js +122 -0
- package/dist/semantic/learn/git-trees.d.ts +8 -0
- package/dist/semantic/learn/git-trees.js +32 -0
- package/dist/semantic/learn/identity.d.ts +18 -0
- package/dist/semantic/learn/identity.js +97 -0
- package/dist/semantic/learn/learn.d.ts +25 -0
- package/dist/semantic/learn/learn.js +109 -0
- package/dist/semantic/learn/learn.test.d.ts +1 -0
- package/dist/semantic/learn/learn.test.js +67 -0
- package/dist/semantic/learn/store.d.ts +4 -0
- package/dist/semantic/learn/store.js +39 -0
- package/dist/semantic/learn/types.d.ts +468 -0
- package/dist/semantic/learn/types.js +55 -0
- package/dist/semantic/origins.d.ts +42 -0
- package/dist/semantic/origins.js +149 -0
- package/dist/semantic/origins.test.d.ts +1 -0
- package/dist/semantic/origins.test.js +77 -0
- package/dist/semantic/program.d.ts +13 -0
- package/dist/semantic/program.js +62 -0
- package/dist/semantic/rules/credential-redirect.d.ts +2 -0
- package/dist/semantic/rules/credential-redirect.js +209 -0
- package/dist/semantic/rules/degraded-response-cached.d.ts +2 -0
- package/dist/semantic/rules/degraded-response-cached.js +271 -0
- package/dist/semantic/rules/in-memory-aggregation.d.ts +2 -0
- package/dist/semantic/rules/in-memory-aggregation.js +179 -0
- package/dist/semantic/summaries.d.ts +18 -0
- package/dist/semantic/summaries.js +33 -0
- package/dist/semantic/types.d.ts +32 -0
- package/dist/semantic/types.js +1 -0
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.js +1 -0
- package/dist/storage/team-diagnostics.d.ts +22 -0
- package/dist/storage/team-diagnostics.js +58 -0
- package/dist/storage/team-diagnostics.test.d.ts +1 -0
- package/dist/storage/team-diagnostics.test.js +51 -0
- package/dist/storage/team-schema.d.ts +6 -0
- package/dist/storage/team-schema.js +82 -0
- package/dist/storage/team-store.js +25 -72
- package/dist/templates/universal-config.js +4 -0
- package/dist/types/index.d.ts +56 -0
- package/dist/types/index.js +7 -0
- package/package.json +6 -6
|
@@ -71,7 +71,7 @@ export function pathsOverlap(watchPath, writePath) {
|
|
|
71
71
|
// ═══════════════════════════════════════════════════════════════════
|
|
72
72
|
// LOOP & RECURSION ANALYSIS — Context-aware body extraction
|
|
73
73
|
// ═══════════════════════════════════════════════════════════════════
|
|
74
|
-
import { findBlockEndBrace, findBlockEndIndent } from './scope-analysis.js';
|
|
74
|
+
import { findBlockEndBrace, findBlockEndIndent, findExpressionEnd } from './scope-analysis.js';
|
|
75
75
|
/**
|
|
76
76
|
* Extract loop body with correct scope tracking.
|
|
77
77
|
* Uses brace/indent matching (not just "next N lines").
|
|
@@ -122,13 +122,24 @@ export function extractFunctionDefs(lines, lang) {
|
|
|
122
122
|
? findBlockEndIndent(lines, i)
|
|
123
123
|
: lang === 'rb'
|
|
124
124
|
? findBlockEndRuby(lines, i)
|
|
125
|
-
: findBlockEndBrace(lines, i);
|
|
125
|
+
: isExpressionArrow(lines, i, stripped, m.index) ? findExpressionEnd(lines, i) : findBlockEndBrace(lines, i);
|
|
126
126
|
defs.push({ name, start: i, end, params: lines[i] });
|
|
127
127
|
break;
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
return defs;
|
|
131
131
|
}
|
|
132
|
+
/** An arrow whose body is an expression, not a `{ ... }` block. */
|
|
133
|
+
function isExpressionArrow(lines, line, stripped, matchAt) {
|
|
134
|
+
const arrow = stripped.indexOf('=>', matchAt);
|
|
135
|
+
if (arrow < 0)
|
|
136
|
+
return false;
|
|
137
|
+
const rest = stripped.slice(arrow + 2).trim();
|
|
138
|
+
if (rest !== '')
|
|
139
|
+
return !rest.startsWith('{');
|
|
140
|
+
const next = lines.slice(line + 1).find(l => l.trim() !== '');
|
|
141
|
+
return next !== undefined && !next.trim().startsWith('{');
|
|
142
|
+
}
|
|
132
143
|
function getFuncDefPatterns(lang) {
|
|
133
144
|
switch (lang) {
|
|
134
145
|
case 'py': return [/^\s*(?:async\s+)?def\s+(\w+)\s*\(/];
|
|
@@ -297,11 +308,24 @@ export function hasCatchWithContinue(body, lang) {
|
|
|
297
308
|
// ═══════════════════════════════════════════════════════════════════
|
|
298
309
|
// PROCESS & TIMER OPERATIONS
|
|
299
310
|
// ═══════════════════════════════════════════════════════════════════
|
|
311
|
+
/**
|
|
312
|
+
* Drop a trailing line comment (and whole comment lines) so prose such as
|
|
313
|
+
* "ported from the fork (verified …)" is not read as code. Strings are
|
|
314
|
+
* already blanked, so `//` inside a URL string is gone before this runs.
|
|
315
|
+
*/
|
|
316
|
+
function stripLineComment(line, lang) {
|
|
317
|
+
const trimmed = line.trim();
|
|
318
|
+
if (trimmed.startsWith('*') || trimmed.startsWith('/*'))
|
|
319
|
+
return '';
|
|
320
|
+
const marker = lang === 'py' || lang === 'rb' ? '#' : '//';
|
|
321
|
+
const at = line.indexOf(marker);
|
|
322
|
+
return at === -1 ? line : line.slice(0, at);
|
|
323
|
+
}
|
|
300
324
|
/**
|
|
301
325
|
* Check if a line contains a process spawn call.
|
|
302
326
|
*/
|
|
303
327
|
export function isProcessSpawn(line, lang) {
|
|
304
|
-
const stripped = stripStrings(line);
|
|
328
|
+
const stripped = stripLineComment(stripStrings(line), lang);
|
|
305
329
|
const patterns = getSpawnPatterns(lang);
|
|
306
330
|
for (const pat of patterns) {
|
|
307
331
|
const m = pat.exec(stripped);
|
|
@@ -314,9 +338,13 @@ function getSpawnPatterns(lang) {
|
|
|
314
338
|
switch (lang) {
|
|
315
339
|
case 'js':
|
|
316
340
|
case 'ts':
|
|
341
|
+
// A bare call (from a child_process import) or a call on a
|
|
342
|
+
// child_process namespace; `re.exec(src)` or `db.exec(sql)` is not a
|
|
343
|
+
// spawn. *Sync variants finish before returning, so they cannot orphan.
|
|
317
344
|
return [
|
|
318
|
-
|
|
319
|
-
|
|
345
|
+
/(?<![.\w$])(?:spawn|exec|execFile|fork)\s*\(/,
|
|
346
|
+
/(?<![.\w$])(?:execa|execaCommand)\s*\(/,
|
|
347
|
+
/\b(?:child_process|childProcess|cp)\.(?:spawn|exec|execFile|fork)\s*\(/,
|
|
320
348
|
];
|
|
321
349
|
case 'py':
|
|
322
350
|
return [
|
|
@@ -354,9 +382,9 @@ function getTimerCreatePatterns(lang) {
|
|
|
354
382
|
switch (lang) {
|
|
355
383
|
case 'js':
|
|
356
384
|
case 'ts':
|
|
385
|
+
// setTimeout fires once and cannot run unbounded; only setInterval repeats.
|
|
357
386
|
return [
|
|
358
387
|
[/\bsetInterval\s*\(/, 'setInterval'],
|
|
359
|
-
[/\bsetTimeout\s*\(/, 'setTimeout'],
|
|
360
388
|
];
|
|
361
389
|
case 'py':
|
|
362
390
|
return [
|
|
@@ -410,7 +438,7 @@ export function getProcessCleanupPatterns(lang) {
|
|
|
410
438
|
case 'js':
|
|
411
439
|
case 'ts':
|
|
412
440
|
return [
|
|
413
|
-
/\.on\s*\(\s*['"](?:exit|close)['"]/,
|
|
441
|
+
/\.(?:on|once|addListener)\s*\(\s*['"](?:exit|close)['"]/,
|
|
414
442
|
/\.kill\s*\(/,
|
|
415
443
|
/\.disconnect\s*\(/,
|
|
416
444
|
];
|
|
@@ -39,8 +39,8 @@ export function extractVariableBinding(line, lang) {
|
|
|
39
39
|
const jsMatch = stripped.match(/^(?:const|let|var|final|auto|val)\s+(\w+)\s*=\s*/);
|
|
40
40
|
if (jsMatch)
|
|
41
41
|
return jsMatch[1];
|
|
42
|
-
// Member assignment: `this.timer = ...`, `self.timer = ...`
|
|
43
|
-
const memberMatch = stripped.match(/^(?:this|self)\.([\w]+)\s*=\s*/);
|
|
42
|
+
// Member assignment: `this.timer = ...`, `self.timer = ...`, `this.#timer = ...`
|
|
43
|
+
const memberMatch = stripped.match(/^(?:this|self)\.(#?[\w]+)\s*=\s*/);
|
|
44
44
|
if (memberMatch)
|
|
45
45
|
return memberMatch[1];
|
|
46
46
|
// Simple assignment: `timer = ...`
|
|
@@ -68,26 +68,33 @@ export function hasCleanupForVariable(lines, varName, scopeStart, scopeEnd, clea
|
|
|
68
68
|
const scope = lines.slice(scopeStart, scopeEnd);
|
|
69
69
|
for (let i = 0; i < scope.length; i++) {
|
|
70
70
|
const stripped = stripStrings(scope[i]);
|
|
71
|
-
// Check cleanup patterns that reference the specific variable
|
|
71
|
+
// Check cleanup patterns that reference the specific variable. Some
|
|
72
|
+
// patterns need the quoted event name (`.once('exit', …)`), which
|
|
73
|
+
// stripStrings blanks, so they are also tried on the raw line.
|
|
72
74
|
for (const pat of cleanupPatterns) {
|
|
73
|
-
if (!pat.test(stripped))
|
|
75
|
+
if (!pat.test(stripped) && !pat.test(scope[i]))
|
|
74
76
|
continue;
|
|
75
|
-
// The cleanup call should reference our variable
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (varRef.test(stripped))
|
|
77
|
+
// The cleanup call should reference our variable. Lookarounds, not
|
|
78
|
+
// \b, so a private field such as `#timer` matches too.
|
|
79
|
+
if (variableReference(varName).test(stripped))
|
|
79
80
|
return true;
|
|
80
81
|
// Also check method calls on the variable: timer.close(), timer.stop()
|
|
81
82
|
// The pattern might match a generic .close() — check if it's on our var
|
|
82
83
|
}
|
|
83
84
|
// Direct method cleanup on the variable: varName.close(), varName.Stop()
|
|
84
|
-
const escapedVar = varName
|
|
85
|
-
const methodCleanup = new RegExp(
|
|
85
|
+
const escapedVar = escapeRegExp(varName);
|
|
86
|
+
const methodCleanup = new RegExp(`(?<![\\w$#])${escapedVar}\\.(?:close|stop|destroy|kill|terminate|dispose|cancel|shutdown|unsubscribe|disconnect|end|release|Clear|Stop|Dispose|Close|Cancel)\\s*\\(`);
|
|
86
87
|
if (methodCleanup.test(stripped))
|
|
87
88
|
return true;
|
|
88
89
|
}
|
|
89
90
|
return false;
|
|
90
91
|
}
|
|
92
|
+
function escapeRegExp(value) {
|
|
93
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
94
|
+
}
|
|
95
|
+
function variableReference(varName) {
|
|
96
|
+
return new RegExp(`(?<![\\w$#])${escapeRegExp(varName)}(?![\\w$])`);
|
|
97
|
+
}
|
|
91
98
|
/**
|
|
92
99
|
* Check if a line is inside a cleanup/teardown context.
|
|
93
100
|
*
|
|
@@ -14,6 +14,12 @@ export declare function findEnclosingFunction(lines: string[], lineIdx: number,
|
|
|
14
14
|
* Find the end of a brace-delimited block starting at `start`.
|
|
15
15
|
*/
|
|
16
16
|
export declare function findBlockEndBrace(lines: string[], start: number): number;
|
|
17
|
+
/**
|
|
18
|
+
* Find the end of an expression-bodied arrow (`const f = (x) => x + 1;`):
|
|
19
|
+
* the first line where brackets balance and the expression does not
|
|
20
|
+
* continue. Scanning for a `{` instead would run into the next block.
|
|
21
|
+
*/
|
|
22
|
+
export declare function findExpressionEnd(lines: string[], start: number): number;
|
|
17
23
|
/**
|
|
18
24
|
* Find the end of an indentation-delimited block (Python).
|
|
19
25
|
*/
|
|
@@ -95,6 +95,26 @@ export function findBlockEndBrace(lines, start) {
|
|
|
95
95
|
}
|
|
96
96
|
return maxScan;
|
|
97
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Find the end of an expression-bodied arrow (`const f = (x) => x + 1;`):
|
|
100
|
+
* the first line where brackets balance and the expression does not
|
|
101
|
+
* continue. Scanning for a `{` instead would run into the next block.
|
|
102
|
+
*/
|
|
103
|
+
export function findExpressionEnd(lines, start) {
|
|
104
|
+
let depth = 0;
|
|
105
|
+
const maxScan = Math.min(lines.length, start + 300);
|
|
106
|
+
for (let j = start; j < maxScan; j++) {
|
|
107
|
+
const stripped = stripStrings(lines[j]);
|
|
108
|
+
depth += bracketDelta(stripped);
|
|
109
|
+
const continues = /(?:=>|[=+\-*/%&|?:,.(])\s*$/.test(stripped.trimEnd());
|
|
110
|
+
if (depth <= 0 && !continues)
|
|
111
|
+
return j + 1;
|
|
112
|
+
}
|
|
113
|
+
return maxScan;
|
|
114
|
+
}
|
|
115
|
+
function bracketDelta(text) {
|
|
116
|
+
return (text.match(/[([{]/g)?.length ?? 0) - (text.match(/[)\]}]/g)?.length ?? 0);
|
|
117
|
+
}
|
|
98
118
|
/**
|
|
99
119
|
* Find the end of an indentation-delimited block (Python).
|
|
100
120
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export { checkLocalPatterns, persistAndReinforce, getProjectStats } from './stor
|
|
|
31
31
|
export type { ProjectStats } from './storage/index.js';
|
|
32
32
|
export { recordInteractionEvidence, countInteractionEvidence, recordInteractionLesson, listLessons, listKnowledgeLessons, getApplicableLessons, transitionLesson, getRepositoryId, queueLocalLessonsForTeam } from './storage/index.js';
|
|
33
33
|
export type { ApplicableLessons, InteractionEvidence, LessonRecord, LessonState, LessonVisibility, LocalLessonImportOptions, LocalLessonImportResult } from './storage/index.js';
|
|
34
|
-
export { loadTeamConfiguration, saveTeamConfiguration, initializeTeamSchema, getTeamModeStatus, doctorTeamConnection, syncTeamOutbox, searchTeamKnowledge, backfillTeamEmbeddings, TEAM_CONFIG_PATH, validateTeamDatabaseUrl, } from './storage/index.js';
|
|
34
|
+
export { loadTeamConfiguration, saveTeamConfiguration, initializeTeamSchema, getTeamModeStatus, doctorTeamConnection, syncTeamOutbox, searchTeamKnowledge, backfillTeamEmbeddings, TEAM_CONFIG_PATH, validateTeamDatabaseUrl, explainTeamConnectionError, } from './storage/index.js';
|
|
35
35
|
export type { TeamConfiguration, TeamSemanticConfiguration, TeamModeStatus, TeamDoctorResult, SemanticKnowledgeCandidate, SemanticKnowledgeResult, } from './storage/index.js';
|
|
36
36
|
export { generateTemporalDriftReport, formatDriftSummary } from './services/temporal-drift.js';
|
|
37
37
|
export type { TemporalDriftReport, ProvenanceStream, MonthlyBucket, WeeklyBucket, DriftDirection } from './services/temporal-drift.js';
|
|
@@ -51,4 +51,8 @@ export * from './services/system-health.js';
|
|
|
51
51
|
export * from './services/agent-history.js';
|
|
52
52
|
export * from './services/engineering-knowledge-graph.js';
|
|
53
53
|
export * from './firewall/index.js';
|
|
54
|
+
export { learnFromFix, type LearnReport, type Rejection } from './semantic/learn/learn.js';
|
|
55
|
+
export { extractFixTrees, type FixTrees } from './semantic/learn/git-trees.js';
|
|
56
|
+
export { saveLearnedRule, loadLearnedRules, LEARNED_RULES_DIR } from './semantic/learn/store.js';
|
|
57
|
+
export type { LearnedRule, LearnedPattern } from './semantic/learn/types.js';
|
|
54
58
|
export { recordContextEvent, recordModelUsage, recordCheckpointMetric, getContextEvents, getModelUsages, getCheckpointMetrics, } from './storage/index.js';
|
package/dist/index.js
CHANGED
|
@@ -30,7 +30,7 @@ export { openDatabase, isSQLiteAvailable, compactDatabase, getDatabaseSize, rese
|
|
|
30
30
|
// Local Project Memory (hybrid intelligence — SQLite-backed per-project learning)
|
|
31
31
|
export { checkLocalPatterns, persistAndReinforce, getProjectStats } from './storage/index.js';
|
|
32
32
|
export { recordInteractionEvidence, countInteractionEvidence, recordInteractionLesson, listLessons, listKnowledgeLessons, getApplicableLessons, transitionLesson, getRepositoryId, queueLocalLessonsForTeam } from './storage/index.js';
|
|
33
|
-
export { loadTeamConfiguration, saveTeamConfiguration, initializeTeamSchema, getTeamModeStatus, doctorTeamConnection, syncTeamOutbox, searchTeamKnowledge, backfillTeamEmbeddings, TEAM_CONFIG_PATH, validateTeamDatabaseUrl, } from './storage/index.js';
|
|
33
|
+
export { loadTeamConfiguration, saveTeamConfiguration, initializeTeamSchema, getTeamModeStatus, doctorTeamConnection, syncTeamOutbox, searchTeamKnowledge, backfillTeamEmbeddings, TEAM_CONFIG_PATH, validateTeamDatabaseUrl, explainTeamConnectionError, } from './storage/index.js';
|
|
34
34
|
// Temporal Drift Engine (v5 — cross-session trend analysis + per-provenance EWMA)
|
|
35
35
|
export { generateTemporalDriftReport, formatDriftSummary } from './services/temporal-drift.js';
|
|
36
36
|
// Adaptive Thresholds (v5 — Z-score + per-provenance trends)
|
|
@@ -50,6 +50,9 @@ export * from './services/system-health.js';
|
|
|
50
50
|
export * from './services/agent-history.js';
|
|
51
51
|
export * from './services/engineering-knowledge-graph.js';
|
|
52
52
|
export * from './firewall/index.js';
|
|
53
|
+
export { learnFromFix } from './semantic/learn/learn.js';
|
|
54
|
+
export { extractFixTrees } from './semantic/learn/git-trees.js';
|
|
55
|
+
export { saveLearnedRule, loadLearnedRules, LEARNED_RULES_DIR } from './semantic/learn/store.js';
|
|
53
56
|
export { recordContextEvent, recordModelUsage, recordCheckpointMetric, getContextEvents, getModelUsages, getCheckpointMetrics, } from './storage/index.js';
|
|
54
57
|
// Pattern Index is intentionally NOT exported here to prevent
|
|
55
58
|
// native dependency issues (sharp/transformers) from leaking into
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Code never leaves the machine: prompts go to a local child process.
|
|
11
11
|
*/
|
|
12
|
+
import crypto from 'crypto';
|
|
12
13
|
import path from 'path';
|
|
13
14
|
import os from 'os';
|
|
14
15
|
import fs from 'fs-extra';
|
|
@@ -88,7 +89,7 @@ export class SidecarProvider {
|
|
|
88
89
|
maxTokens: options?.maxTokens || 1024,
|
|
89
90
|
threads: this.threads,
|
|
90
91
|
temperature: options?.temperature ?? 0.1,
|
|
91
|
-
schemaPath: options?.jsonMode ? this.schemaPath ?? undefined : undefined,
|
|
92
|
+
schemaPath: options?.jsonSchema ? await schemaFileFor(options.jsonSchema) : options?.jsonMode ? this.schemaPath ?? undefined : undefined,
|
|
92
93
|
});
|
|
93
94
|
let result;
|
|
94
95
|
try {
|
|
@@ -159,6 +160,16 @@ async function findOnPath(name) {
|
|
|
159
160
|
return null;
|
|
160
161
|
}
|
|
161
162
|
}
|
|
163
|
+
/** A schema file per distinct schema, written once and reused across calls. */
|
|
164
|
+
async function schemaFileFor(schema) {
|
|
165
|
+
const text = JSON.stringify(schema);
|
|
166
|
+
const schemaPath = path.join(managedEngineDir(), '..', 'schemas', `${crypto.createHash('sha256').update(text).digest('hex').slice(0, 16)}.json`);
|
|
167
|
+
if (!(await fs.pathExists(schemaPath))) {
|
|
168
|
+
await fs.ensureDir(path.dirname(schemaPath));
|
|
169
|
+
await fs.writeFile(schemaPath, text);
|
|
170
|
+
}
|
|
171
|
+
return schemaPath;
|
|
172
|
+
}
|
|
162
173
|
async function writeFindingsSchema() {
|
|
163
174
|
const schemaPath = path.join(managedEngineDir(), '..', 'findings.schema.json');
|
|
164
175
|
await fs.ensureDir(path.dirname(schemaPath));
|
|
@@ -29,6 +29,8 @@ export interface InferenceOptions {
|
|
|
29
29
|
temperature?: number;
|
|
30
30
|
timeout?: number;
|
|
31
31
|
jsonMode?: boolean;
|
|
32
|
+
/** Constrain local output to this JSON schema instead of the findings schema. */
|
|
33
|
+
jsonSchema?: Record<string, unknown>;
|
|
32
34
|
}
|
|
33
35
|
/**
|
|
34
36
|
* A single finding from deep LLM analysis.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
export type FunctionLike = ts.FunctionDeclaration | ts.FunctionExpression | ts.ArrowFunction | ts.MethodDeclaration;
|
|
3
|
+
export declare function isFunctionLike(node: ts.Node): node is FunctionLike;
|
|
4
|
+
/** Drop parentheses, type assertions, non-null and `await` around an expression. */
|
|
5
|
+
export declare function unwrap(expr: ts.Expression): ts.Expression;
|
|
6
|
+
/**
|
|
7
|
+
* Symbol for a node, following import aliases to the declaration. For the
|
|
8
|
+
* name in a shorthand property (`{ headers }`) this is the variable it reads,
|
|
9
|
+
* not the property it creates.
|
|
10
|
+
*/
|
|
11
|
+
export declare function symbolOf(checker: ts.TypeChecker, node: ts.Node): ts.Symbol | undefined;
|
|
12
|
+
/** The function a call invokes, when it resolves to one declared in source. */
|
|
13
|
+
export declare function calledFunction(checker: ts.TypeChecker, call: ts.CallExpression): FunctionLike | undefined;
|
|
14
|
+
/** `fetch` in `fetch(...)`, `deps.fetch(...)`, `this.fetch(...)`. */
|
|
15
|
+
export declare function calleeName(call: ts.CallExpression): string | undefined;
|
|
16
|
+
export declare function enclosingFunction(node: ts.Node): FunctionLike | undefined;
|
|
17
|
+
/** Visit every descendant of `root` that belongs to `root` itself, not to a nested function. */
|
|
18
|
+
export declare function forEachOwnNode(root: ts.Node, visit: (node: ts.Node) => void): void;
|
|
19
|
+
export declare function forEachNode(root: ts.Node, visit: (node: ts.Node) => void): void;
|
|
20
|
+
/** Text of a property name (`a`, `'a'`, `"x-token"`), or undefined for computed names. */
|
|
21
|
+
export declare function propertyNameText(name: ts.PropertyName | undefined): string | undefined;
|
|
22
|
+
export declare function lineOf(node: ts.Node): number;
|
|
23
|
+
/** "http.ts:51" style location for evidence, relative to cwd. */
|
|
24
|
+
export declare function locationOf(cwd: string, node: ts.Node): string;
|
|
25
|
+
export declare function relativeFile(cwd: string, sf: ts.SourceFile): string;
|
|
26
|
+
/** Short, single-line source text for messages. */
|
|
27
|
+
export declare function snippet(node: ts.Node, max?: number): string;
|
|
28
|
+
/** True when any identifier inside `expr` resolves to `target`. */
|
|
29
|
+
export declare function mentionsSymbol(checker: ts.TypeChecker, expr: ts.Node, target: ts.Symbol): boolean;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small, checker-aware AST helpers shared by the semantic rules.
|
|
3
|
+
*/
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import ts from 'typescript';
|
|
6
|
+
export function isFunctionLike(node) {
|
|
7
|
+
return ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node)
|
|
8
|
+
|| ts.isArrowFunction(node) || ts.isMethodDeclaration(node);
|
|
9
|
+
}
|
|
10
|
+
/** Drop parentheses, type assertions, non-null and `await` around an expression. */
|
|
11
|
+
export function unwrap(expr) {
|
|
12
|
+
let current = expr;
|
|
13
|
+
for (;;) {
|
|
14
|
+
if (ts.isParenthesizedExpression(current) || ts.isAsExpression(current) || ts.isNonNullExpression(current)
|
|
15
|
+
|| ts.isTypeAssertionExpression(current) || ts.isSatisfiesExpression(current) || ts.isAwaitExpression(current)) {
|
|
16
|
+
current = current.expression;
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
return current;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Symbol for a node, following import aliases to the declaration. For the
|
|
25
|
+
* name in a shorthand property (`{ headers }`) this is the variable it reads,
|
|
26
|
+
* not the property it creates.
|
|
27
|
+
*/
|
|
28
|
+
export function symbolOf(checker, node) {
|
|
29
|
+
const symbol = ts.isShorthandPropertyAssignment(node.parent) && node.parent.name === node
|
|
30
|
+
? checker.getShorthandAssignmentValueSymbol(node.parent)
|
|
31
|
+
: checker.getSymbolAtLocation(node);
|
|
32
|
+
if (symbol && symbol.flags & ts.SymbolFlags.Alias)
|
|
33
|
+
return checker.getAliasedSymbol(symbol);
|
|
34
|
+
return symbol;
|
|
35
|
+
}
|
|
36
|
+
/** The function a call invokes, when it resolves to one declared in source. */
|
|
37
|
+
export function calledFunction(checker, call) {
|
|
38
|
+
const symbol = symbolOf(checker, call.expression);
|
|
39
|
+
for (const decl of symbol?.declarations ?? []) {
|
|
40
|
+
if (isFunctionLike(decl) && decl.body)
|
|
41
|
+
return decl;
|
|
42
|
+
if ((ts.isVariableDeclaration(decl) || ts.isPropertyAssignment(decl)) && decl.initializer) {
|
|
43
|
+
const init = unwrap(decl.initializer);
|
|
44
|
+
if (isFunctionLike(init) && init.body)
|
|
45
|
+
return init;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
/** `fetch` in `fetch(...)`, `deps.fetch(...)`, `this.fetch(...)`. */
|
|
51
|
+
export function calleeName(call) {
|
|
52
|
+
const callee = unwrap(call.expression);
|
|
53
|
+
if (ts.isIdentifier(callee))
|
|
54
|
+
return callee.text;
|
|
55
|
+
if (ts.isPropertyAccessExpression(callee))
|
|
56
|
+
return callee.name.text;
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
export function enclosingFunction(node) {
|
|
60
|
+
for (let current = node.parent; current; current = current.parent) {
|
|
61
|
+
if (isFunctionLike(current))
|
|
62
|
+
return current;
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
/** Visit every descendant of `root` that belongs to `root` itself, not to a nested function. */
|
|
67
|
+
export function forEachOwnNode(root, visit) {
|
|
68
|
+
const walk = (node) => {
|
|
69
|
+
visit(node);
|
|
70
|
+
if (node !== root && isFunctionLike(node))
|
|
71
|
+
return;
|
|
72
|
+
ts.forEachChild(node, walk);
|
|
73
|
+
};
|
|
74
|
+
ts.forEachChild(root, walk);
|
|
75
|
+
}
|
|
76
|
+
export function forEachNode(root, visit) {
|
|
77
|
+
const walk = (node) => {
|
|
78
|
+
visit(node);
|
|
79
|
+
ts.forEachChild(node, walk);
|
|
80
|
+
};
|
|
81
|
+
ts.forEachChild(root, walk);
|
|
82
|
+
}
|
|
83
|
+
/** Text of a property name (`a`, `'a'`, `"x-token"`), or undefined for computed names. */
|
|
84
|
+
export function propertyNameText(name) {
|
|
85
|
+
if (!name)
|
|
86
|
+
return undefined;
|
|
87
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name))
|
|
88
|
+
return name.text;
|
|
89
|
+
if (ts.isNoSubstitutionTemplateLiteral(name))
|
|
90
|
+
return name.text;
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
export function lineOf(node) {
|
|
94
|
+
const sf = node.getSourceFile();
|
|
95
|
+
return sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
|
|
96
|
+
}
|
|
97
|
+
/** "http.ts:51" style location for evidence, relative to cwd. */
|
|
98
|
+
export function locationOf(cwd, node) {
|
|
99
|
+
return `${relativeFile(cwd, node.getSourceFile())}:${lineOf(node)}`;
|
|
100
|
+
}
|
|
101
|
+
export function relativeFile(cwd, sf) {
|
|
102
|
+
return path.relative(cwd, sf.fileName).replace(/\\/g, '/');
|
|
103
|
+
}
|
|
104
|
+
/** Short, single-line source text for messages. */
|
|
105
|
+
export function snippet(node, max = 60) {
|
|
106
|
+
const text = node.getText(node.getSourceFile()).replace(/\s+/g, ' ');
|
|
107
|
+
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
108
|
+
}
|
|
109
|
+
/** True when any identifier inside `expr` resolves to `target`. */
|
|
110
|
+
export function mentionsSymbol(checker, expr, target) {
|
|
111
|
+
let found = false;
|
|
112
|
+
const walk = (node) => {
|
|
113
|
+
if (found)
|
|
114
|
+
return;
|
|
115
|
+
if (ts.isIdentifier(node) && symbolOf(checker, node) === target)
|
|
116
|
+
found = true;
|
|
117
|
+
else
|
|
118
|
+
ts.forEachChild(node, walk);
|
|
119
|
+
};
|
|
120
|
+
walk(expr);
|
|
121
|
+
return found;
|
|
122
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface BenchmarkCase {
|
|
2
|
+
name: string;
|
|
3
|
+
rule: string | null;
|
|
4
|
+
file: string | null;
|
|
5
|
+
line: number | null;
|
|
6
|
+
note: string;
|
|
7
|
+
}
|
|
8
|
+
export interface CaseResult {
|
|
9
|
+
case: BenchmarkCase;
|
|
10
|
+
/** Expected finding reported on `before/` (true for negatives). */
|
|
11
|
+
caught: boolean;
|
|
12
|
+
/** Findings on `after/` plus unexpected findings on `before/`. */
|
|
13
|
+
falsePositives: string[];
|
|
14
|
+
}
|
|
15
|
+
export declare function loadCases(root: string): BenchmarkCase[];
|
|
16
|
+
export declare function runCase(root: string, bench: BenchmarkCase): CaseResult;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic benchmark: synthetic before/after cases per bug class.
|
|
3
|
+
*
|
|
4
|
+
* `before/` must produce the expected finding (recall); `after/` and cases
|
|
5
|
+
* with `rule: null` must produce none (false positives).
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { analyzeFiles } from './engine.js';
|
|
10
|
+
export function loadCases(root) {
|
|
11
|
+
return fs.readdirSync(root, { withFileTypes: true })
|
|
12
|
+
.filter(entry => entry.isDirectory() && fs.existsSync(path.join(root, entry.name, 'case.json')))
|
|
13
|
+
.map(entry => ({ name: entry.name, ...JSON.parse(fs.readFileSync(path.join(root, entry.name, 'case.json'), 'utf8')) }))
|
|
14
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
15
|
+
}
|
|
16
|
+
export function runCase(root, bench) {
|
|
17
|
+
const before = findingsIn(path.join(root, bench.name, 'before'));
|
|
18
|
+
const after = findingsIn(path.join(root, bench.name, 'after'));
|
|
19
|
+
const isExpected = (f) => bench.rule !== null && f === `${bench.rule} ${bench.file}:${bench.line}`;
|
|
20
|
+
return {
|
|
21
|
+
case: bench,
|
|
22
|
+
caught: bench.rule === null || before.some(isExpected),
|
|
23
|
+
falsePositives: [...before.filter(f => !isExpected(f)).map(f => `before: ${f}`), ...after.map(f => `after: ${f}`)],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function findingsIn(dir) {
|
|
27
|
+
if (!fs.existsSync(dir))
|
|
28
|
+
return [];
|
|
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}`);
|
|
31
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { loadCases, runCase } from './benchmark.js';
|
|
4
|
+
const ROOT = path.resolve(__dirname, '../../../../benchmarks/semantic');
|
|
5
|
+
describe('semantic benchmark', () => {
|
|
6
|
+
const cases = loadCases(ROOT);
|
|
7
|
+
it('has cases for every built-in rule, including negatives', () => {
|
|
8
|
+
const rules = new Set(cases.map(c => c.rule));
|
|
9
|
+
expect(rules).toEqual(new Set(['in-memory-aggregation', 'credential-redirect', 'degraded-response-cached', null]));
|
|
10
|
+
});
|
|
11
|
+
for (const bench of cases) {
|
|
12
|
+
it(`${bench.name}: ${bench.note}`, () => {
|
|
13
|
+
const result = runCase(ROOT, bench);
|
|
14
|
+
expect(result.falsePositives).toEqual([]);
|
|
15
|
+
expect(result.caught).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SemanticFinding, SemanticRule } from './types.js';
|
|
2
|
+
export declare const BUILT_IN_RULES: SemanticRule[];
|
|
3
|
+
export interface EngineOptions {
|
|
4
|
+
rules?: SemanticRule[];
|
|
5
|
+
batchSize?: number;
|
|
6
|
+
}
|
|
7
|
+
/** Analyze `files` (cwd-relative or absolute). Declaration files are skipped. */
|
|
8
|
+
export declare function analyzeFiles(cwd: string, files: string[], opts?: EngineOptions): SemanticFinding[];
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs semantic rules over a set of JS/TS files.
|
|
3
|
+
*/
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { loadProjectConfig, programBatches } from './program.js';
|
|
6
|
+
import { Summaries } from './summaries.js';
|
|
7
|
+
import { inMemoryAggregation } from './rules/in-memory-aggregation.js';
|
|
8
|
+
import { credentialRedirect } from './rules/credential-redirect.js';
|
|
9
|
+
import { degradedResponseCached } from './rules/degraded-response-cached.js';
|
|
10
|
+
export const BUILT_IN_RULES = [inMemoryAggregation, credentialRedirect, degradedResponseCached];
|
|
11
|
+
/** Analyze `files` (cwd-relative or absolute). Declaration files are skipped. */
|
|
12
|
+
export function analyzeFiles(cwd, files, opts = {}) {
|
|
13
|
+
const rules = opts.rules ?? BUILT_IN_RULES;
|
|
14
|
+
const roots = files
|
|
15
|
+
.filter(f => /\.(?:[cm]?[jt]s|[jt]sx)$/i.test(f) && !/\.d\.[cm]?ts$/i.test(f))
|
|
16
|
+
.map(f => path.resolve(cwd, f));
|
|
17
|
+
if (roots.length === 0 || rules.length === 0)
|
|
18
|
+
return [];
|
|
19
|
+
const { options } = loadProjectConfig(cwd);
|
|
20
|
+
const rootSet = new Set(roots.map(r => path.normalize(r)));
|
|
21
|
+
const findings = [];
|
|
22
|
+
for (const program of programBatches(roots, options, opts.batchSize)) {
|
|
23
|
+
const checker = program.getTypeChecker();
|
|
24
|
+
const summaries = new Summaries(checker);
|
|
25
|
+
for (const sourceFile of program.getSourceFiles()) {
|
|
26
|
+
if (!rootSet.has(path.normalize(sourceFile.fileName)))
|
|
27
|
+
continue;
|
|
28
|
+
for (const rule of rules) {
|
|
29
|
+
findings.push(...rule.check({ cwd, checker, sourceFile, summaries }));
|
|
30
|
+
}
|
|
31
|
+
rootSet.delete(path.normalize(sourceFile.fileName));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return dedupe(findings);
|
|
35
|
+
}
|
|
36
|
+
function dedupe(findings) {
|
|
37
|
+
const seen = new Set();
|
|
38
|
+
return findings.filter(f => {
|
|
39
|
+
const key = `${f.rule}:${f.file}:${f.line}`;
|
|
40
|
+
if (seen.has(key))
|
|
41
|
+
return false;
|
|
42
|
+
seen.add(key);
|
|
43
|
+
return true;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type ReadVerdict, type YesNoModel } from './intent-check.js';
|
|
2
|
+
export interface IntentCase {
|
|
3
|
+
name: string;
|
|
4
|
+
note: string;
|
|
5
|
+
labels: Record<string, 'optional' | 'required'>;
|
|
6
|
+
expectFinding: boolean;
|
|
7
|
+
/** False when the finder must skip the site (no model call). */
|
|
8
|
+
expectSite?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface IntentCaseResult {
|
|
11
|
+
case: IntentCase;
|
|
12
|
+
siteFound: boolean;
|
|
13
|
+
verdicts: ReadVerdict[];
|
|
14
|
+
findings: number;
|
|
15
|
+
}
|
|
16
|
+
export interface IntentScore {
|
|
17
|
+
/** Individual answers matching the label (question 1 expects yes for optional, question 2 no). */
|
|
18
|
+
rawCorrect: number;
|
|
19
|
+
rawTotal: number;
|
|
20
|
+
/** Reads whose flipped answers agreed, and how many of those were right. */
|
|
21
|
+
consistent: number;
|
|
22
|
+
consistentCorrect: number;
|
|
23
|
+
reads: number;
|
|
24
|
+
truePositives: number;
|
|
25
|
+
falseNegatives: number;
|
|
26
|
+
falsePositives: number;
|
|
27
|
+
finderErrors: string[];
|
|
28
|
+
}
|
|
29
|
+
export declare function loadIntentCases(root: string): IntentCase[];
|
|
30
|
+
export declare function runIntentCase(root: string, bench: IntentCase, model: YesNoModel): Promise<IntentCaseResult>;
|
|
31
|
+
export declare function scoreIntent(results: IntentCaseResult[]): IntentScore;
|