@nerviq/cli 1.16.0 → 1.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -16
- package/package.json +60 -60
- package/src/audit.js +36 -0
- package/src/codex/techniques.js +180 -44
- package/src/cursor/context.js +24 -4
- package/src/cursor/techniques.js +54 -26
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.1",
|
|
227
|
+
"timestamp": "2026-04-12T12:00:00.000Z"
|
|
222
228
|
}
|
|
223
229
|
}
|
|
224
230
|
```
|
package/package.json
CHANGED
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@nerviq/cli",
|
|
3
|
-
"version": "1.
|
|
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
|
-
"main": "src/index.js",
|
|
6
|
-
"bin": {
|
|
7
|
-
"nerviq": "bin/cli.js",
|
|
8
|
-
"@nerviq/cli": "bin/cli.js",
|
|
9
|
-
"nerviq-mcp": "src/mcp-server.js"
|
|
10
|
-
},
|
|
11
|
-
"files": [
|
|
12
|
-
"bin",
|
|
13
|
-
"src",
|
|
14
|
-
"README.md"
|
|
15
|
-
],
|
|
16
|
-
"scripts": {
|
|
17
|
-
"start": "node bin/cli.js",
|
|
18
|
-
"build": "npm pack --dry-run",
|
|
19
|
-
"test": "node test/run.js",
|
|
20
|
-
"verify:release-metadata": "node tools/validate-release-metadata.js",
|
|
21
|
-
"test:jest": "jest",
|
|
22
|
-
"test:coverage": "jest --coverage",
|
|
23
|
-
"test:all": "npm test && npx jest && node test/check-matrix.js && node test/codex-check-matrix.js && node test/gemini-check-matrix.js && node test/copilot-check-matrix.js && node test/cursor-check-matrix.js && node test/windsurf-check-matrix.js && node test/aider-check-matrix.js && node test/opencode-check-matrix.js && node test/golden-matrix.js && node test/codex-golden-matrix.js && node test/gemini-golden-matrix.js && node test/copilot-golden-matrix.js && node test/cursor-golden-matrix.js && node test/windsurf-golden-matrix.js && node test/aider-golden-matrix.js && node test/opencode-golden-matrix.js",
|
|
24
|
-
"benchmark:perf": "node tools/benchmark.js",
|
|
25
|
-
"catalog": "node -e \"const {generateCatalog}=require('./src/catalog');console.log(JSON.stringify(generateCatalog(),null,2))\""
|
|
26
|
-
},
|
|
27
|
-
"keywords": [
|
|
28
|
-
"nerviq",
|
|
29
|
-
"ai-agents",
|
|
30
|
-
"agent-governance",
|
|
31
|
-
"agent-config",
|
|
32
|
-
"harmony",
|
|
33
|
-
"synergy",
|
|
34
|
-
"audit",
|
|
35
|
-
"claude",
|
|
36
|
-
"codex",
|
|
37
|
-
"gemini",
|
|
38
|
-
"copilot",
|
|
39
|
-
"cursor",
|
|
40
|
-
"windsurf",
|
|
41
|
-
"aider",
|
|
42
|
-
"developer-tools",
|
|
43
|
-
"cli",
|
|
44
|
-
"mcp",
|
|
45
|
-
"multi-agent"
|
|
46
|
-
],
|
|
47
|
-
"author": "Nerviq <hello@nerviq.net>",
|
|
48
|
-
"license": "AGPL-3.0",
|
|
49
|
-
"repository": {
|
|
50
|
-
"type": "git",
|
|
51
|
-
"url": "git+https://github.com/nerviq/nerviq.git"
|
|
52
|
-
},
|
|
53
|
-
"homepage": "https://nerviq.net",
|
|
54
|
-
"engines": {
|
|
55
|
-
"node": ">=18.0.0"
|
|
56
|
-
},
|
|
57
|
-
"devDependencies": {
|
|
58
|
-
"jest": "^30.3.0"
|
|
59
|
-
}
|
|
60
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@nerviq/cli",
|
|
3
|
+
"version": "1.17.1",
|
|
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
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"nerviq": "bin/cli.js",
|
|
8
|
+
"@nerviq/cli": "bin/cli.js",
|
|
9
|
+
"nerviq-mcp": "src/mcp-server.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"bin",
|
|
13
|
+
"src",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"start": "node bin/cli.js",
|
|
18
|
+
"build": "npm pack --dry-run",
|
|
19
|
+
"test": "node test/run.js",
|
|
20
|
+
"verify:release-metadata": "node tools/validate-release-metadata.js",
|
|
21
|
+
"test:jest": "jest",
|
|
22
|
+
"test:coverage": "jest --coverage",
|
|
23
|
+
"test:all": "npm test && npx jest && node test/check-matrix.js && node test/codex-check-matrix.js && node test/gemini-check-matrix.js && node test/copilot-check-matrix.js && node test/cursor-check-matrix.js && node test/windsurf-check-matrix.js && node test/aider-check-matrix.js && node test/opencode-check-matrix.js && node test/golden-matrix.js && node test/codex-golden-matrix.js && node test/gemini-golden-matrix.js && node test/copilot-golden-matrix.js && node test/cursor-golden-matrix.js && node test/windsurf-golden-matrix.js && node test/aider-golden-matrix.js && node test/opencode-golden-matrix.js",
|
|
24
|
+
"benchmark:perf": "node tools/benchmark.js",
|
|
25
|
+
"catalog": "node -e \"const {generateCatalog}=require('./src/catalog');console.log(JSON.stringify(generateCatalog(),null,2))\""
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"nerviq",
|
|
29
|
+
"ai-agents",
|
|
30
|
+
"agent-governance",
|
|
31
|
+
"agent-config",
|
|
32
|
+
"harmony",
|
|
33
|
+
"synergy",
|
|
34
|
+
"audit",
|
|
35
|
+
"claude",
|
|
36
|
+
"codex",
|
|
37
|
+
"gemini",
|
|
38
|
+
"copilot",
|
|
39
|
+
"cursor",
|
|
40
|
+
"windsurf",
|
|
41
|
+
"aider",
|
|
42
|
+
"developer-tools",
|
|
43
|
+
"cli",
|
|
44
|
+
"mcp",
|
|
45
|
+
"multi-agent"
|
|
46
|
+
],
|
|
47
|
+
"author": "Nerviq <hello@nerviq.net>",
|
|
48
|
+
"license": "AGPL-3.0",
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "git+https://github.com/nerviq/nerviq.git"
|
|
52
|
+
},
|
|
53
|
+
"homepage": "https://nerviq.net",
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=18.0.0"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"jest": "^30.3.0"
|
|
59
|
+
}
|
|
60
|
+
}
|
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,30 @@ 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
|
+
// Dotfiles/config-only repos: they don't ship code, so pack recommendations
|
|
3117
|
+
// aren't meaningful — N/A is the correct answer.
|
|
3118
|
+
const dotfilesSignals = ['.zshrc', '.bashrc', '.vimrc', '.tmux.conf', '.gitconfig', 'install.sh', 'bootstrap.sh'];
|
|
3119
|
+
const looksLikeDotfiles = dotfilesSignals.filter(f => ctx.files.includes(f)).length >= 2;
|
|
3120
|
+
if (looksLikeDotfiles) return null;
|
|
3121
|
+
// If no signals at all, N/A rather than fail
|
|
3122
|
+
if (!agents && !config && !hasManifest) return null;
|
|
3056
3123
|
// At least 2 signal sources for grounded recommendation
|
|
3057
|
-
return [Boolean(agents), Boolean(config),
|
|
3124
|
+
return [Boolean(agents), Boolean(config), hasManifest].filter(Boolean).length >= 2;
|
|
3058
3125
|
},
|
|
3059
3126
|
impact: 'medium',
|
|
3060
3127
|
rating: 3,
|
|
3061
3128
|
category: 'pack-posture',
|
|
3062
|
-
fix: 'Add package.json
|
|
3129
|
+
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
3130
|
template: 'codex-agents-md',
|
|
3064
3131
|
file: () => 'AGENTS.md',
|
|
3065
3132
|
line: () => 1,
|
|
@@ -3098,15 +3165,15 @@ const CODEX_TECHNIQUES = {
|
|
|
3098
3165
|
// CP-08: New checks (O. Repeat-Usage Hygiene)
|
|
3099
3166
|
// =============================================
|
|
3100
3167
|
|
|
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'));
|
|
3168
|
+
codexSnapshotRetention: {
|
|
3169
|
+
id: 'CX-O01',
|
|
3170
|
+
name: 'At least one prior audit snapshot exists for repeat-usage',
|
|
3171
|
+
check: (ctx) => {
|
|
3172
|
+
try {
|
|
3173
|
+
const indexPath = resolveProjectStateReadPath(ctx.dir, 'snapshots', 'index.json');
|
|
3174
|
+
const fs = require('fs');
|
|
3175
|
+
if (!fs.existsSync(indexPath)) return null; // No snapshots yet, not a failure
|
|
3176
|
+
const entries = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
|
|
3110
3177
|
return Array.isArray(entries) && entries.length > 0;
|
|
3111
3178
|
} catch {
|
|
3112
3179
|
return null;
|
|
@@ -3121,15 +3188,15 @@ const CODEX_TECHNIQUES = {
|
|
|
3121
3188
|
line: () => null,
|
|
3122
3189
|
},
|
|
3123
3190
|
|
|
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'));
|
|
3191
|
+
codexFeedbackLoopHealth: {
|
|
3192
|
+
id: 'CX-O02',
|
|
3193
|
+
name: 'Feedback loop is functional when feedback has been submitted',
|
|
3194
|
+
check: (ctx) => {
|
|
3195
|
+
try {
|
|
3196
|
+
const indexPath = resolveProjectStateReadPath(ctx.dir, 'outcomes', 'index.json');
|
|
3197
|
+
const fs = require('fs');
|
|
3198
|
+
if (!fs.existsSync(indexPath)) return null; // No feedback yet, not a failure
|
|
3199
|
+
const entries = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
|
|
3133
3200
|
return Array.isArray(entries) && entries.length > 0;
|
|
3134
3201
|
} catch {
|
|
3135
3202
|
return null;
|
|
@@ -3144,15 +3211,15 @@ const CODEX_TECHNIQUES = {
|
|
|
3144
3211
|
line: () => null,
|
|
3145
3212
|
},
|
|
3146
3213
|
|
|
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'));
|
|
3214
|
+
codexTrendDataAvailability: {
|
|
3215
|
+
id: 'CX-O03',
|
|
3216
|
+
name: 'Trend data is computable (2+ snapshots with compatible schemas)',
|
|
3217
|
+
check: (ctx) => {
|
|
3218
|
+
try {
|
|
3219
|
+
const indexPath = resolveProjectStateReadPath(ctx.dir, 'snapshots', 'index.json');
|
|
3220
|
+
const fs = require('fs');
|
|
3221
|
+
if (!fs.existsSync(indexPath)) return null;
|
|
3222
|
+
const entries = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
|
|
3156
3223
|
const audits = (Array.isArray(entries) ? entries : []).filter(e => e.snapshotKind === 'audit');
|
|
3157
3224
|
return audits.length >= 2;
|
|
3158
3225
|
} catch {
|
|
@@ -3342,8 +3409,25 @@ const CODEX_TECHNIQUES = {
|
|
|
3342
3409
|
|
|
3343
3410
|
codexPythonFormatterConfigured: {
|
|
3344
3411
|
id: 'CX-PY08',
|
|
3345
|
-
name: 'Formatter configured (black / isort / ruff
|
|
3346
|
-
check: (ctx) => {
|
|
3412
|
+
name: 'Formatter configured (black / isort / ruff / yapf)',
|
|
3413
|
+
check: (ctx) => {
|
|
3414
|
+
const hasPy = ctx.files.some(f => /pyproject\.toml$|requirements\.txt$|setup\.py$|manage\.py$/.test(f));
|
|
3415
|
+
if (!hasPy) return null;
|
|
3416
|
+
const pp = ctx.fileContent('pyproject.toml') || '';
|
|
3417
|
+
// Explicit formatter sections
|
|
3418
|
+
if (/\[tool\.black|\[tool\.isort|\[tool\.ruff\.format|\[tool\.yapf|\[tool\.autopep8/i.test(pp)) return true;
|
|
3419
|
+
// Ruff config implies formatting capability in modern setups (ruff 0.3+)
|
|
3420
|
+
if (/\[tool\.ruff(\.lint)?\]/i.test(pp)) return true;
|
|
3421
|
+
// Standalone config files
|
|
3422
|
+
if (ctx.files.some(f => /^(ruff\.toml|\.ruff\.toml|\.isort\.cfg|\.yapfrc|setup\.cfg)$/i.test(f))) {
|
|
3423
|
+
const setupCfg = ctx.fileContent('setup.cfg') || '';
|
|
3424
|
+
if (/\[isort\]|\[yapf\]|\[flake8\]/i.test(setupCfg)) return true;
|
|
3425
|
+
if (f => /ruff|yapf|isort/i.test(f)) return true;
|
|
3426
|
+
}
|
|
3427
|
+
// Dev dependency signal
|
|
3428
|
+
if (/\b(black|isort|ruff|yapf|autopep8)\b/i.test(pp)) return true;
|
|
3429
|
+
return false;
|
|
3430
|
+
},
|
|
3347
3431
|
impact: 'medium',
|
|
3348
3432
|
category: 'python',
|
|
3349
3433
|
fix: 'Configure black, isort, or ruff format in pyproject.toml.',
|
|
@@ -3365,7 +3449,24 @@ const CODEX_TECHNIQUES = {
|
|
|
3365
3449
|
codexPythonFastapiEntryDocumented: {
|
|
3366
3450
|
id: 'CX-PY10',
|
|
3367
3451
|
name: 'FastAPI entry point documented if FastAPI project',
|
|
3368
|
-
check: (ctx) => {
|
|
3452
|
+
check: (ctx) => {
|
|
3453
|
+
const hasPy = ctx.files.some(f => /pyproject\.toml$|requirements\.txt$|setup\.py$|manage\.py$/.test(f));
|
|
3454
|
+
if (!hasPy) return null;
|
|
3455
|
+
const pp = ctx.fileContent('pyproject.toml') || '';
|
|
3456
|
+
const reqs = ctx.fileContent('requirements.txt') || '';
|
|
3457
|
+
// FastAPI only in dev/optional/example deps → N/A (SDK with example server)
|
|
3458
|
+
const inMain = /^\s*fastapi\b/im.test(reqs) ||
|
|
3459
|
+
/\[project\.dependencies\][\s\S]*?fastapi/i.test(pp) ||
|
|
3460
|
+
/\[tool\.poetry\.dependencies\][\s\S]*?fastapi/i.test(pp) ||
|
|
3461
|
+
/^\s*dependencies\s*=\s*\[[^\]]*"fastapi/im.test(pp);
|
|
3462
|
+
if (!inMain) return null;
|
|
3463
|
+
const docs = [
|
|
3464
|
+
ctx.claudeMdContent ? ctx.claudeMdContent() : ctx.fileContent('CLAUDE.md'),
|
|
3465
|
+
ctx.fileContent('README.md'),
|
|
3466
|
+
ctx.fileContent('AGENTS.md'),
|
|
3467
|
+
].filter(Boolean).join('\n');
|
|
3468
|
+
return /fastapi|uvicorn|app\.py|main\.py/i.test(docs);
|
|
3469
|
+
},
|
|
3369
3470
|
impact: 'high',
|
|
3370
3471
|
category: 'python',
|
|
3371
3472
|
fix: 'Document FastAPI entry point and how to run the development server.',
|
|
@@ -3376,7 +3477,18 @@ const CODEX_TECHNIQUES = {
|
|
|
3376
3477
|
codexPythonMigrationsDocumented: {
|
|
3377
3478
|
id: 'CX-PY11',
|
|
3378
3479
|
name: 'Database migrations mentioned (alembic / Django migrations)',
|
|
3379
|
-
check: (ctx) => {
|
|
3480
|
+
check: (ctx) => {
|
|
3481
|
+
const hasPy = ctx.files.some(f => /pyproject\.toml$|requirements\.txt$|setup\.py$|manage\.py$/.test(f));
|
|
3482
|
+
if (!hasPy) return null;
|
|
3483
|
+
// SDK/library repos don't need migration docs
|
|
3484
|
+
if (isSdkOrLibraryRepo(ctx)) return null;
|
|
3485
|
+
// Only applicable when repo actually uses a database
|
|
3486
|
+
const deps = (ctx.fileContent('pyproject.toml') || '') + (ctx.fileContent('requirements.txt') || '');
|
|
3487
|
+
const hasDb = /sqlalchemy|django|peewee|tortoise|asyncpg|psycopg|pymongo|pymysql|alembic/i.test(deps);
|
|
3488
|
+
if (!hasDb) return null;
|
|
3489
|
+
const docs = (ctx.claudeMdContent ? ctx.claudeMdContent() : ctx.fileContent('CLAUDE.md')) || ctx.fileContent('README.md') || '';
|
|
3490
|
+
return /alembic|migrate|makemigrations|django.{0,10}migration/i.test(docs) || ctx.files.some(f => /alembic[.]ini$|alembic[/]/.test(f));
|
|
3491
|
+
},
|
|
3380
3492
|
impact: 'medium',
|
|
3381
3493
|
category: 'python',
|
|
3382
3494
|
fix: 'Document database migration workflow (alembic or Django migrations).',
|
|
@@ -3464,7 +3576,31 @@ const CODEX_TECHNIQUES = {
|
|
|
3464
3576
|
codexPythonPackageStructure: {
|
|
3465
3577
|
id: 'CX-PY19',
|
|
3466
3578
|
name: 'Python package has proper structure (src/ layout or __init__.py)',
|
|
3467
|
-
check: (ctx) => {
|
|
3579
|
+
check: (ctx) => {
|
|
3580
|
+
const hasPy = ctx.files.some(f => /pyproject\.toml$|requirements\.txt$|setup\.py$|manage\.py$/.test(f));
|
|
3581
|
+
if (!hasPy) return null;
|
|
3582
|
+
const fs = require('fs');
|
|
3583
|
+
const path = require('path');
|
|
3584
|
+
// ctx.files only lists root — probe common package layouts directly
|
|
3585
|
+
try {
|
|
3586
|
+
// src/ layout: look for any src/*/__init__.py
|
|
3587
|
+
const srcDir = path.join(ctx.dir, 'src');
|
|
3588
|
+
if (fs.existsSync(srcDir) && fs.statSync(srcDir).isDirectory()) {
|
|
3589
|
+
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
3590
|
+
for (const e of entries) {
|
|
3591
|
+
if (e.isDirectory() && fs.existsSync(path.join(srcDir, e.name, '__init__.py'))) return true;
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
3594
|
+
// Flat layout: <package>/__init__.py at root
|
|
3595
|
+
const rootEntries = fs.readdirSync(ctx.dir, { withFileTypes: true });
|
|
3596
|
+
for (const e of rootEntries) {
|
|
3597
|
+
if (e.isDirectory() && !e.name.startsWith('.') && e.name !== 'tests' && e.name !== 'docs') {
|
|
3598
|
+
if (fs.existsSync(path.join(ctx.dir, e.name, '__init__.py'))) return true;
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
} catch { /* fall through */ }
|
|
3602
|
+
return false;
|
|
3603
|
+
},
|
|
3468
3604
|
impact: 'medium',
|
|
3469
3605
|
category: 'python',
|
|
3470
3606
|
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,
|
|
@@ -1323,12 +1331,32 @@ const CURSOR_TECHNIQUES = {
|
|
|
1323
1331
|
check: (ctx) => {
|
|
1324
1332
|
const docs = docsBundle(ctx);
|
|
1325
1333
|
if (!docs.trim()) return null;
|
|
1326
|
-
|
|
1334
|
+
// BugBot is an optional Cursor enterprise feature requiring separate installation.
|
|
1335
|
+
// Return N/A unless there is actual evidence the repo uses or intends to use BugBot.
|
|
1336
|
+
const files = new Set(ctx.files || []);
|
|
1337
|
+
const hasBugbotConfigFile = ['bugbot.yml', 'bugbot.yaml', '.bugbot.yml', '.bugbot.yaml'].some(f => files.has(f));
|
|
1338
|
+
let hasBugbotWorkflow = false;
|
|
1339
|
+
try {
|
|
1340
|
+
const fs = require('fs');
|
|
1341
|
+
const path = require('path');
|
|
1342
|
+
const wfDir = path.join(ctx.dir, '.github', 'workflows');
|
|
1343
|
+
if (fs.existsSync(wfDir)) {
|
|
1344
|
+
const wfFiles = fs.readdirSync(wfDir).filter(f => /\.ya?ml$/i.test(f));
|
|
1345
|
+
for (const f of wfFiles) {
|
|
1346
|
+
const content = fs.readFileSync(path.join(wfDir, f), 'utf8');
|
|
1347
|
+
if (/bugbot|bug.?bot/i.test(content)) { hasBugbotWorkflow = true; break; }
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
} catch { /* no workflows dir */ }
|
|
1351
|
+
const mentionedInDocs = /bugbot|bug.?bot|automated.*pr.*review/i.test(docs);
|
|
1352
|
+
// N/A when no signal at all that this repo uses/wants BugBot
|
|
1353
|
+
if (!hasBugbotConfigFile && !hasBugbotWorkflow && !mentionedInDocs) return null;
|
|
1354
|
+
return hasBugbotConfigFile || hasBugbotWorkflow || mentionedInDocs;
|
|
1327
1355
|
},
|
|
1328
|
-
impact: '
|
|
1356
|
+
impact: 'low',
|
|
1329
1357
|
rating: 3,
|
|
1330
1358
|
category: 'bugbot',
|
|
1331
|
-
fix: '
|
|
1359
|
+
fix: 'If you use BugBot for automated PR code review, document it in your rules or add the BugBot workflow. Otherwise ignore this check.',
|
|
1332
1360
|
template: null,
|
|
1333
1361
|
file: () => '.cursor/rules/',
|
|
1334
1362
|
line: () => null,
|
|
@@ -2212,17 +2240,17 @@ const CURSOR_TECHNIQUES = {
|
|
|
2212
2240
|
fix: 'Enable prompt caching in MCP configuration or document caching strategy for repeated prompts.',
|
|
2213
2241
|
template: null, file: () => '.cursor/mcp.json', line: () => null,
|
|
2214
2242
|
},
|
|
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
|
-
},
|
|
2243
|
+
cursorCostBudgetDefined: {
|
|
2244
|
+
id: 'CU-T48', name: 'AI cost budget or per-run usage tracking documented',
|
|
2245
|
+
check: (ctx) => {
|
|
2246
|
+
const docs = docsBundle(ctx) + (ctx.fileContent('CLAUDE.md') || '');
|
|
2247
|
+
if (!docs.trim() && !hasCostBudgetOrUsageTracking('', ctx)) return null;
|
|
2248
|
+
return hasCostBudgetOrUsageTracking(docs, ctx);
|
|
2249
|
+
},
|
|
2250
|
+
impact: 'low', rating: 2, category: 'cost-optimization',
|
|
2251
|
+
fix: 'Document AI cost guardrails or per-run usage tracking in README.md or Cursor rules so spend is observable, not guessed.',
|
|
2252
|
+
template: null, file: () => 'README.md', line: () => null,
|
|
2253
|
+
},
|
|
2226
2254
|
|
|
2227
2255
|
// ============================================================
|
|
2228
2256
|
// === PYTHON STACK CHECKS (category: 'python') ===============
|