@nerviq/cli 1.16.0 → 1.17.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 +22 -16
- package/package.json +1 -1
- package/src/audit.js +36 -0
- package/src/codex/techniques.js +156 -44
- package/src/cursor/context.js +24 -4
- package/src/cursor/techniques.js +31 -23
package/README.md
CHANGED
|
@@ -42,28 +42,34 @@ Nerviq audits, sets up, and governs AI coding agent configurations for **8 platf
|
|
|
42
42
|
|
|
43
43
|
## What Nerviq Does
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
Most repos use **more than one** AI coding agent — Claude and Cursor, Copilot and Codex, Gemini and Windsurf. Their configs drift silently. Nerviq is the neutral control plane that detects that drift, scores it, and helps align it.
|
|
46
|
+
|
|
47
|
+
When your repo has 2+ platforms configured, `nerviq audit` leads with the Harmony Score — cross-platform alignment — before any single-platform results. Single-platform repos still get a normal per-platform audit.
|
|
46
48
|
|
|
47
49
|
```
|
|
48
|
-
nerviq audit
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
$ nerviq audit
|
|
51
|
+
Detected: Next.js + TypeScript + Claude + Cursor
|
|
52
|
+
|
|
53
|
+
Harmony Score: 78/100 — 3 drift issues across 2 platforms (Claude Code + Cursor)
|
|
54
|
+
Run `nerviq harmony-audit` for the full cross-platform report.
|
|
51
55
|
|
|
52
|
-
|
|
56
|
+
Platform: Claude Code
|
|
57
|
+
CLAUDE.md .............. 23/25 checks passed
|
|
58
|
+
.claude/settings.json .. 6/9 checks passed
|
|
53
59
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
✅ MCP servers configured
|
|
60
|
+
Platform: Cursor
|
|
61
|
+
.cursor/rules/ ......... 14/16 checks passed
|
|
62
|
+
.cursorrules ........... 4/7 checks passed
|
|
58
63
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
3. Add path-specific rules in .claude/rules/
|
|
64
|
+
Drift: trust-mode mismatch (Claude relaxed / Cursor strict)
|
|
65
|
+
MCP coverage gap — 3 servers in Claude, 1 in Cursor
|
|
66
|
+
format-on-save hook missing from Cursor
|
|
63
67
|
|
|
64
|
-
|
|
68
|
+
3 suggestions → `nerviq harmony-sync` to align.
|
|
65
69
|
```
|
|
66
70
|
|
|
71
|
+
Single-platform repos still work the same way — the Harmony Score only appears when 2+ platforms are detected. Use `--no-harmony-first` to suppress it even on multi-platform repos.
|
|
72
|
+
|
|
67
73
|
## Quick Start
|
|
68
74
|
|
|
69
75
|
```bash
|
|
@@ -217,8 +223,8 @@ All successful operational responses are wrapped in a JSON envelope:
|
|
|
217
223
|
{
|
|
218
224
|
"data": {},
|
|
219
225
|
"meta": {
|
|
220
|
-
"version": "1.
|
|
221
|
-
"timestamp": "2026-04-
|
|
226
|
+
"version": "1.17.0",
|
|
227
|
+
"timestamp": "2026-04-12T12:00:00.000Z"
|
|
222
228
|
}
|
|
223
229
|
}
|
|
224
230
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nerviq/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.0",
|
|
4
4
|
"description": "The intelligent nervous system for AI coding agents — 2,441 checks (8 platforms × ~300 governance rules), 10 languages, 62 domain packs. Audit, align, and amplify.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
package/src/audit.js
CHANGED
|
@@ -514,6 +514,42 @@ async function audit(options) {
|
|
|
514
514
|
const recommendedDomainPacks = spec.platform === 'codex'
|
|
515
515
|
? detectCodexDomainPacks(ctx, stacks, getCodexDomainPackSignals(ctx))
|
|
516
516
|
: [];
|
|
517
|
+
|
|
518
|
+
// FB-05: framework-aware fix rewriting — don't recommend `npm test` on a
|
|
519
|
+
// Python/Go/Rust-only repo. Only rewrites when Node/JS stacks are absent.
|
|
520
|
+
const stackKeys = new Set(stacks.map(s => s.key));
|
|
521
|
+
const hasNodeStack = stackKeys.has('node') || stackKeys.has('react') || stackKeys.has('vue') ||
|
|
522
|
+
stackKeys.has('nextjs') || stackKeys.has('angular') || stackKeys.has('svelte') ||
|
|
523
|
+
stackKeys.has('nestjs') || stackKeys.has('remix') || stackKeys.has('astro') ||
|
|
524
|
+
stackKeys.has('typescript') || stackKeys.has('deno') || stackKeys.has('bun');
|
|
525
|
+
if (!hasNodeStack) {
|
|
526
|
+
let preferredTest = null;
|
|
527
|
+
let preferredInstall = null;
|
|
528
|
+
if (stackKeys.has('python') || stackKeys.has('django') || stackKeys.has('fastapi')) {
|
|
529
|
+
preferredTest = 'pytest'; preferredInstall = 'pip install -r requirements.txt';
|
|
530
|
+
} else if (stackKeys.has('go')) {
|
|
531
|
+
preferredTest = 'go test ./...'; preferredInstall = 'go mod download';
|
|
532
|
+
} else if (stackKeys.has('rust')) {
|
|
533
|
+
preferredTest = 'cargo test'; preferredInstall = 'cargo fetch';
|
|
534
|
+
} else if (stackKeys.has('ruby')) {
|
|
535
|
+
preferredTest = 'bundle exec rspec'; preferredInstall = 'bundle install';
|
|
536
|
+
} else if (stackKeys.has('java') || stackKeys.has('kotlin')) {
|
|
537
|
+
preferredTest = './gradlew test'; preferredInstall = './gradlew build';
|
|
538
|
+
} else if (stackKeys.has('elixir')) {
|
|
539
|
+
preferredTest = 'mix test'; preferredInstall = 'mix deps.get';
|
|
540
|
+
} else if (stackKeys.has('dotnet')) {
|
|
541
|
+
preferredTest = 'dotnet test'; preferredInstall = 'dotnet restore';
|
|
542
|
+
}
|
|
543
|
+
if (preferredTest) {
|
|
544
|
+
for (const r of results) {
|
|
545
|
+
if (typeof r.fix !== 'string') continue;
|
|
546
|
+
if (/\bnpm\s+test\b/i.test(r.fix)) r.fix = r.fix.replace(/`npm\s+test`/gi, '`' + preferredTest + '`').replace(/\bnpm\s+test\b/gi, preferredTest);
|
|
547
|
+
if (/\bnpm\s+ci\b/i.test(r.fix) && preferredInstall) r.fix = r.fix.replace(/`npm\s+ci`/gi, '`' + preferredInstall + '`').replace(/\bnpm\s+ci\b/gi, preferredInstall);
|
|
548
|
+
if (/\bnpm\s+install\b/i.test(r.fix) && preferredInstall) r.fix = r.fix.replace(/`npm\s+install`/gi, '`' + preferredInstall + '`').replace(/\bnpm\s+install\b/gi, preferredInstall);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
517
553
|
const result = {
|
|
518
554
|
platform: spec.platform,
|
|
519
555
|
platformLabel: spec.platformLabel,
|
package/src/codex/techniques.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
const os = require('os');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const { EMBEDDED_SECRET_PATTERNS, containsEmbeddedSecret } = require('../secret-patterns');
|
|
4
|
-
const { attachSourceUrls } = require('../source-urls');
|
|
5
|
-
const { buildSupplementalChecks } = require('../supplemental-checks');
|
|
6
|
-
const { resolveProjectStateReadPath } = require('../state-paths');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { EMBEDDED_SECRET_PATTERNS, containsEmbeddedSecret } = require('../secret-patterns');
|
|
4
|
+
const { attachSourceUrls } = require('../source-urls');
|
|
5
|
+
const { buildSupplementalChecks } = require('../supplemental-checks');
|
|
6
|
+
const { resolveProjectStateReadPath } = require('../state-paths');
|
|
7
7
|
|
|
8
8
|
const CODEX_SUPPLEMENTAL_SOURCE_URLS = {
|
|
9
9
|
'testing-strategy': 'https://developers.openai.com/codex/cli',
|
|
@@ -157,7 +157,14 @@ function hasCommandMention(content, category) {
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
function agentsHasArchitecture(content) {
|
|
160
|
-
|
|
160
|
+
// Explicit architecture/structure markers
|
|
161
|
+
if (/```mermaid|flowchart\b|graph\s+(TD|LR|RL|BT)\b/i.test(content)) return true;
|
|
162
|
+
// Heading variants seen in real repos (openai-agents-python, etc.)
|
|
163
|
+
if (/^#{1,4}\s+(Architecture|Project Map|Structure|Project Structure( Guide)?|Repo Structure( & Important Files)?|Repository Layout|Codebase (Guide|Map|Overview|Structure)|Repo Map|Key Directories|Module Map|Directory Layout|Folder Structure|Package Structure)\b/im.test(content)) return true;
|
|
164
|
+
// Enumerated file/directory maps (3+ backtick-wrapped paths in a row)
|
|
165
|
+
const pathList = content.match(/^[-*]?\s*`[^`]+\/`?/gm);
|
|
166
|
+
if (pathList && pathList.length >= 3) return true;
|
|
167
|
+
return false;
|
|
161
168
|
}
|
|
162
169
|
|
|
163
170
|
function findFillerLine(content) {
|
|
@@ -345,7 +352,31 @@ function projectMcpServers(ctx) {
|
|
|
345
352
|
return config.data.mcp_servers;
|
|
346
353
|
}
|
|
347
354
|
|
|
355
|
+
function isSdkOrLibraryRepo(ctx) {
|
|
356
|
+
const pkg = ctx.fileContent('package.json');
|
|
357
|
+
if (pkg) {
|
|
358
|
+
try {
|
|
359
|
+
const parsed = JSON.parse(pkg);
|
|
360
|
+
// Library if it declares a main/exports/module entry AND no start script that runs a server
|
|
361
|
+
const hasEntry = parsed.main || parsed.exports || parsed.module;
|
|
362
|
+
const scripts = parsed.scripts || {};
|
|
363
|
+
const hasServerStart = /(node|uvicorn|gunicorn).*(server|app|index\.js)/i.test(JSON.stringify(scripts));
|
|
364
|
+
if (hasEntry && !hasServerStart) return true;
|
|
365
|
+
} catch { /* fall through */ }
|
|
366
|
+
}
|
|
367
|
+
const pyproject = ctx.fileContent('pyproject.toml') || '';
|
|
368
|
+
if (/\[project\][^\[]*\b(packages|py_modules)\s*=/i.test(pyproject) && !ctx.hasDir('app') && !ctx.hasDir('server')) return true;
|
|
369
|
+
// README signals
|
|
370
|
+
const readme = ctx.fileContent('README.md') || '';
|
|
371
|
+
if (/\b(npm install|pip install|pnpm add|yarn add)\b.*\b(this|the)? ?(package|library|sdk)/i.test(readme)) return true;
|
|
372
|
+
if (/^# .*\bSDK\b/im.test(readme)) return true;
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
|
|
348
376
|
function repoNeedsExternalTools(ctx) {
|
|
377
|
+
// SDK/library repos document integrations but don't need project-scoped MCP.
|
|
378
|
+
if (isSdkOrLibraryRepo(ctx)) return false;
|
|
379
|
+
|
|
349
380
|
const deps = ctx.projectDependencies ? Object.keys(ctx.projectDependencies()) : [];
|
|
350
381
|
const depSet = new Set(deps);
|
|
351
382
|
const files = new Set(ctx.files || []);
|
|
@@ -639,12 +670,27 @@ function skillArtifacts(ctx) {
|
|
|
639
670
|
}));
|
|
640
671
|
}
|
|
641
672
|
|
|
673
|
+
function extractFrontmatter(content) {
|
|
674
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
|
|
675
|
+
if (!match) return null;
|
|
676
|
+
const fm = {};
|
|
677
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
678
|
+
const m = line.match(/^([a-zA-Z_-]+)\s*:\s*(.+)$/);
|
|
679
|
+
if (m) fm[m[1].trim()] = m[2].trim().replace(/^["']|["']$/g, '');
|
|
680
|
+
}
|
|
681
|
+
return fm;
|
|
682
|
+
}
|
|
683
|
+
|
|
642
684
|
function extractSkillTitle(content) {
|
|
685
|
+
const fm = extractFrontmatter(content);
|
|
686
|
+
if (fm && fm.name) return fm.name;
|
|
643
687
|
const match = content.match(/^#\s+(.+)$/m);
|
|
644
688
|
return match ? match[1].trim() : '';
|
|
645
689
|
}
|
|
646
690
|
|
|
647
691
|
function extractSkillDescription(content) {
|
|
692
|
+
const fm = extractFrontmatter(content);
|
|
693
|
+
if (fm && fm.description) return fm.description;
|
|
648
694
|
const lines = content.split(/\r?\n/);
|
|
649
695
|
const meaningful = [];
|
|
650
696
|
let seenHeading = false;
|
|
@@ -1116,8 +1162,16 @@ function requirementsTomlIssue(ctx) {
|
|
|
1116
1162
|
|
|
1117
1163
|
function sharedOrManagedMachineSignals(ctx) {
|
|
1118
1164
|
const docs = docsBundle(ctx);
|
|
1119
|
-
|
|
1120
|
-
|
|
1165
|
+
if (ctx.fileContent('requirements.toml')) return true;
|
|
1166
|
+
// Explicit workstation/admin context only — not generic "shared" or "managed" words.
|
|
1167
|
+
// Must match specific managed-device or multi-user terminology.
|
|
1168
|
+
if (/\bmanaged[- ](device|laptop|workstation|host)\b/i.test(docs)) return true;
|
|
1169
|
+
if (/\bshared[- ](workstation|host|laptop|machine|computer)\b/i.test(docs)) return true;
|
|
1170
|
+
if (/\bmulti-user[- ](host|machine|workstation)\b/i.test(docs)) return true;
|
|
1171
|
+
if (/\b(kiosk|vdi|virtual desktop)\b/i.test(docs)) return true;
|
|
1172
|
+
if (/\benterprise[- ](managed|deployment|workstation)\b/i.test(docs)) return true;
|
|
1173
|
+
if (/\badmin[- ]enforced\b/i.test(docs)) return true;
|
|
1174
|
+
return false;
|
|
1121
1175
|
}
|
|
1122
1176
|
|
|
1123
1177
|
function authCredentialsStoreIssue(ctx) {
|
|
@@ -3049,17 +3103,25 @@ const CODEX_TECHNIQUES = {
|
|
|
3049
3103
|
id: 'CX-N03',
|
|
3050
3104
|
name: 'Pack recommendations are grounded in detected signals',
|
|
3051
3105
|
check: (ctx) => {
|
|
3052
|
-
//
|
|
3106
|
+
// Ecosystem-neutral grounding: any primary manifest counts.
|
|
3053
3107
|
const agents = agentsContent(ctx);
|
|
3054
3108
|
const config = ctx.configContent ? (ctx.configContent() || '') : (ctx.fileContent('.codex/config.toml') || '');
|
|
3055
|
-
const
|
|
3109
|
+
const manifestFiles = [
|
|
3110
|
+
'package.json', 'pyproject.toml', 'Cargo.toml', 'go.mod',
|
|
3111
|
+
'Gemfile', 'composer.json', 'pom.xml', 'build.gradle',
|
|
3112
|
+
'flake.nix', 'shard.yml', 'mix.exs', 'rebar.config',
|
|
3113
|
+
'Makefile', 'CMakeLists.txt', 'Package.swift', 'pubspec.yaml',
|
|
3114
|
+
];
|
|
3115
|
+
const hasManifest = manifestFiles.some(f => ctx.files.includes(f));
|
|
3116
|
+
// If no signals at all, N/A rather than fail
|
|
3117
|
+
if (!agents && !config && !hasManifest) return null;
|
|
3056
3118
|
// At least 2 signal sources for grounded recommendation
|
|
3057
|
-
return [Boolean(agents), Boolean(config),
|
|
3119
|
+
return [Boolean(agents), Boolean(config), hasManifest].filter(Boolean).length >= 2;
|
|
3058
3120
|
},
|
|
3059
3121
|
impact: 'medium',
|
|
3060
3122
|
rating: 3,
|
|
3061
3123
|
category: 'pack-posture',
|
|
3062
|
-
fix: 'Add package.json
|
|
3124
|
+
fix: 'Add AGENTS.md and ensure a primary manifest (package.json, pyproject.toml, Cargo.toml, go.mod, etc.) is present so pack recommendations can be grounded in real project signals.',
|
|
3063
3125
|
template: 'codex-agents-md',
|
|
3064
3126
|
file: () => 'AGENTS.md',
|
|
3065
3127
|
line: () => 1,
|
|
@@ -3098,15 +3160,15 @@ const CODEX_TECHNIQUES = {
|
|
|
3098
3160
|
// CP-08: New checks (O. Repeat-Usage Hygiene)
|
|
3099
3161
|
// =============================================
|
|
3100
3162
|
|
|
3101
|
-
codexSnapshotRetention: {
|
|
3102
|
-
id: 'CX-O01',
|
|
3103
|
-
name: 'At least one prior audit snapshot exists for repeat-usage',
|
|
3104
|
-
check: (ctx) => {
|
|
3105
|
-
try {
|
|
3106
|
-
const indexPath = resolveProjectStateReadPath(ctx.dir, 'snapshots', 'index.json');
|
|
3107
|
-
const fs = require('fs');
|
|
3108
|
-
if (!fs.existsSync(indexPath)) return null; // No snapshots yet, not a failure
|
|
3109
|
-
const entries = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
|
|
3163
|
+
codexSnapshotRetention: {
|
|
3164
|
+
id: 'CX-O01',
|
|
3165
|
+
name: 'At least one prior audit snapshot exists for repeat-usage',
|
|
3166
|
+
check: (ctx) => {
|
|
3167
|
+
try {
|
|
3168
|
+
const indexPath = resolveProjectStateReadPath(ctx.dir, 'snapshots', 'index.json');
|
|
3169
|
+
const fs = require('fs');
|
|
3170
|
+
if (!fs.existsSync(indexPath)) return null; // No snapshots yet, not a failure
|
|
3171
|
+
const entries = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
|
|
3110
3172
|
return Array.isArray(entries) && entries.length > 0;
|
|
3111
3173
|
} catch {
|
|
3112
3174
|
return null;
|
|
@@ -3121,15 +3183,15 @@ const CODEX_TECHNIQUES = {
|
|
|
3121
3183
|
line: () => null,
|
|
3122
3184
|
},
|
|
3123
3185
|
|
|
3124
|
-
codexFeedbackLoopHealth: {
|
|
3125
|
-
id: 'CX-O02',
|
|
3126
|
-
name: 'Feedback loop is functional when feedback has been submitted',
|
|
3127
|
-
check: (ctx) => {
|
|
3128
|
-
try {
|
|
3129
|
-
const indexPath = resolveProjectStateReadPath(ctx.dir, 'outcomes', 'index.json');
|
|
3130
|
-
const fs = require('fs');
|
|
3131
|
-
if (!fs.existsSync(indexPath)) return null; // No feedback yet, not a failure
|
|
3132
|
-
const entries = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
|
|
3186
|
+
codexFeedbackLoopHealth: {
|
|
3187
|
+
id: 'CX-O02',
|
|
3188
|
+
name: 'Feedback loop is functional when feedback has been submitted',
|
|
3189
|
+
check: (ctx) => {
|
|
3190
|
+
try {
|
|
3191
|
+
const indexPath = resolveProjectStateReadPath(ctx.dir, 'outcomes', 'index.json');
|
|
3192
|
+
const fs = require('fs');
|
|
3193
|
+
if (!fs.existsSync(indexPath)) return null; // No feedback yet, not a failure
|
|
3194
|
+
const entries = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
|
|
3133
3195
|
return Array.isArray(entries) && entries.length > 0;
|
|
3134
3196
|
} catch {
|
|
3135
3197
|
return null;
|
|
@@ -3144,15 +3206,15 @@ const CODEX_TECHNIQUES = {
|
|
|
3144
3206
|
line: () => null,
|
|
3145
3207
|
},
|
|
3146
3208
|
|
|
3147
|
-
codexTrendDataAvailability: {
|
|
3148
|
-
id: 'CX-O03',
|
|
3149
|
-
name: 'Trend data is computable (2+ snapshots with compatible schemas)',
|
|
3150
|
-
check: (ctx) => {
|
|
3151
|
-
try {
|
|
3152
|
-
const indexPath = resolveProjectStateReadPath(ctx.dir, 'snapshots', 'index.json');
|
|
3153
|
-
const fs = require('fs');
|
|
3154
|
-
if (!fs.existsSync(indexPath)) return null;
|
|
3155
|
-
const entries = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
|
|
3209
|
+
codexTrendDataAvailability: {
|
|
3210
|
+
id: 'CX-O03',
|
|
3211
|
+
name: 'Trend data is computable (2+ snapshots with compatible schemas)',
|
|
3212
|
+
check: (ctx) => {
|
|
3213
|
+
try {
|
|
3214
|
+
const indexPath = resolveProjectStateReadPath(ctx.dir, 'snapshots', 'index.json');
|
|
3215
|
+
const fs = require('fs');
|
|
3216
|
+
if (!fs.existsSync(indexPath)) return null;
|
|
3217
|
+
const entries = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
|
|
3156
3218
|
const audits = (Array.isArray(entries) ? entries : []).filter(e => e.snapshotKind === 'audit');
|
|
3157
3219
|
return audits.length >= 2;
|
|
3158
3220
|
} catch {
|
|
@@ -3342,8 +3404,25 @@ const CODEX_TECHNIQUES = {
|
|
|
3342
3404
|
|
|
3343
3405
|
codexPythonFormatterConfigured: {
|
|
3344
3406
|
id: 'CX-PY08',
|
|
3345
|
-
name: 'Formatter configured (black / isort / ruff
|
|
3346
|
-
check: (ctx) => {
|
|
3407
|
+
name: 'Formatter configured (black / isort / ruff / yapf)',
|
|
3408
|
+
check: (ctx) => {
|
|
3409
|
+
const hasPy = ctx.files.some(f => /pyproject\.toml$|requirements\.txt$|setup\.py$|manage\.py$/.test(f));
|
|
3410
|
+
if (!hasPy) return null;
|
|
3411
|
+
const pp = ctx.fileContent('pyproject.toml') || '';
|
|
3412
|
+
// Explicit formatter sections
|
|
3413
|
+
if (/\[tool\.black|\[tool\.isort|\[tool\.ruff\.format|\[tool\.yapf|\[tool\.autopep8/i.test(pp)) return true;
|
|
3414
|
+
// Ruff config implies formatting capability in modern setups (ruff 0.3+)
|
|
3415
|
+
if (/\[tool\.ruff(\.lint)?\]/i.test(pp)) return true;
|
|
3416
|
+
// Standalone config files
|
|
3417
|
+
if (ctx.files.some(f => /^(ruff\.toml|\.ruff\.toml|\.isort\.cfg|\.yapfrc|setup\.cfg)$/i.test(f))) {
|
|
3418
|
+
const setupCfg = ctx.fileContent('setup.cfg') || '';
|
|
3419
|
+
if (/\[isort\]|\[yapf\]|\[flake8\]/i.test(setupCfg)) return true;
|
|
3420
|
+
if (f => /ruff|yapf|isort/i.test(f)) return true;
|
|
3421
|
+
}
|
|
3422
|
+
// Dev dependency signal
|
|
3423
|
+
if (/\b(black|isort|ruff|yapf|autopep8)\b/i.test(pp)) return true;
|
|
3424
|
+
return false;
|
|
3425
|
+
},
|
|
3347
3426
|
impact: 'medium',
|
|
3348
3427
|
category: 'python',
|
|
3349
3428
|
fix: 'Configure black, isort, or ruff format in pyproject.toml.',
|
|
@@ -3365,7 +3444,24 @@ const CODEX_TECHNIQUES = {
|
|
|
3365
3444
|
codexPythonFastapiEntryDocumented: {
|
|
3366
3445
|
id: 'CX-PY10',
|
|
3367
3446
|
name: 'FastAPI entry point documented if FastAPI project',
|
|
3368
|
-
check: (ctx) => {
|
|
3447
|
+
check: (ctx) => {
|
|
3448
|
+
const hasPy = ctx.files.some(f => /pyproject\.toml$|requirements\.txt$|setup\.py$|manage\.py$/.test(f));
|
|
3449
|
+
if (!hasPy) return null;
|
|
3450
|
+
const pp = ctx.fileContent('pyproject.toml') || '';
|
|
3451
|
+
const reqs = ctx.fileContent('requirements.txt') || '';
|
|
3452
|
+
// FastAPI only in dev/optional/example deps → N/A (SDK with example server)
|
|
3453
|
+
const inMain = /^\s*fastapi\b/im.test(reqs) ||
|
|
3454
|
+
/\[project\.dependencies\][\s\S]*?fastapi/i.test(pp) ||
|
|
3455
|
+
/\[tool\.poetry\.dependencies\][\s\S]*?fastapi/i.test(pp) ||
|
|
3456
|
+
/^\s*dependencies\s*=\s*\[[^\]]*"fastapi/im.test(pp);
|
|
3457
|
+
if (!inMain) return null;
|
|
3458
|
+
const docs = [
|
|
3459
|
+
ctx.claudeMdContent ? ctx.claudeMdContent() : ctx.fileContent('CLAUDE.md'),
|
|
3460
|
+
ctx.fileContent('README.md'),
|
|
3461
|
+
ctx.fileContent('AGENTS.md'),
|
|
3462
|
+
].filter(Boolean).join('\n');
|
|
3463
|
+
return /fastapi|uvicorn|app\.py|main\.py/i.test(docs);
|
|
3464
|
+
},
|
|
3369
3465
|
impact: 'high',
|
|
3370
3466
|
category: 'python',
|
|
3371
3467
|
fix: 'Document FastAPI entry point and how to run the development server.',
|
|
@@ -3376,7 +3472,18 @@ const CODEX_TECHNIQUES = {
|
|
|
3376
3472
|
codexPythonMigrationsDocumented: {
|
|
3377
3473
|
id: 'CX-PY11',
|
|
3378
3474
|
name: 'Database migrations mentioned (alembic / Django migrations)',
|
|
3379
|
-
check: (ctx) => {
|
|
3475
|
+
check: (ctx) => {
|
|
3476
|
+
const hasPy = ctx.files.some(f => /pyproject\.toml$|requirements\.txt$|setup\.py$|manage\.py$/.test(f));
|
|
3477
|
+
if (!hasPy) return null;
|
|
3478
|
+
// SDK/library repos don't need migration docs
|
|
3479
|
+
if (isSdkOrLibraryRepo(ctx)) return null;
|
|
3480
|
+
// Only applicable when repo actually uses a database
|
|
3481
|
+
const deps = (ctx.fileContent('pyproject.toml') || '') + (ctx.fileContent('requirements.txt') || '');
|
|
3482
|
+
const hasDb = /sqlalchemy|django|peewee|tortoise|asyncpg|psycopg|pymongo|pymysql|alembic/i.test(deps);
|
|
3483
|
+
if (!hasDb) return null;
|
|
3484
|
+
const docs = (ctx.claudeMdContent ? ctx.claudeMdContent() : ctx.fileContent('CLAUDE.md')) || ctx.fileContent('README.md') || '';
|
|
3485
|
+
return /alembic|migrate|makemigrations|django.{0,10}migration/i.test(docs) || ctx.files.some(f => /alembic[.]ini$|alembic[/]/.test(f));
|
|
3486
|
+
},
|
|
3380
3487
|
impact: 'medium',
|
|
3381
3488
|
category: 'python',
|
|
3382
3489
|
fix: 'Document database migration workflow (alembic or Django migrations).',
|
|
@@ -3464,7 +3571,12 @@ const CODEX_TECHNIQUES = {
|
|
|
3464
3571
|
codexPythonPackageStructure: {
|
|
3465
3572
|
id: 'CX-PY19',
|
|
3466
3573
|
name: 'Python package has proper structure (src/ layout or __init__.py)',
|
|
3467
|
-
check: (ctx) => {
|
|
3574
|
+
check: (ctx) => {
|
|
3575
|
+
const hasPy = ctx.files.some(f => /pyproject\.toml$|requirements\.txt$|setup\.py$|manage\.py$/.test(f));
|
|
3576
|
+
if (!hasPy) return null;
|
|
3577
|
+
// Path-separator agnostic: match both forward and back slashes (Windows compat)
|
|
3578
|
+
return ctx.files.some(f => /(^|[/\\])src[/\\].*[/\\]__init__\.py$|^[^/\\]+[/\\]__init__\.py$/.test(f));
|
|
3579
|
+
},
|
|
3468
3580
|
impact: 'medium',
|
|
3469
3581
|
category: 'python',
|
|
3470
3582
|
fix: 'Use src/ layout or ensure packages have __init__.py files.',
|
package/src/cursor/context.js
CHANGED
|
@@ -36,16 +36,36 @@ class CursorProjectContext extends ProjectContext {
|
|
|
36
36
|
* NOTE: Subdirectories inside .cursor/rules/ are silently ignored by Cursor.
|
|
37
37
|
*/
|
|
38
38
|
cursorRules() {
|
|
39
|
-
const
|
|
40
|
-
const
|
|
39
|
+
const fs = require('fs');
|
|
40
|
+
const rulesPath = path.join(this.dir, '.cursor', 'rules');
|
|
41
|
+
let dir = rulesPath;
|
|
42
|
+
let basePath = '.cursor/rules';
|
|
43
|
+
|
|
44
|
+
// File-redirect pattern: .cursor/rules is a file pointing to another path
|
|
45
|
+
// (e.g., cal.com uses agents/rules/ with .cursor/rules as a text pointer).
|
|
46
|
+
try {
|
|
47
|
+
const stat = fs.statSync(rulesPath);
|
|
48
|
+
if (stat.isFile()) {
|
|
49
|
+
const redirect = fs.readFileSync(rulesPath, 'utf8').trim();
|
|
50
|
+
if (redirect && redirect.length < 500) {
|
|
51
|
+
const resolved = path.resolve(path.dirname(rulesPath), redirect);
|
|
52
|
+
if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
|
|
53
|
+
dir = resolved;
|
|
54
|
+
basePath = path.relative(this.dir, resolved).replace(/\\/g, '/');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} catch { /* .cursor/rules may not exist — fall through to empty list */ }
|
|
59
|
+
|
|
60
|
+
const files = listFiles(dir, f => f.endsWith('.mdc') || f.endsWith('.md'));
|
|
41
61
|
return files.map(f => {
|
|
42
|
-
const relPath =
|
|
62
|
+
const relPath = `${basePath}/${f}`;
|
|
43
63
|
const content = this.fileContent(relPath);
|
|
44
64
|
if (!content) return null;
|
|
45
65
|
const parsed = parseMdc(content);
|
|
46
66
|
const ruleType = detectRuleType(parsed.frontmatter);
|
|
47
67
|
return {
|
|
48
|
-
name: f.replace(
|
|
68
|
+
name: f.replace(/\.(mdc|md)$/, ''),
|
|
49
69
|
path: relPath,
|
|
50
70
|
frontmatter: parsed.frontmatter,
|
|
51
71
|
body: parsed.body,
|
package/src/cursor/techniques.js
CHANGED
|
@@ -14,12 +14,12 @@
|
|
|
14
14
|
const os = require('os');
|
|
15
15
|
const path = require('path');
|
|
16
16
|
const { CursorProjectContext } = require('./context');
|
|
17
|
-
const { EMBEDDED_SECRET_PATTERNS, containsEmbeddedSecret } = require('../secret-patterns');
|
|
18
|
-
const { attachSourceUrls } = require('../source-urls');
|
|
19
|
-
const { buildStackChecks } = require('../stack-checks');
|
|
20
|
-
const { isApiProject, isDatabaseProject, isAuthProject, isMonitoringRelevant } = require('../supplemental-checks');
|
|
21
|
-
const { hasCostBudgetOrUsageTracking } = require('../cost-tracking');
|
|
22
|
-
const { validateMdcFrontmatter, validateMcpEnvVars } = require('./config-parser');
|
|
17
|
+
const { EMBEDDED_SECRET_PATTERNS, containsEmbeddedSecret } = require('../secret-patterns');
|
|
18
|
+
const { attachSourceUrls } = require('../source-urls');
|
|
19
|
+
const { buildStackChecks } = require('../stack-checks');
|
|
20
|
+
const { isApiProject, isDatabaseProject, isAuthProject, isMonitoringRelevant } = require('../supplemental-checks');
|
|
21
|
+
const { hasCostBudgetOrUsageTracking } = require('../cost-tracking');
|
|
22
|
+
const { validateMdcFrontmatter, validateMcpEnvVars } = require('./config-parser');
|
|
23
23
|
|
|
24
24
|
// ─── Shared helpers ─────────────────────────────────────────────────────────
|
|
25
25
|
|
|
@@ -180,6 +180,10 @@ const CURSOR_TECHNIQUES = {
|
|
|
180
180
|
name: 'No .cursorrules without migration warning',
|
|
181
181
|
check: (ctx) => {
|
|
182
182
|
const hasLegacy = ctx.hasLegacyRules ? ctx.hasLegacyRules() : Boolean(ctx.fileContent('.cursorrules'));
|
|
183
|
+
const hasNewRules = ctx.cursorRules ? ctx.cursorRules().length > 0 : false;
|
|
184
|
+
const hasMcp = Boolean(ctx.fileContent('.cursor/mcp.json'));
|
|
185
|
+
// N/A when repo has no Cursor configuration at all — don't reward absence
|
|
186
|
+
if (!hasLegacy && !hasNewRules && !hasMcp) return null;
|
|
183
187
|
return !hasLegacy;
|
|
184
188
|
},
|
|
185
189
|
impact: 'critical',
|
|
@@ -507,17 +511,21 @@ const CURSOR_TECHNIQUES = {
|
|
|
507
511
|
|
|
508
512
|
cursorPrivacyMode: {
|
|
509
513
|
id: 'CU-C01',
|
|
510
|
-
name: 'Privacy Mode
|
|
514
|
+
name: 'Privacy Mode documented in rules/docs',
|
|
511
515
|
check: (ctx) => {
|
|
512
|
-
//
|
|
513
|
-
//
|
|
516
|
+
// Privacy Mode is an IDE setting stored in SQLite state.vscdb — not auditable
|
|
517
|
+
// from repo files. This check validates that the repo documents its stance.
|
|
518
|
+
const hasNewRules = ctx.cursorRules ? ctx.cursorRules().length > 0 : false;
|
|
519
|
+
const hasLegacy = ctx.hasLegacyRules ? ctx.hasLegacyRules() : Boolean(ctx.fileContent('.cursorrules'));
|
|
520
|
+
// N/A when no rules exist to check against
|
|
521
|
+
if (!hasNewRules && !hasLegacy) return null;
|
|
514
522
|
const docs = docsBundle(ctx);
|
|
515
523
|
return /privacy mode|zero.?retention|data retention|privacy.*enabled/i.test(docs);
|
|
516
524
|
},
|
|
517
|
-
impact: '
|
|
518
|
-
rating:
|
|
525
|
+
impact: 'low',
|
|
526
|
+
rating: 3,
|
|
519
527
|
category: 'trust',
|
|
520
|
-
fix: 'Privacy Mode is OFF by default — code is sent to
|
|
528
|
+
fix: 'Privacy Mode is OFF by default in Cursor — code is sent to providers unless enabled. Document your stance in a rules file so contributors know whether Privacy Mode is expected on/off. Enable in Cursor Settings → Privacy → Privacy Mode.',
|
|
521
529
|
template: null,
|
|
522
530
|
file: () => '.cursor/rules/',
|
|
523
531
|
line: () => null,
|
|
@@ -2212,17 +2220,17 @@ const CURSOR_TECHNIQUES = {
|
|
|
2212
2220
|
fix: 'Enable prompt caching in MCP configuration or document caching strategy for repeated prompts.',
|
|
2213
2221
|
template: null, file: () => '.cursor/mcp.json', line: () => null,
|
|
2214
2222
|
},
|
|
2215
|
-
cursorCostBudgetDefined: {
|
|
2216
|
-
id: 'CU-T48', name: 'AI cost budget or per-run usage tracking documented',
|
|
2217
|
-
check: (ctx) => {
|
|
2218
|
-
const docs = docsBundle(ctx) + (ctx.fileContent('CLAUDE.md') || '');
|
|
2219
|
-
if (!docs.trim() && !hasCostBudgetOrUsageTracking('', ctx)) return null;
|
|
2220
|
-
return hasCostBudgetOrUsageTracking(docs, ctx);
|
|
2221
|
-
},
|
|
2222
|
-
impact: 'low', rating: 2, category: 'cost-optimization',
|
|
2223
|
-
fix: 'Document AI cost guardrails or per-run usage tracking in README.md or Cursor rules so spend is observable, not guessed.',
|
|
2224
|
-
template: null, file: () => 'README.md', line: () => null,
|
|
2225
|
-
},
|
|
2223
|
+
cursorCostBudgetDefined: {
|
|
2224
|
+
id: 'CU-T48', name: 'AI cost budget or per-run usage tracking documented',
|
|
2225
|
+
check: (ctx) => {
|
|
2226
|
+
const docs = docsBundle(ctx) + (ctx.fileContent('CLAUDE.md') || '');
|
|
2227
|
+
if (!docs.trim() && !hasCostBudgetOrUsageTracking('', ctx)) return null;
|
|
2228
|
+
return hasCostBudgetOrUsageTracking(docs, ctx);
|
|
2229
|
+
},
|
|
2230
|
+
impact: 'low', rating: 2, category: 'cost-optimization',
|
|
2231
|
+
fix: 'Document AI cost guardrails or per-run usage tracking in README.md or Cursor rules so spend is observable, not guessed.',
|
|
2232
|
+
template: null, file: () => 'README.md', line: () => null,
|
|
2233
|
+
},
|
|
2226
2234
|
|
|
2227
2235
|
// ============================================================
|
|
2228
2236
|
// === PYTHON STACK CHECKS (category: 'python') ===============
|