@rigour-labs/core 5.4.0 → 5.5.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.
@@ -68,6 +68,31 @@ export declare function setComponentCache(componentName: string, commitSha: stri
68
68
  export declare function normalizeQuery(query: string): string;
69
69
  export declare function getSemanticQueryCache(query: string, commitSha: string, cwd?: string): Promise<SemanticQueryEntry | null>;
70
70
  export declare function setSemanticQueryCache(query: string, commitSha: string, entry: SemanticQueryEntry, cwd?: string): Promise<void>;
71
+ /** Jaccard similarity of normalized query tokens — used for quality-safe partial reuse. */
72
+ export declare function queryTokenOverlap(a: string, b: string): number;
73
+ /**
74
+ * Distinctive tokens (len >= 5) must mostly agree — blocks "task service" ≈ "payment service".
75
+ */
76
+ export declare function distinctiveTokenAgreement(a: string, b: string): number;
77
+ export interface RelatedSemanticHit {
78
+ entry: SemanticQueryEntry;
79
+ overlap: number;
80
+ sourceQuery: string;
81
+ }
82
+ /**
83
+ * Quality-safe partial semantic reuse: same commit only.
84
+ * Requires high Jaccard (>= 0.75) AND distinctive-token agreement (>= 0.67).
85
+ * Never crosses commits — avoids stale scopes after code moves.
86
+ * File existence is re-validated by the caller before serving.
87
+ */
88
+ export declare function findRelatedSemanticQueryCache(query: string, commitSha: string, cwd?: string, minOverlap?: number, minDistinctive?: number): Promise<RelatedSemanticHit | null>;
89
+ /**
90
+ * Drop cached scopes that point at files no longer on disk (quality guard).
91
+ */
92
+ export declare function filterExistingEditScope(editScope: string[], cwd: string): Promise<{
93
+ valid: string[];
94
+ missing: string[];
95
+ }>;
71
96
  /**
72
97
  * Layer 4: Task Checkpoint Cache
73
98
  */
@@ -8,7 +8,9 @@
8
8
  * 4. Task checkpoint cache (Subagent handoff packets)
9
9
  */
10
10
  import { createHash } from 'crypto';
11
- import { setContextCacheRecord, getContextCacheRecord, recordCheckpointMetric } from '../storage/index.js';
11
+ import fs from 'fs-extra';
12
+ import path from 'path';
13
+ import { setContextCacheRecord, getContextCacheRecord, listContextCacheRecords, recordCheckpointMetric } from '../storage/index.js';
12
14
  /**
13
15
  * Generate SHA-256 content hash.
14
16
  */
@@ -114,6 +116,82 @@ export async function setSemanticQueryCache(query, commitSha, entry, cwd) {
114
116
  payloadTokens,
115
117
  }, cwd);
116
118
  }
119
+ function queryTokenSet(query) {
120
+ return new Set(normalizeQuery(query).split(' ').filter((t) => t.length > 1));
121
+ }
122
+ /** Jaccard similarity of normalized query tokens — used for quality-safe partial reuse. */
123
+ export function queryTokenOverlap(a, b) {
124
+ const left = queryTokenSet(a);
125
+ const right = queryTokenSet(b);
126
+ if (left.size === 0 || right.size === 0)
127
+ return 0;
128
+ let inter = 0;
129
+ for (const t of left) {
130
+ if (right.has(t))
131
+ inter++;
132
+ }
133
+ const union = left.size + right.size - inter;
134
+ return union === 0 ? 0 : inter / union;
135
+ }
136
+ /**
137
+ * Distinctive tokens (len >= 5) must mostly agree — blocks "task service" ≈ "payment service".
138
+ */
139
+ export function distinctiveTokenAgreement(a, b) {
140
+ const left = [...queryTokenSet(a)].filter((t) => t.length >= 5);
141
+ const right = new Set([...queryTokenSet(b)].filter((t) => t.length >= 5));
142
+ if (left.length === 0 || right.size === 0)
143
+ return 0;
144
+ const hits = left.filter((t) => right.has(t)).length;
145
+ return hits / Math.max(left.length, right.size);
146
+ }
147
+ /**
148
+ * Quality-safe partial semantic reuse: same commit only.
149
+ * Requires high Jaccard (>= 0.75) AND distinctive-token agreement (>= 0.67).
150
+ * Never crosses commits — avoids stale scopes after code moves.
151
+ * File existence is re-validated by the caller before serving.
152
+ */
153
+ export async function findRelatedSemanticQueryCache(query, commitSha, cwd, minOverlap = 0.75, minDistinctive = 0.67) {
154
+ const records = await listContextCacheRecords({ cacheType: 'semantic', commitSha, limit: 80 }, cwd);
155
+ let best = null;
156
+ for (const record of records) {
157
+ try {
158
+ const entry = JSON.parse(record.payloadJson);
159
+ const sourceQuery = entry.query || '';
160
+ const overlap = queryTokenOverlap(query, sourceQuery);
161
+ const distinctive = distinctiveTokenAgreement(query, sourceQuery);
162
+ if (overlap < minOverlap || distinctive < minDistinctive)
163
+ continue;
164
+ if (normalizeQuery(query) === normalizeQuery(sourceQuery))
165
+ continue; // exact path uses getSemanticQueryCache
166
+ if (!best || overlap > best.overlap) {
167
+ best = { entry, overlap, sourceQuery };
168
+ }
169
+ }
170
+ catch {
171
+ // skip bad payloads
172
+ }
173
+ }
174
+ return best;
175
+ }
176
+ /**
177
+ * Drop cached scopes that point at files no longer on disk (quality guard).
178
+ */
179
+ export async function filterExistingEditScope(editScope, cwd) {
180
+ const valid = [];
181
+ const missing = [];
182
+ for (const file of editScope) {
183
+ try {
184
+ if (await fs.pathExists(path.join(cwd, file)))
185
+ valid.push(file);
186
+ else
187
+ missing.push(file);
188
+ }
189
+ catch {
190
+ missing.push(file);
191
+ }
192
+ }
193
+ return { valid, missing };
194
+ }
117
195
  /**
118
196
  * Layer 4: Task Checkpoint Cache
119
197
  */
@@ -8,7 +8,7 @@ vi.mock('./db.js', () => ({
8
8
  DB_PATH: '/tmp/rigour-test.db',
9
9
  RIGOUR_DIR: '/tmp/.rigour',
10
10
  }));
11
- import { getStaticCache, setStaticCache, getComponentCache, setComponentCache, getSemanticQueryCache, setSemanticQueryCache, getTaskCheckpointCache, setTaskCheckpointCache, hashContent, normalizeQuery } from './cache-engine.js';
11
+ import { getStaticCache, setStaticCache, getComponentCache, setComponentCache, getSemanticQueryCache, setSemanticQueryCache, findRelatedSemanticQueryCache, queryTokenOverlap, distinctiveTokenAgreement, filterExistingEditScope, getTaskCheckpointCache, setTaskCheckpointCache, hashContent, normalizeQuery } from './cache-engine.js';
12
12
  describe('4-Layer Context Cache Engine', () => {
13
13
  const testCwd = path.join(os.tmpdir(), `rigour-cache-test-${Date.now()}`);
14
14
  beforeEach(async () => {
@@ -89,6 +89,42 @@ describe('4-Layer Context Cache Engine', () => {
89
89
  expect(retrieved?.taskId).toBe('CTP-142');
90
90
  expect(retrieved?.decisions).toContain('Added priority field');
91
91
  });
92
+ it('Layer 3: finds related semantic queries at same commit for partial reuse', async () => {
93
+ expect(queryTokenOverlap('task priority persistence service', 'task priority persistence service layer')).toBeGreaterThan(0.7);
94
+ expect(distinctiveTokenAgreement('task service', 'payment service')).toBeLessThan(0.67);
95
+ await setSemanticQueryCache('task priority persistence service', 'commit-rel', {
96
+ query: 'task priority persistence service',
97
+ resolvedOwner: 'task-team',
98
+ editScope: ['services/task.ts'],
99
+ validationScope: ['npm test'],
100
+ evidence: ['related'],
101
+ commitSha: 'commit-rel',
102
+ confidence: 0.9,
103
+ }, testCwd);
104
+ const related = await findRelatedSemanticQueryCache('task priority persistence service layer', 'commit-rel', testCwd);
105
+ expect(related).not.toBeNull();
106
+ expect(related?.entry.editScope).toContain('services/task.ts');
107
+ // Same generic stem, different distinctive noun → reject
108
+ await setSemanticQueryCache('payment service billing', 'commit-rel', {
109
+ query: 'payment service billing',
110
+ resolvedOwner: 'pay',
111
+ editScope: ['services/pay.ts'],
112
+ validationScope: [],
113
+ evidence: [],
114
+ commitSha: 'commit-rel',
115
+ confidence: 0.9,
116
+ }, testCwd);
117
+ const crossDomain = await findRelatedSemanticQueryCache('task service billing', 'commit-rel', testCwd);
118
+ expect(crossDomain).toBeNull();
119
+ const otherCommit = await findRelatedSemanticQueryCache('task priority persistence service layer', 'commit-other', testCwd);
120
+ expect(otherCommit).toBeNull();
121
+ });
122
+ it('filters missing edit-scope files for quality', async () => {
123
+ await fs.writeFile(path.join(testCwd, 'alive.ts'), 'export const ok = 1;\n');
124
+ const result = await filterExistingEditScope(['alive.ts', 'gone.ts'], testCwd);
125
+ expect(result.valid).toEqual(['alive.ts']);
126
+ expect(result.missing).toEqual(['gone.ts']);
127
+ });
92
128
  it('Layer 1: invalidates static cache when file content changes', async () => {
93
129
  const originalContent = 'export function processTask() { return true; }';
94
130
  const entry = {
package/dist/index.d.ts CHANGED
@@ -12,7 +12,7 @@ export * from './utils/logger.js';
12
12
  export { FileScanner } from './utils/scanner.js';
13
13
  export * from './services/score-history.js';
14
14
  export * from './hooks/index.js';
15
- export { loadSettings, saveSettings, getSettingsPath, resolveDeepOptions, getProviderKey, getAgentConfig, getCliPreferences, updateProviderKey, removeProviderKey, getCursorApiKey, updateCursorApiKey } from './settings.js';
15
+ export { loadSettings, saveSettings, getSettingsPath, resolveDeepOptions, getProviderKey, getAgentConfig, getCliPreferences, updateProviderKey, removeProviderKey, getCursorApiKey, isCursorApiKeyConfigured, getCursorApiKeyHint, updateCursorApiKey, removeCursorApiKey, } from './settings.js';
16
16
  export type { RigourSettings, ResolvedDeepOptions, CLIDeepOptions } from './settings.js';
17
17
  export { DeepAnalysisGate } from './gates/deep-analysis.js';
18
18
  export { createProvider } from './inference/index.js';
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@ export { FileScanner } from './utils/scanner.js';
13
13
  export * from './services/score-history.js';
14
14
  export * from './hooks/index.js';
15
15
  // Settings Module (Global user config at ~/.rigour/settings.json)
16
- export { loadSettings, saveSettings, getSettingsPath, resolveDeepOptions, getProviderKey, getAgentConfig, getCliPreferences, updateProviderKey, removeProviderKey, getCursorApiKey, updateCursorApiKey } from './settings.js';
16
+ export { loadSettings, saveSettings, getSettingsPath, resolveDeepOptions, getProviderKey, getAgentConfig, getCliPreferences, updateProviderKey, removeProviderKey, getCursorApiKey, isCursorApiKeyConfigured, getCursorApiKeyHint, updateCursorApiKey, removeCursorApiKey, } from './settings.js';
17
17
  // Deep Analysis Pipeline (v4.0+)
18
18
  export { DeepAnalysisGate } from './gates/deep-analysis.js';
19
19
  export { createProvider } from './inference/index.js';
@@ -106,10 +106,23 @@ export declare function updateProviderKey(provider: string, apiKey: string): voi
106
106
  */
107
107
  export declare function removeProviderKey(provider: string): void;
108
108
  /**
109
- * Get Cursor Admin API key from settings
109
+ * Get Cursor Admin API key — prefer env over file-stored secret.
110
+ * Env: RIGOUR_CURSOR_API_KEY or CURSOR_ADMIN_API_KEY
110
111
  */
111
112
  export declare function getCursorApiKey(): string | undefined;
112
113
  /**
113
- * Update Cursor Admin API key in settings
114
+ * Whether a Cursor vendor adapter key is available (env or file).
115
+ */
116
+ export declare function isCursorApiKeyConfigured(): boolean;
117
+ /**
118
+ * Last-4 hint for UI status — never return the full key.
119
+ */
120
+ export declare function getCursorApiKeyHint(): string | undefined;
121
+ /**
122
+ * Persist Cursor Admin API key to settings (optional; prefer env in production).
114
123
  */
115
124
  export declare function updateCursorApiKey(apiKey: string): void;
125
+ /**
126
+ * Remove file-stored Cursor Admin API key (env vars are untouched).
127
+ */
128
+ export declare function removeCursorApiKey(): void;
package/dist/settings.js CHANGED
@@ -45,10 +45,22 @@ export function saveSettings(settings) {
45
45
  const settingsPath = getSettingsPath();
46
46
  const settingsDir = path.dirname(settingsPath);
47
47
  try {
48
- // Ensure directory exists
48
+ // Ensure directory exists with owner-only access
49
49
  fs.ensureDirSync(settingsDir);
50
+ try {
51
+ fs.chmodSync(settingsDir, 0o700);
52
+ }
53
+ catch {
54
+ // ignore chmod failures on platforms that do not support it
55
+ }
50
56
  // Write with pretty formatting (2-space indent)
51
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
57
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), { encoding: 'utf-8', mode: 0o600 });
58
+ try {
59
+ fs.chmodSync(settingsPath, 0o600);
60
+ }
61
+ catch {
62
+ // ignore chmod failures on platforms that do not support it
63
+ }
52
64
  Logger.debug(`Settings saved to ${settingsPath}`);
53
65
  }
54
66
  catch (error) {
@@ -185,20 +197,58 @@ export function removeProviderKey(provider) {
185
197
  }
186
198
  }
187
199
  /**
188
- * Get Cursor Admin API key from settings
200
+ * Get Cursor Admin API key — prefer env over file-stored secret.
201
+ * Env: RIGOUR_CURSOR_API_KEY or CURSOR_ADMIN_API_KEY
189
202
  */
190
203
  export function getCursorApiKey() {
204
+ const fromEnv = process.env.RIGOUR_CURSOR_API_KEY?.trim() ||
205
+ process.env.CURSOR_ADMIN_API_KEY?.trim();
206
+ if (fromEnv)
207
+ return fromEnv;
191
208
  const settings = loadSettings();
192
- return settings.cursor?.apiKey;
209
+ const fromFile = settings.cursor?.apiKey?.trim();
210
+ return fromFile || undefined;
211
+ }
212
+ /**
213
+ * Whether a Cursor vendor adapter key is available (env or file).
214
+ */
215
+ export function isCursorApiKeyConfigured() {
216
+ return Boolean(getCursorApiKey());
193
217
  }
194
218
  /**
195
- * Update Cursor Admin API key in settings
219
+ * Last-4 hint for UI status — never return the full key.
220
+ */
221
+ export function getCursorApiKeyHint() {
222
+ const key = getCursorApiKey();
223
+ if (!key || key.length < 4)
224
+ return undefined;
225
+ return `••••${key.slice(-4)}`;
226
+ }
227
+ /**
228
+ * Persist Cursor Admin API key to settings (optional; prefer env in production).
196
229
  */
197
230
  export function updateCursorApiKey(apiKey) {
231
+ const trimmed = apiKey.trim();
232
+ if (!trimmed || trimmed.length > 512) {
233
+ throw new Error('Invalid Cursor Admin API key');
234
+ }
198
235
  const settings = loadSettings();
199
236
  if (!settings.cursor) {
200
237
  settings.cursor = {};
201
238
  }
202
- settings.cursor.apiKey = apiKey;
239
+ settings.cursor.apiKey = trimmed;
203
240
  saveSettings(settings);
204
241
  }
242
+ /**
243
+ * Remove file-stored Cursor Admin API key (env vars are untouched).
244
+ */
245
+ export function removeCursorApiKey() {
246
+ const settings = loadSettings();
247
+ if (settings.cursor?.apiKey) {
248
+ delete settings.cursor.apiKey;
249
+ if (Object.keys(settings.cursor).length === 0) {
250
+ delete settings.cursor;
251
+ }
252
+ saveSettings(settings);
253
+ }
254
+ }
@@ -62,6 +62,14 @@ export declare function recordModelUsage(usage: ModelUsage, cwd?: string): Promi
62
62
  * Insert or update cache entry.
63
63
  */
64
64
  export declare function setContextCacheRecord(record: ContextCacheRecord, cwd?: string): Promise<void>;
65
+ /**
66
+ * List cache records by type (and optional commit). Does not increment hit counts.
67
+ */
68
+ export declare function listContextCacheRecords(opts: {
69
+ cacheType?: ContextCacheRecord['cacheType'];
70
+ commitSha?: string;
71
+ limit?: number;
72
+ }, cwd?: string): Promise<ContextCacheRecord[]>;
65
73
  /**
66
74
  * Fetch cached context record by cache key. Increments hit count.
67
75
  */
@@ -112,6 +112,67 @@ export async function setContextCacheRecord(record, cwd) {
112
112
  cacheData[record.cacheKey] = { ...record, createdAt: timestamp };
113
113
  await fs.writeJson(fallbackPath, cacheData, { spaces: 2 });
114
114
  }
115
+ /**
116
+ * List cache records by type (and optional commit). Does not increment hit counts.
117
+ */
118
+ export async function listContextCacheRecords(opts, cwd) {
119
+ const limit = opts.limit ?? 50;
120
+ if (isSQLiteAvailable()) {
121
+ try {
122
+ const db = await openDatabase();
123
+ if (db) {
124
+ const clauses = [];
125
+ const params = [];
126
+ if (opts.cacheType) {
127
+ clauses.push('cache_type = ?');
128
+ params.push(opts.cacheType);
129
+ }
130
+ if (opts.commitSha) {
131
+ clauses.push('commit_sha = ?');
132
+ params.push(opts.commitSha);
133
+ }
134
+ const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : '';
135
+ const rows = await db.all(`SELECT * FROM context_cache ${where} ORDER BY COALESCE(last_hit_at, created_at) DESC LIMIT ?`, ...params, limit);
136
+ await db.close();
137
+ return (rows || []).map((row) => ({
138
+ cacheKey: row.cache_key,
139
+ cacheType: row.cache_type,
140
+ repo: row.repo,
141
+ branch: row.branch,
142
+ commitSha: row.commit_sha,
143
+ dependencyFingerprint: row.dependency_fingerprint,
144
+ payloadJson: row.payload_json,
145
+ payloadTokens: row.payload_tokens,
146
+ hitCount: row.hit_count || 0,
147
+ createdAt: row.created_at,
148
+ lastHitAt: row.last_hit_at,
149
+ }));
150
+ }
151
+ }
152
+ catch {
153
+ // fallback
154
+ }
155
+ }
156
+ const fallbackPath = path.join(getFallbackDir(cwd), 'context-cache.json');
157
+ if (!(await fs.pathExists(fallbackPath)))
158
+ return [];
159
+ try {
160
+ const cacheData = await fs.readJson(fallbackPath);
161
+ return Object.values(cacheData)
162
+ .filter((entry) => {
163
+ if (opts.cacheType && entry.cacheType !== opts.cacheType)
164
+ return false;
165
+ if (opts.commitSha && entry.commitSha !== opts.commitSha)
166
+ return false;
167
+ return true;
168
+ })
169
+ .sort((a, b) => (b.lastHitAt || b.createdAt || 0) - (a.lastHitAt || a.createdAt || 0))
170
+ .slice(0, limit);
171
+ }
172
+ catch {
173
+ return [];
174
+ }
175
+ }
115
176
  /**
116
177
  * Fetch cached context record by cache key. Increments hit count.
117
178
  */
@@ -9,5 +9,5 @@ export { insertFindings, getFindingsForScan, getDeepFindings } from './findings.
9
9
  export { reinforcePattern, decayPatterns, getStrongPatterns, getPatterns, getHardRules } from './patterns.js';
10
10
  export { checkLocalPatterns, persistAndReinforce, getProjectStats } from './local-memory.js';
11
11
  export type { ProjectStats } from './local-memory.js';
12
- export { recordContextEvent, recordModelUsage, setContextCacheRecord, getContextCacheRecord, recordCheckpointMetric, getContextEvents, getModelUsages, getCheckpointMetrics } from './context-telemetry.js';
12
+ export { recordContextEvent, recordModelUsage, setContextCacheRecord, getContextCacheRecord, listContextCacheRecords, recordCheckpointMetric, getContextEvents, getModelUsages, getCheckpointMetrics } from './context-telemetry.js';
13
13
  export type { ContextEvent, ModelUsage, ContextCacheRecord, CheckpointMetric } from './context-telemetry.js';
@@ -7,4 +7,4 @@ export { insertScan, getRecentScans, getScoreTrendFromDB, getTopIssues } from '.
7
7
  export { insertFindings, getFindingsForScan, getDeepFindings } from './findings.js';
8
8
  export { reinforcePattern, decayPatterns, getStrongPatterns, getPatterns, getHardRules } from './patterns.js';
9
9
  export { checkLocalPatterns, persistAndReinforce, getProjectStats } from './local-memory.js';
10
- export { recordContextEvent, recordModelUsage, setContextCacheRecord, getContextCacheRecord, recordCheckpointMetric, getContextEvents, getModelUsages, getCheckpointMetrics } from './context-telemetry.js';
10
+ export { recordContextEvent, recordModelUsage, setContextCacheRecord, getContextCacheRecord, listContextCacheRecords, recordCheckpointMetric, getContextEvents, getModelUsages, getCheckpointMetrics } from './context-telemetry.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigour-labs/core",
3
- "version": "5.4.0",
3
+ "version": "5.5.0",
4
4
  "description": "AI-native quality gate engine with local Bayesian learning. AST analysis, drift detection, Fix Packet generation, and agent self-healing across TypeScript, JavaScript, Python, Go, Ruby, and C#.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://rigour.run",
@@ -66,11 +66,11 @@
66
66
  "@xenova/transformers": "^2.17.2",
67
67
  "sqlite3": "^5.1.7",
68
68
  "openai": "^4.104.0",
69
- "@rigour-labs/brain-darwin-arm64": "5.4.0",
70
- "@rigour-labs/brain-darwin-x64": "5.4.0",
71
- "@rigour-labs/brain-linux-arm64": "5.4.0",
72
- "@rigour-labs/brain-linux-x64": "5.4.0",
73
- "@rigour-labs/brain-win-x64": "5.4.0"
69
+ "@rigour-labs/brain-darwin-arm64": "5.5.0",
70
+ "@rigour-labs/brain-darwin-x64": "5.5.0",
71
+ "@rigour-labs/brain-linux-x64": "5.5.0",
72
+ "@rigour-labs/brain-win-x64": "5.5.0",
73
+ "@rigour-labs/brain-linux-arm64": "5.5.0"
74
74
  },
75
75
  "devDependencies": {
76
76
  "@types/fs-extra": "^11.0.4",