@nerviq/cli 1.19.0 → 1.20.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 +2 -2
- package/bin/cli.js +2 -0
- package/package.json +3 -1
- package/src/aider/context.js +2 -1
- package/src/context.js +10 -4
- package/src/gemini/context.js +74 -5
- package/src/gemini/techniques.js +91 -19
- package/src/windsurf/context.js +120 -10
- package/src/windsurf/techniques.js +155 -33
package/README.md
CHANGED
|
@@ -223,8 +223,8 @@ All successful operational responses are wrapped in a JSON envelope:
|
|
|
223
223
|
{
|
|
224
224
|
"data": {},
|
|
225
225
|
"meta": {
|
|
226
|
-
"version": "1.
|
|
227
|
-
"timestamp": "2026-04-
|
|
226
|
+
"version": "1.20.1",
|
|
227
|
+
"timestamp": "2026-04-14T12:00:00.000Z"
|
|
228
228
|
}
|
|
229
229
|
}
|
|
230
230
|
```
|
package/bin/cli.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
1
2
|
// macOS pipe-flush guard: when stdout is a pipe, Node defaults to
|
|
2
3
|
// non-blocking writes. `console.log(...) + process.exit(N)` then drops
|
|
3
4
|
// the trailing write (empty stdout on macOS Node 18, truncation at the
|
|
@@ -2536,6 +2537,7 @@ async function main() {
|
|
|
2536
2537
|
}
|
|
2537
2538
|
} catch (err) {
|
|
2538
2539
|
console.error(`\n Error: ${err.message}`);
|
|
2540
|
+
if (process.env.NERVIQ_DEBUG) console.error(err.stack);
|
|
2539
2541
|
console.error(' Fix: Run `npx nerviq doctor` to diagnose common issues, or check https://github.com/nerviq/nerviq#troubleshooting');
|
|
2540
2542
|
process.exit(2);
|
|
2541
2543
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nerviq/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.20.1",
|
|
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": {
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
"build": "npm pack --dry-run",
|
|
19
19
|
"test": "node test/run.js",
|
|
20
20
|
"verify:release-metadata": "node tools/validate-release-metadata.js",
|
|
21
|
+
"prepublish:check": "node tools/pre-publish.js",
|
|
22
|
+
"prepublishOnly": "node tools/pre-publish.js",
|
|
21
23
|
"test:jest": "jest",
|
|
22
24
|
"test:coverage": "jest --coverage",
|
|
23
25
|
"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",
|
package/src/aider/context.js
CHANGED
|
@@ -39,7 +39,8 @@ function detectAiderVersion() {
|
|
|
39
39
|
|
|
40
40
|
class AiderProjectContext extends ProjectContext {
|
|
41
41
|
configContent() {
|
|
42
|
-
|
|
42
|
+
// Aider accepts both .yml and .yaml extensions for the project config
|
|
43
|
+
return this.fileContent('.aider.conf.yml') || this.fileContent('.aider.conf.yaml');
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
modelSettingsContent() {
|
package/src/context.js
CHANGED
|
@@ -83,14 +83,20 @@ class ProjectContext {
|
|
|
83
83
|
if (!raw) return null;
|
|
84
84
|
|
|
85
85
|
// If the file is very short and looks like a file reference, follow it.
|
|
86
|
-
//
|
|
86
|
+
// Recognised pointer shapes on each line:
|
|
87
|
+
// AGENTS.md
|
|
88
|
+
// docs/CODING.md
|
|
89
|
+
// @AGENTS.md (Claude Code @import syntax)
|
|
90
|
+
// @./docs/CODING.md (Claude Code @import with relative prefix)
|
|
87
91
|
const trimmed = raw.trim();
|
|
88
|
-
|
|
92
|
+
const pointerLine = /^@?\.?\/?[a-zA-Z0-9_./-]+\.(md|txt|rst)$/;
|
|
93
|
+
if (trimmed.length < 200 && pointerLine.test(trimmed.split(/\r?\n/)[0].trim())) {
|
|
89
94
|
const lines = trimmed.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
|
|
90
95
|
let combined = raw;
|
|
91
96
|
for (const line of lines) {
|
|
92
|
-
if (
|
|
93
|
-
const
|
|
97
|
+
if (pointerLine.test(line)) {
|
|
98
|
+
const ref = line.replace(/^@/, '').replace(/^\.\//, '');
|
|
99
|
+
const referenced = this.fileContent(ref);
|
|
94
100
|
if (referenced) {
|
|
95
101
|
combined += '\n' + referenced;
|
|
96
102
|
}
|
package/src/gemini/context.js
CHANGED
|
@@ -54,18 +54,87 @@ class GeminiProjectContext extends ProjectContext {
|
|
|
54
54
|
|
|
55
55
|
geminiMdContent() {
|
|
56
56
|
const direct = this.fileContent('GEMINI.md');
|
|
57
|
-
if (direct) return direct;
|
|
57
|
+
if (direct) return this._expandGeminiMdImports(direct);
|
|
58
58
|
|
|
59
|
-
// Fallback: use context.fileName from settings if configured
|
|
59
|
+
// Fallback: use context.fileName from settings if configured.
|
|
60
|
+
// Per Gemini CLI spec, context.fileName may be a string or an array of strings.
|
|
60
61
|
const contextFileName = this.configValue('context.fileName');
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
const candidates = Array.isArray(contextFileName)
|
|
63
|
+
? contextFileName.filter(n => typeof n === 'string' && n.length > 0)
|
|
64
|
+
: (typeof contextFileName === 'string' && contextFileName ? [contextFileName] : []);
|
|
65
|
+
for (const name of candidates) {
|
|
66
|
+
const content = this.fileContent(name);
|
|
67
|
+
if (content) return this._expandGeminiMdImports(content);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Further fallback: recognise common alternate instruction surfaces
|
|
71
|
+
// (AGENTS.md, CLAUDE.md) and Gemini Code Assist styleguides
|
|
72
|
+
// (.gemini/styleguide.md) even when not explicitly declared in settings,
|
|
73
|
+
// mirroring how real Gemini-using repos document guidance.
|
|
74
|
+
for (const alt of ['AGENTS.md', 'CLAUDE.md', '.gemini/styleguide.md']) {
|
|
75
|
+
const content = this.fileContent(alt);
|
|
76
|
+
if (content) return this._expandGeminiMdImports(content);
|
|
64
77
|
}
|
|
65
78
|
|
|
66
79
|
return null;
|
|
67
80
|
}
|
|
68
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Expand Gemini CLI-style imports inside an instructions file. Gemini CLI
|
|
84
|
+
* supports `@path/to/file.md` imports and treats GEMINI.md files that are
|
|
85
|
+
* a single pointer line as an alias for the referenced file. For audit
|
|
86
|
+
* purposes we concatenate the referenced bodies so substance/architecture/
|
|
87
|
+
* command checks see the effective instructions bundle.
|
|
88
|
+
*/
|
|
89
|
+
_expandGeminiMdImports(content, depth = 0) {
|
|
90
|
+
if (!content || depth > 3) return content || '';
|
|
91
|
+
let out = content;
|
|
92
|
+
const importRe = /@([^\s@]+\.(?:md|markdown|MD))/g;
|
|
93
|
+
const seen = new Set();
|
|
94
|
+
let m;
|
|
95
|
+
while ((m = importRe.exec(content)) !== null) {
|
|
96
|
+
const ref = m[1].replace(/^\.\//, '');
|
|
97
|
+
if (seen.has(ref)) continue;
|
|
98
|
+
seen.add(ref);
|
|
99
|
+
const body = this.fileContent(ref);
|
|
100
|
+
if (body) out += '\n\n' + this._expandGeminiMdImports(body, depth + 1);
|
|
101
|
+
}
|
|
102
|
+
// "Pointer" GEMINI.md: the whole file is a single relative path to another
|
|
103
|
+
// markdown doc (no @ prefix). Observed in google/dotprompt.
|
|
104
|
+
const trimmed = content.trim();
|
|
105
|
+
if (/^[\w./-]+\.(md|markdown)$/.test(trimmed) && !trimmed.includes('\n')) {
|
|
106
|
+
const body = this.fileContent(trimmed);
|
|
107
|
+
if (body) out += '\n\n' + this._expandGeminiMdImports(body, depth + 1);
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Returns true when the repo exposes any Gemini-recognisable instruction
|
|
114
|
+
* surface — GEMINI.md (directly or via context.fileName override), an
|
|
115
|
+
* imported pointer, AGENTS.md, or CLAUDE.md. Used to gate checks that
|
|
116
|
+
* would otherwise hard-fail on repos that use alternative conventions.
|
|
117
|
+
*/
|
|
118
|
+
hasAnyInstructionsSurface() {
|
|
119
|
+
return Boolean(this.geminiMdContent());
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Returns true when the repo exposes any evidence of Gemini CLI usage.
|
|
124
|
+
* This is deliberately narrower than `isGeminiRepo`: it also counts
|
|
125
|
+
* `.idx/airules.md` (Project IDX) and Gemini-specific settings keys.
|
|
126
|
+
*/
|
|
127
|
+
hasGeminiCliSurface() {
|
|
128
|
+
if (this.fileContent('.gemini/settings.json')) return true;
|
|
129
|
+
if (this.fileContent('GEMINI.md')) return true;
|
|
130
|
+
const extDirs = this.extensionDirs ? this.extensionDirs() : [];
|
|
131
|
+
if (extDirs.length > 0) return true;
|
|
132
|
+
const cmdFiles = this.commandFiles ? this.commandFiles() : [];
|
|
133
|
+
if (cmdFiles.length > 0) return true;
|
|
134
|
+
if (this.fileContent('.idx/airules.md')) return true;
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
69
138
|
globalGeminiMdContent() {
|
|
70
139
|
const homeDir = os.homedir();
|
|
71
140
|
const globalPath = path.join(homeDir, '.gemini', 'GEMINI.md');
|
package/src/gemini/techniques.js
CHANGED
|
@@ -93,10 +93,37 @@ function settingsData(ctx) {
|
|
|
93
93
|
return result && result.ok ? result.data : null;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* True when .gemini/settings.json is effectively an MCP-only config — i.e.
|
|
98
|
+
* it configures external tool servers but does not attempt to tune CLI
|
|
99
|
+
* behaviour (model, sandbox, approval, theme, history, etc.). Checks that
|
|
100
|
+
* assert on CLI-behaviour keys should be N/A on such configs.
|
|
101
|
+
*/
|
|
102
|
+
function isMcpOnlySettings(data) {
|
|
103
|
+
if (!data || typeof data !== 'object') return false;
|
|
104
|
+
const keys = Object.keys(data).filter(k => k !== '$schema' && k !== 'ide' && k !== 'context');
|
|
105
|
+
if (keys.length === 0) return true;
|
|
106
|
+
const behaviourKeys = new Set(['model', 'sandbox', 'safety', 'theme', 'approval', 'approvalMode', 'history', 'session', 'telemetry', 'hooks', 'tools', 'skills', 'commands', 'extensions', 'security']);
|
|
107
|
+
return keys.every(k => k === 'mcpServers' || !behaviourKeys.has(k));
|
|
108
|
+
}
|
|
109
|
+
|
|
96
110
|
function docsBundle(ctx) {
|
|
97
111
|
const gmd = geminiMd(ctx) || '';
|
|
98
112
|
const readme = ctx.fileContent('README.md') || '';
|
|
99
|
-
|
|
113
|
+
const agents = ctx.fileContent('AGENTS.md') || '';
|
|
114
|
+
const claudeMd = ctx.fileContent('CLAUDE.md') || '';
|
|
115
|
+
const contributing = ctx.fileContent('CONTRIBUTING.md') || '';
|
|
116
|
+
const architecture = ctx.fileContent('ARCHITECTURE.md') || '';
|
|
117
|
+
const development = ctx.fileContent('DEVELOPMENT.md') || ctx.fileContent('docs/development.md') || '';
|
|
118
|
+
return [gmd, readme, agents, claudeMd, contributing, architecture, development].join('\n');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Broader bundle for stack-specific docs discovery (mirrors the Copilot
|
|
122
|
+
// PP-01 stackDocsBundle approach): consults common developer docs in
|
|
123
|
+
// addition to the instruction surfaces so stack checks don't hard-fail
|
|
124
|
+
// when conventions live in CONTRIBUTING/DEVELOPMENT instead of GEMINI.md.
|
|
125
|
+
function stackDocsBundle(ctx) {
|
|
126
|
+
return docsBundle(ctx);
|
|
100
127
|
}
|
|
101
128
|
|
|
102
129
|
function expectedVerificationCategories(ctx) {
|
|
@@ -262,11 +289,18 @@ const GEMINI_TECHNIQUES = {
|
|
|
262
289
|
|
|
263
290
|
geminiMdArchitecture: {
|
|
264
291
|
id: 'GM-A04',
|
|
265
|
-
name: '
|
|
292
|
+
name: 'Instructions have architecture section or Mermaid diagram',
|
|
266
293
|
check: (ctx) => {
|
|
267
294
|
const content = geminiMd(ctx);
|
|
268
295
|
if (!content) return null;
|
|
269
|
-
|
|
296
|
+
if (hasArchitecture(content)) return true;
|
|
297
|
+
// Credit an ARCHITECTURE.md at repo root, or architecture content
|
|
298
|
+
// surfaced in README.md — both are legitimate ways Gemini will pick
|
|
299
|
+
// up repo shape, especially when GEMINI.md is a pointer/import.
|
|
300
|
+
const arch = ctx.fileContent('ARCHITECTURE.md') || ctx.fileContent('docs/architecture.md');
|
|
301
|
+
if (arch) return true;
|
|
302
|
+
const readme = ctx.fileContent('README.md') || '';
|
|
303
|
+
return hasArchitecture(readme);
|
|
270
304
|
},
|
|
271
305
|
impact: 'medium',
|
|
272
306
|
rating: 4,
|
|
@@ -352,7 +386,15 @@ const GEMINI_TECHNIQUES = {
|
|
|
352
386
|
geminiSettingsExists: {
|
|
353
387
|
id: 'GM-B01',
|
|
354
388
|
name: '.gemini/settings.json exists',
|
|
355
|
-
check: (ctx) =>
|
|
389
|
+
check: (ctx) => {
|
|
390
|
+
if (ctx.fileContent('.gemini/settings.json')) return true;
|
|
391
|
+
// N/A when the repo uses only the GEMINI.md-instruction convention
|
|
392
|
+
// without any .gemini/ configuration directory. settings.json is
|
|
393
|
+
// opt-in for CLI tuning; instruction-only repos should not fail.
|
|
394
|
+
const hasGeminiDir = ctx.hasDir && ctx.hasDir('.gemini');
|
|
395
|
+
if (!hasGeminiDir) return null;
|
|
396
|
+
return false;
|
|
397
|
+
},
|
|
356
398
|
impact: 'high',
|
|
357
399
|
rating: 5,
|
|
358
400
|
category: 'config',
|
|
@@ -397,6 +439,7 @@ const GEMINI_TECHNIQUES = {
|
|
|
397
439
|
check: (ctx) => {
|
|
398
440
|
const data = settingsData(ctx);
|
|
399
441
|
if (!data) return null;
|
|
442
|
+
if (isMcpOnlySettings(data)) return null;
|
|
400
443
|
if (!data.model) return false;
|
|
401
444
|
// v0.36.0: model field MUST be an object { name: "..." }, not a string
|
|
402
445
|
// String format causes exit code 41: "Expected object, received string"
|
|
@@ -419,8 +462,9 @@ const GEMINI_TECHNIQUES = {
|
|
|
419
462
|
check: (ctx) => {
|
|
420
463
|
const data = settingsData(ctx);
|
|
421
464
|
if (!data) return null;
|
|
422
|
-
|
|
423
|
-
|
|
465
|
+
if (isMcpOnlySettings(data)) return null;
|
|
466
|
+
// At least one CLI-behaviour setting should be explicit.
|
|
467
|
+
return Boolean(data.sandbox || data.safety || data.theme || data.approval || data.approvalMode);
|
|
424
468
|
},
|
|
425
469
|
impact: 'medium',
|
|
426
470
|
rating: 4,
|
|
@@ -479,13 +523,17 @@ const GEMINI_TECHNIQUES = {
|
|
|
479
523
|
|
|
480
524
|
geminiEnvApiKey: {
|
|
481
525
|
id: 'GM-B07',
|
|
482
|
-
name: '
|
|
526
|
+
name: 'API key / auth documented (env file, README, or GEMINI.md)',
|
|
483
527
|
check: (ctx) => {
|
|
484
528
|
const envContent = ctx.fileContent('.env') || '';
|
|
485
|
-
const envExample = ctx.fileContent('.env.example') || ctx.fileContent('.env.template') || '';
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
|
|
529
|
+
const envExample = ctx.fileContent('.env.example') || ctx.fileContent('.env.template') || ctx.fileContent('.env.sample') || '';
|
|
530
|
+
const docs = docsBundle(ctx);
|
|
531
|
+
const combined = `${envContent}\n${envExample}\n${docs}`;
|
|
532
|
+
if (!combined.trim()) return null;
|
|
533
|
+
// Credit env files OR documentation that mentions any Gemini/Google auth mechanism,
|
|
534
|
+
// including `gemini auth`, ADC / application default credentials, Vertex AI, or a
|
|
535
|
+
// direct mention of the standard env var names.
|
|
536
|
+
return /\bGEMINI_API_KEY\b|\bGOOGLE_API_KEY\b|\bGOOGLE_APPLICATION_CREDENTIALS\b|\bgcloud\b|\bapplication[- ]default credentials?\b|\bADC\b|\bvertex[- ]?ai\b|\bgemini auth\b|\bservice account\b/i.test(combined);
|
|
489
537
|
},
|
|
490
538
|
impact: 'high',
|
|
491
539
|
rating: 4,
|
|
@@ -542,6 +590,7 @@ const GEMINI_TECHNIQUES = {
|
|
|
542
590
|
check: (ctx) => {
|
|
543
591
|
const data = settingsData(ctx);
|
|
544
592
|
if (!data) return null;
|
|
593
|
+
if (isMcpOnlySettings(data)) return null;
|
|
545
594
|
return Boolean(data.sandbox && (data.sandbox.mode || typeof data.sandbox === 'string'));
|
|
546
595
|
},
|
|
547
596
|
impact: 'high',
|
|
@@ -1017,6 +1066,7 @@ const GEMINI_TECHNIQUES = {
|
|
|
1017
1066
|
check: (ctx) => {
|
|
1018
1067
|
const data = settingsData(ctx);
|
|
1019
1068
|
if (!data) return null;
|
|
1069
|
+
if (isMcpOnlySettings(data)) return null;
|
|
1020
1070
|
const sandbox = data.sandbox;
|
|
1021
1071
|
if (!sandbox) return false;
|
|
1022
1072
|
const mode = typeof sandbox === 'string' ? sandbox : sandbox.mode;
|
|
@@ -1633,8 +1683,9 @@ const GEMINI_TECHNIQUES = {
|
|
|
1633
1683
|
id: 'GM-J01',
|
|
1634
1684
|
name: 'Rate limit/quota awareness documented',
|
|
1635
1685
|
check: (ctx) => {
|
|
1636
|
-
const
|
|
1637
|
-
|
|
1686
|
+
const docs = docsBundle(ctx);
|
|
1687
|
+
if (!docs.trim()) return null;
|
|
1688
|
+
return /\brate[- ]?limit\b|\bquota\b|\brequests? per\b|\bcost\b|\btoken\b.*\blimit\b|\bthrottl|\b429\b/i.test(docs);
|
|
1638
1689
|
},
|
|
1639
1690
|
impact: 'medium',
|
|
1640
1691
|
rating: 3,
|
|
@@ -1677,6 +1728,7 @@ const GEMINI_TECHNIQUES = {
|
|
|
1677
1728
|
check: (ctx) => {
|
|
1678
1729
|
const data = settingsData(ctx);
|
|
1679
1730
|
if (!data) return null;
|
|
1731
|
+
if (isMcpOnlySettings(data)) return null;
|
|
1680
1732
|
// Check if session/history settings are explicit
|
|
1681
1733
|
return data.history !== undefined || data.session !== undefined || data.telemetry !== undefined;
|
|
1682
1734
|
},
|
|
@@ -1836,11 +1888,11 @@ const GEMINI_TECHNIQUES = {
|
|
|
1836
1888
|
|
|
1837
1889
|
geminiTokenUsageAwareness: {
|
|
1838
1890
|
id: 'GM-K05',
|
|
1839
|
-
name: 'Token usage awareness
|
|
1891
|
+
name: 'Token usage awareness documented',
|
|
1840
1892
|
check: (ctx) => {
|
|
1841
|
-
const
|
|
1842
|
-
if (!
|
|
1843
|
-
return /\btoken\b|\bcontext window\b|\bcontext length\b|\b1M\b|\btruncat/i.test(
|
|
1893
|
+
const docs = docsBundle(ctx);
|
|
1894
|
+
if (!docs.trim()) return null;
|
|
1895
|
+
return /\btoken\b|\bcontext window\b|\bcontext length\b|\b1M\b|\btruncat/i.test(docs);
|
|
1844
1896
|
},
|
|
1845
1897
|
impact: 'low',
|
|
1846
1898
|
rating: 2,
|
|
@@ -1863,7 +1915,12 @@ const GEMINI_TECHNIQUES = {
|
|
|
1863
1915
|
name: 'Custom commands exist in .gemini/commands/',
|
|
1864
1916
|
check: (ctx) => {
|
|
1865
1917
|
const commandFiles = ctx.commandFiles ? ctx.commandFiles() : [];
|
|
1866
|
-
|
|
1918
|
+
if (commandFiles.length > 0) return true;
|
|
1919
|
+
// Custom commands are opt-in — only fire when the repo already has
|
|
1920
|
+
// a .gemini/commands/ directory (implying the user intends to use
|
|
1921
|
+
// commands but hasn't populated it).
|
|
1922
|
+
const hasCommandsDir = ctx.hasDir && ctx.hasDir('.gemini/commands');
|
|
1923
|
+
return hasCommandsDir ? false : null;
|
|
1867
1924
|
},
|
|
1868
1925
|
impact: 'medium',
|
|
1869
1926
|
rating: 3,
|
|
@@ -2109,7 +2166,22 @@ const GEMINI_TECHNIQUES = {
|
|
|
2109
2166
|
},
|
|
2110
2167
|
geminiPropagationCompleteness: {
|
|
2111
2168
|
id: 'GM-P03', name: 'No dangling surface references',
|
|
2112
|
-
check: (ctx) => {
|
|
2169
|
+
check: (ctx) => {
|
|
2170
|
+
const g = ctx.geminiMdContent();
|
|
2171
|
+
if (!g) return null;
|
|
2172
|
+
const issues = [];
|
|
2173
|
+
// Require specific Gemini-CLI vocabulary before asserting a dangling
|
|
2174
|
+
// reference. "skills" is too generic a word (appears in unrelated
|
|
2175
|
+
// product copy); require `.gemini/skills` path or `gemini skills`
|
|
2176
|
+
// phrasing. Same for hooks/extensions.
|
|
2177
|
+
if (/\.gemini\/hooks\b|\bgemini hooks?\b|\bhooksConfig\b|\bBeforeTool\b|\bAfterTool\b/i.test(g)) {
|
|
2178
|
+
const s = ctx.settingsJson();
|
|
2179
|
+
if (!s || !s.ok || (!s.data || (!s.data.hooks && !s.data.BeforeTool && !s.data.AfterTool))) issues.push('hooks');
|
|
2180
|
+
}
|
|
2181
|
+
if (/\.gemini\/skills\b|\bgemini skills?\b/i.test(g) && !(ctx.hasDir ? ctx.hasDir('.gemini/skills') : false)) issues.push('skills');
|
|
2182
|
+
if (/\.gemini\/extensions\b|\bgemini extensions?\b/i.test(g) && !(ctx.hasDir ? ctx.hasDir('.gemini/extensions') : false)) issues.push('extensions');
|
|
2183
|
+
return issues.length === 0;
|
|
2184
|
+
},
|
|
2113
2185
|
impact: 'high', rating: 4, category: 'release-freshness',
|
|
2114
2186
|
fix: 'Ensure all surfaces mentioned in GEMINI.md have corresponding definition files.',
|
|
2115
2187
|
template: 'gemini-md', file: () => 'GEMINI.md', line: () => 1,
|
package/src/windsurf/context.js
CHANGED
|
@@ -36,26 +36,59 @@ class WindsurfProjectContext extends ProjectContext {
|
|
|
36
36
|
* Windsurf uses Markdown + YAML frontmatter (NOT MDC like Cursor).
|
|
37
37
|
* 4 activation modes: Always, Auto, Agent-Requested, Manual.
|
|
38
38
|
* 10K char limit per rule file.
|
|
39
|
+
*
|
|
40
|
+
* PP-03: also recognises the `.windsurfrules/` *directory* convention
|
|
41
|
+
* (observed in rudrankriyam/Ichi) where the rule files sit in
|
|
42
|
+
* `.windsurfrules/*.md` or `*.mdc` instead of `.windsurf/rules/`.
|
|
39
43
|
*/
|
|
40
44
|
windsurfRules() {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
const collected = [];
|
|
46
|
+
|
|
47
|
+
// Primary: .windsurf/rules/*.md
|
|
48
|
+
const primaryDir = path.join(this.dir, '.windsurf', 'rules');
|
|
49
|
+
const primaryFiles = listFiles(primaryDir, f => f.endsWith('.md'));
|
|
50
|
+
for (const f of primaryFiles) {
|
|
44
51
|
const relPath = `.windsurf/rules/${f}`;
|
|
45
52
|
const content = this.fileContent(relPath);
|
|
46
|
-
if (!content)
|
|
53
|
+
if (!content) continue;
|
|
47
54
|
const parsed = parseWindsurfRule(content);
|
|
48
55
|
const ruleType = detectRuleType(parsed.frontmatter);
|
|
49
|
-
|
|
56
|
+
collected.push({
|
|
50
57
|
name: f.replace('.md', ''),
|
|
51
58
|
path: relPath,
|
|
52
59
|
frontmatter: parsed.frontmatter,
|
|
53
60
|
body: parsed.body,
|
|
54
61
|
ruleType,
|
|
55
|
-
charCount:
|
|
56
|
-
overLimit:
|
|
57
|
-
};
|
|
58
|
-
}
|
|
62
|
+
charCount: content.length,
|
|
63
|
+
overLimit: content.length > 10000,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// PP-03: fallback — `.windsurfrules/` as a directory.
|
|
68
|
+
const altDir = path.join(this.dir, '.windsurfrules');
|
|
69
|
+
try {
|
|
70
|
+
if (fs.statSync(altDir).isDirectory()) {
|
|
71
|
+
const altFiles = listFiles(altDir, f => f.endsWith('.md') || f.endsWith('.mdc'));
|
|
72
|
+
for (const f of altFiles) {
|
|
73
|
+
const relPath = `.windsurfrules/${f}`;
|
|
74
|
+
const content = this.fileContent(relPath);
|
|
75
|
+
if (!content) continue;
|
|
76
|
+
const parsed = parseWindsurfRule(content);
|
|
77
|
+
const ruleType = detectRuleType(parsed.frontmatter);
|
|
78
|
+
collected.push({
|
|
79
|
+
name: f.replace(/\.(md|mdc)$/, ''),
|
|
80
|
+
path: relPath,
|
|
81
|
+
frontmatter: parsed.frontmatter,
|
|
82
|
+
body: parsed.body,
|
|
83
|
+
ruleType,
|
|
84
|
+
charCount: content.length,
|
|
85
|
+
overLimit: content.length > 10000,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} catch { /* not a directory */ }
|
|
90
|
+
|
|
91
|
+
return collected;
|
|
59
92
|
}
|
|
60
93
|
|
|
61
94
|
/**
|
|
@@ -81,15 +114,92 @@ class WindsurfProjectContext extends ProjectContext {
|
|
|
81
114
|
|
|
82
115
|
/**
|
|
83
116
|
* .windsurfrules content (deprecated).
|
|
117
|
+
*
|
|
118
|
+
* PP-03: handles three real-world shapes:
|
|
119
|
+
* 1. Classic file with rule text.
|
|
120
|
+
* 2. Pointer file — a single short line referencing another markdown
|
|
121
|
+
* file (e.g. `.ai/instructions.md`, `.llmrules`,
|
|
122
|
+
* `.ai/tech-stack.md`). Observed in ShareX/XerahS,
|
|
123
|
+
* Brawl345/Image-Reverse-Search-WebExtension, wepublish/wepublish.
|
|
124
|
+
* 3. Directory convention — `.windsurfrules/` is itself a directory
|
|
125
|
+
* of rule files. Observed in rudrankriyam/Ichi. In that case this
|
|
126
|
+
* method returns the concatenated body of all contained rule files
|
|
127
|
+
* so consumer checks (architecture / verification / etc.) see the
|
|
128
|
+
* effective instruction bundle.
|
|
84
129
|
*/
|
|
85
130
|
legacyWindsurfrules() {
|
|
86
|
-
|
|
131
|
+
// Directory form first — `.windsurfrules/` as a directory.
|
|
132
|
+
const altDir = path.join(this.dir, '.windsurfrules');
|
|
133
|
+
try {
|
|
134
|
+
if (fs.statSync(altDir).isDirectory()) {
|
|
135
|
+
const files = listFiles(altDir, f => f.endsWith('.md') || f.endsWith('.mdc'));
|
|
136
|
+
const bodies = files
|
|
137
|
+
.map(f => this.fileContent(`.windsurfrules/${f}`) || '')
|
|
138
|
+
.filter(Boolean);
|
|
139
|
+
return bodies.length > 0 ? bodies.join('\n') : '';
|
|
140
|
+
}
|
|
141
|
+
} catch { /* not a directory */ }
|
|
142
|
+
|
|
143
|
+
const raw = this.fileContent('.windsurfrules');
|
|
144
|
+
if (!raw) return null;
|
|
145
|
+
|
|
146
|
+
// Pointer form — one short line that looks like a relative path.
|
|
147
|
+
const trimmed = raw.trim();
|
|
148
|
+
if (trimmed.length < 200) {
|
|
149
|
+
const lines = trimmed.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
|
|
150
|
+
if (lines.length <= 3 && lines.every(l => /^[a-zA-Z0-9_./-]+(\.(md|mdc|txt|rst))?$/.test(l))) {
|
|
151
|
+
let combined = raw;
|
|
152
|
+
for (const line of lines) {
|
|
153
|
+
const referenced = this.fileContent(line);
|
|
154
|
+
if (referenced) combined += '\n' + referenced;
|
|
155
|
+
}
|
|
156
|
+
return combined;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return raw;
|
|
87
160
|
}
|
|
88
161
|
|
|
89
162
|
hasLegacyRules() {
|
|
90
163
|
return Boolean(this.legacyWindsurfrules());
|
|
91
164
|
}
|
|
92
165
|
|
|
166
|
+
/**
|
|
167
|
+
* PP-03: True only when `.windsurfrules` exists as a regular file
|
|
168
|
+
* containing legacy rule text (not a pointer and not a directory).
|
|
169
|
+
* Used by checks that warn about the deprecated single-file format.
|
|
170
|
+
*/
|
|
171
|
+
hasRawLegacyWindsurfrules() {
|
|
172
|
+
const altDir = path.join(this.dir, '.windsurfrules');
|
|
173
|
+
try {
|
|
174
|
+
if (fs.statSync(altDir).isDirectory()) return false;
|
|
175
|
+
} catch { /* not a dir */ }
|
|
176
|
+
const raw = this.fileContent('.windsurfrules');
|
|
177
|
+
if (!raw) return false;
|
|
178
|
+
const trimmed = raw.trim();
|
|
179
|
+
if (trimmed.length < 200) {
|
|
180
|
+
const lines = trimmed.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
|
|
181
|
+
if (lines.length <= 3 && lines.every(l => /^[a-zA-Z0-9_./-]+(\.(md|mdc|txt|rst))?$/.test(l))) {
|
|
182
|
+
// It's a pointer — not a raw legacy file.
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* PP-03: surface detection helper — any instruction surface that
|
|
191
|
+
* Cascade/Windsurf can pick up.
|
|
192
|
+
*/
|
|
193
|
+
hasAnyInstructionsSurface() {
|
|
194
|
+
return (
|
|
195
|
+
this.windsurfRules().length > 0 ||
|
|
196
|
+
Boolean(this.legacyWindsurfrules()) ||
|
|
197
|
+
Boolean(this.fileContent('AGENTS.md')) ||
|
|
198
|
+
Boolean(this.fileContent('CLAUDE.md')) ||
|
|
199
|
+
Boolean(this.fileContent('.ai/instructions.md'))
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
93
203
|
// ─── MCP config (.windsurf/mcp.json) ──────────────────────────────────
|
|
94
204
|
|
|
95
205
|
/**
|
|
@@ -160,10 +160,23 @@ function isValidWindsurfFrontmatter(frontmatter) {
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
function docsBundle(ctx) {
|
|
163
|
+
// PP-03: broadened to include the surfaces real Windsurf-using repos
|
|
164
|
+
// actually use for instructions (AGENTS.md, CLAUDE.md, CONTRIBUTING.md,
|
|
165
|
+
// ARCHITECTURE.md, DEVELOPMENT.md) plus the `.ai/` convention observed
|
|
166
|
+
// in ShareX/XerahS and wepublish/wepublish. This mirrors the Gemini
|
|
167
|
+
// PP-02 broadening and ensures docs-quality checks do not FP on repos
|
|
168
|
+
// that keep guidance outside `.windsurf/rules/` alone.
|
|
163
169
|
const rules = allRulesContent(ctx) || '';
|
|
164
170
|
const readme = ctx.fileContent('README.md') || '';
|
|
165
171
|
const legacy = ctx.legacyWindsurfrules ? (ctx.legacyWindsurfrules() || '') : '';
|
|
166
|
-
|
|
172
|
+
const agents = ctx.fileContent('AGENTS.md') || '';
|
|
173
|
+
const claudeMd = ctx.fileContent('CLAUDE.md') || ctx.fileContent('.claude/CLAUDE.md') || '';
|
|
174
|
+
const contributing = ctx.fileContent('CONTRIBUTING.md') || '';
|
|
175
|
+
const architecture = ctx.fileContent('ARCHITECTURE.md') || '';
|
|
176
|
+
const development = ctx.fileContent('DEVELOPMENT.md') || ctx.fileContent('DEVELOPING.md') || '';
|
|
177
|
+
const aiInstructions = ctx.fileContent('.ai/instructions.md') || ctx.fileContent('.ai/tech-stack.md') || '';
|
|
178
|
+
const windsurfMd = ctx.fileContent('WINDSURF.md') || ctx.fileContent('windsurf_rules.md') || '';
|
|
179
|
+
return `${rules}\n${readme}\n${legacy}\n${agents}\n${claudeMd}\n${contributing}\n${architecture}\n${development}\n${aiInstructions}\n${windsurfMd}`;
|
|
167
180
|
}
|
|
168
181
|
|
|
169
182
|
function expectedVerificationCategories(ctx) {
|
|
@@ -257,8 +270,14 @@ const WINDSURF_TECHNIQUES = {
|
|
|
257
270
|
id: 'WS-A01',
|
|
258
271
|
name: '.windsurf/rules/ directory exists with .md files',
|
|
259
272
|
check: (ctx) => {
|
|
273
|
+
// PP-03: `windsurfRules()` now also enumerates the
|
|
274
|
+
// `.windsurfrules/` directory form. In addition, pointer-style
|
|
275
|
+
// `.windsurfrules` (one-liner referencing e.g. `.ai/instructions.md`)
|
|
276
|
+
// counts because it resolves to a real instruction body.
|
|
260
277
|
const rules = ctx.windsurfRules ? ctx.windsurfRules() : [];
|
|
261
|
-
|
|
278
|
+
if (rules.length > 0) return true;
|
|
279
|
+
const legacy = ctx.legacyWindsurfrules ? ctx.legacyWindsurfrules() : null;
|
|
280
|
+
return Boolean(legacy && legacy.trim().length > 0);
|
|
262
281
|
},
|
|
263
282
|
impact: 'critical',
|
|
264
283
|
rating: 5,
|
|
@@ -273,8 +292,13 @@ const WINDSURF_TECHNIQUES = {
|
|
|
273
292
|
id: 'WS-A02',
|
|
274
293
|
name: 'No .windsurfrules without migration to .windsurf/rules/',
|
|
275
294
|
check: (ctx) => {
|
|
276
|
-
|
|
277
|
-
|
|
295
|
+
// PP-03: only raw legacy single-file `.windsurfrules` (non-pointer,
|
|
296
|
+
// non-directory) counts as the deprecated form. Pointer files
|
|
297
|
+
// delegating to a modern instruction surface (e.g.
|
|
298
|
+
// `.ai/instructions.md`) and the `.windsurfrules/` directory
|
|
299
|
+
// convention are both acceptable modern patterns.
|
|
300
|
+
const raw = ctx.hasRawLegacyWindsurfrules ? ctx.hasRawLegacyWindsurfrules() : false;
|
|
301
|
+
return !raw;
|
|
278
302
|
},
|
|
279
303
|
impact: 'critical',
|
|
280
304
|
rating: 5,
|
|
@@ -306,9 +330,12 @@ const WINDSURF_TECHNIQUES = {
|
|
|
306
330
|
id: 'WS-A04',
|
|
307
331
|
name: 'Rules have valid YAML frontmatter',
|
|
308
332
|
check: (ctx) => {
|
|
333
|
+
// PP-03: absent frontmatter is acceptable — Windsurf defaults such
|
|
334
|
+
// rules to `always_on`. Only flag when frontmatter *is* present
|
|
335
|
+
// and malformed, or when the declared trigger/field is invalid.
|
|
309
336
|
const rules = ctx.windsurfRules ? ctx.windsurfRules() : [];
|
|
310
337
|
if (rules.length === 0) return null;
|
|
311
|
-
return rules.every((rule) => isValidWindsurfFrontmatter(rule.frontmatter));
|
|
338
|
+
return rules.every((rule) => rule.frontmatter == null || isValidWindsurfFrontmatter(rule.frontmatter));
|
|
312
339
|
},
|
|
313
340
|
impact: 'high',
|
|
314
341
|
rating: 4,
|
|
@@ -462,8 +489,13 @@ const WINDSURF_TECHNIQUES = {
|
|
|
462
489
|
id: 'WS-B03',
|
|
463
490
|
name: 'Workflow slash commands exist in .windsurf/workflows/',
|
|
464
491
|
check: (ctx) => {
|
|
492
|
+
// PP-03: workflows are opt-in. N/A when the repo has no
|
|
493
|
+
// `.windsurf/workflows/` directory at all — firing a fail on every
|
|
494
|
+
// Windsurf repo without workflows produced systematic bias.
|
|
465
495
|
const files = ctx.workflowFiles ? ctx.workflowFiles() : [];
|
|
466
|
-
|
|
496
|
+
if (files.length > 0) return true;
|
|
497
|
+
if (!ctx.hasDir || !ctx.hasDir('.windsurf/workflows')) return null;
|
|
498
|
+
return false;
|
|
467
499
|
},
|
|
468
500
|
impact: 'medium',
|
|
469
501
|
rating: 3,
|
|
@@ -498,8 +530,15 @@ const WINDSURF_TECHNIQUES = {
|
|
|
498
530
|
id: 'WS-B05',
|
|
499
531
|
name: 'Memories configured for persistent context',
|
|
500
532
|
check: (ctx) => {
|
|
533
|
+
// PP-03: memories are workspace-local and strictly opt-in. The
|
|
534
|
+
// technique docs themselves warn not to rely on them (see
|
|
535
|
+
// windsurfMemoryScopeDocumented). Firing a fail on every repo that
|
|
536
|
+
// doesn't ship a `.windsurf/memories/` directory produced a 10/10
|
|
537
|
+
// FP rate. N/A when the repo doesn't opt in.
|
|
501
538
|
const memories = ctx.memoryFiles ? ctx.memoryFiles() : [];
|
|
502
|
-
|
|
539
|
+
if (memories.length > 0) return true;
|
|
540
|
+
if (!ctx.hasDir || !ctx.hasDir('.windsurf/memories')) return null;
|
|
541
|
+
return false;
|
|
503
542
|
},
|
|
504
543
|
impact: 'medium',
|
|
505
544
|
rating: 3,
|
|
@@ -740,10 +779,18 @@ const WINDSURF_TECHNIQUES = {
|
|
|
740
779
|
id: 'WS-D01',
|
|
741
780
|
name: 'Rules properly reach Cascade (not just .windsurfrules)',
|
|
742
781
|
check: (ctx) => {
|
|
782
|
+
// PP-03: `windsurfRules()` now includes `.windsurfrules/`
|
|
783
|
+
// directory form. Pointer-style legacy `.windsurfrules` that
|
|
784
|
+
// points at a modern instruction file (`.ai/instructions.md`,
|
|
785
|
+
// AGENTS.md, etc.) is also acceptable since the referenced body
|
|
786
|
+
// is what Cascade actually receives.
|
|
743
787
|
const rules = ctx.windsurfRules ? ctx.windsurfRules() : [];
|
|
744
788
|
const hasLegacy = ctx.hasLegacyRules ? ctx.hasLegacyRules() : false;
|
|
745
789
|
if (rules.length === 0 && !hasLegacy) return null;
|
|
746
|
-
|
|
790
|
+
if (rules.length > 0) return true;
|
|
791
|
+
// Raw legacy single-file is a genuine miss; pointer/dir is fine.
|
|
792
|
+
const raw = ctx.hasRawLegacyWindsurfrules ? ctx.hasRawLegacyWindsurfrules() : false;
|
|
793
|
+
return !raw;
|
|
747
794
|
},
|
|
748
795
|
impact: 'critical',
|
|
749
796
|
rating: 5,
|
|
@@ -758,9 +805,15 @@ const WINDSURF_TECHNIQUES = {
|
|
|
758
805
|
id: 'WS-D02',
|
|
759
806
|
name: 'Cascade multi-file editing awareness documented',
|
|
760
807
|
check: (ctx) => {
|
|
808
|
+
// PP-03: this is a Cascade-specific awareness advisory. It should
|
|
809
|
+
// only fire when the repo has actual `.windsurf/rules/` content
|
|
810
|
+
// that could reasonably cover Cascade guidance. Pointer-only
|
|
811
|
+
// `.windsurfrules` repos and repos with just a README keep this
|
|
812
|
+
// check N/A — the README is not the right place for Cascade
|
|
813
|
+
// multi-file editing notes.
|
|
761
814
|
const rules = allRulesContent(ctx);
|
|
762
815
|
if (!rules.trim()) return null;
|
|
763
|
-
return /multi.?file|cross.?file|cascade.*edit|multiple.*file/i.test(
|
|
816
|
+
return /multi.?file|cross.?file|cascade.*edit|multiple.*file/i.test(docsBundle(ctx));
|
|
764
817
|
},
|
|
765
818
|
impact: 'medium',
|
|
766
819
|
rating: 3,
|
|
@@ -775,9 +828,11 @@ const WINDSURF_TECHNIQUES = {
|
|
|
775
828
|
id: 'WS-D03',
|
|
776
829
|
name: 'Steps automation awareness documented',
|
|
777
830
|
check: (ctx) => {
|
|
831
|
+
// PP-03: Cascade-specific advisory; N/A when no
|
|
832
|
+
// `.windsurf/rules/` content exists.
|
|
778
833
|
const rules = allRulesContent(ctx);
|
|
779
834
|
if (!rules.trim()) return null;
|
|
780
|
-
return /steps|automation|step.?by.?step|cascade.*step/i.test(
|
|
835
|
+
return /steps|automation|step.?by.?step|cascade.*step/i.test(docsBundle(ctx));
|
|
781
836
|
},
|
|
782
837
|
impact: 'medium',
|
|
783
838
|
rating: 3,
|
|
@@ -792,9 +847,12 @@ const WINDSURF_TECHNIQUES = {
|
|
|
792
847
|
id: 'WS-D04',
|
|
793
848
|
name: 'Agent session length awareness',
|
|
794
849
|
check: (ctx) => {
|
|
850
|
+
// PP-03: Cascade-specific advisory; N/A when no
|
|
851
|
+
// `.windsurf/rules/` content exists (advisory belongs in rules,
|
|
852
|
+
// not in README).
|
|
795
853
|
const rules = allRulesContent(ctx);
|
|
796
854
|
if (!rules.trim()) return null;
|
|
797
|
-
return /session.*length|session.*limit|context.*drift|long.*session/i.test(
|
|
855
|
+
return /session.*length|session.*limit|context.*drift|long.*session/i.test(docsBundle(ctx));
|
|
798
856
|
},
|
|
799
857
|
impact: 'low',
|
|
800
858
|
rating: 2,
|
|
@@ -809,9 +867,13 @@ const WINDSURF_TECHNIQUES = {
|
|
|
809
867
|
id: 'WS-D05',
|
|
810
868
|
name: 'Cascade skills configured for project needs',
|
|
811
869
|
check: (ctx) => {
|
|
870
|
+
// PP-03: the `.windsurf/skills/` directory is itself a valid
|
|
871
|
+
// signal (observed in snyk/snyk-intellij-plugin). Otherwise
|
|
872
|
+
// N/A unless the repo has `.windsurf/rules/` content.
|
|
873
|
+
if (ctx.hasDir && ctx.hasDir('.windsurf/skills')) return true;
|
|
812
874
|
const rules = allRulesContent(ctx);
|
|
813
875
|
if (!rules.trim()) return null;
|
|
814
|
-
return
|
|
876
|
+
return /\bskill\b|\bcapability\b|tool.*use|cascade.*skill/i.test(docsBundle(ctx));
|
|
815
877
|
},
|
|
816
878
|
impact: 'medium',
|
|
817
879
|
rating: 3,
|
|
@@ -925,11 +987,21 @@ const WINDSURF_TECHNIQUES = {
|
|
|
925
987
|
id: 'WS-F01',
|
|
926
988
|
name: 'Rules include build/test/lint commands',
|
|
927
989
|
check: (ctx) => {
|
|
928
|
-
|
|
929
|
-
|
|
990
|
+
// PP-03: verification commands often live in README / AGENTS /
|
|
991
|
+
// CONTRIBUTING. Fall back to the full docsBundle if the core
|
|
992
|
+
// rules don't mention them, so we don't FP on repos that keep
|
|
993
|
+
// commands in a standard README section.
|
|
994
|
+
const core = coreRulesContent(ctx) || allRulesContent(ctx);
|
|
930
995
|
const expected = expectedVerificationCategories(ctx);
|
|
931
|
-
if (expected.length === 0)
|
|
932
|
-
|
|
996
|
+
if (expected.length === 0) {
|
|
997
|
+
const combined = core || docsBundle(ctx);
|
|
998
|
+
if (!combined.trim()) return null;
|
|
999
|
+
return /\bverify\b|\btest\b|\blint\b|\bbuild\b/i.test(combined);
|
|
1000
|
+
}
|
|
1001
|
+
if (expected.every(cat => hasCommandMention(core, cat))) return true;
|
|
1002
|
+
const docs = docsBundle(ctx);
|
|
1003
|
+
if (!docs.trim()) return null;
|
|
1004
|
+
return expected.every(cat => hasCommandMention(docs, cat));
|
|
933
1005
|
},
|
|
934
1006
|
impact: 'high',
|
|
935
1007
|
rating: 5,
|
|
@@ -944,9 +1016,13 @@ const WINDSURF_TECHNIQUES = {
|
|
|
944
1016
|
id: 'WS-F02',
|
|
945
1017
|
name: 'Rules include architecture section or Mermaid diagram',
|
|
946
1018
|
check: (ctx) => {
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1019
|
+
// PP-03: architecture content commonly lives in ARCHITECTURE.md
|
|
1020
|
+
// or a README section, not duplicated inside rules. Widen to
|
|
1021
|
+
// docsBundle. N/A only when the repo has no instruction surface
|
|
1022
|
+
// whatsoever (not even a README).
|
|
1023
|
+
const bundle = docsBundle(ctx);
|
|
1024
|
+
if (!bundle.trim()) return null;
|
|
1025
|
+
return hasArchitecture(bundle);
|
|
950
1026
|
},
|
|
951
1027
|
impact: 'medium',
|
|
952
1028
|
rating: 4,
|
|
@@ -997,12 +1073,17 @@ const WINDSURF_TECHNIQUES = {
|
|
|
997
1073
|
id: 'WS-F05',
|
|
998
1074
|
name: 'Rules reference project-specific patterns (not generic)',
|
|
999
1075
|
check: (ctx) => {
|
|
1000
|
-
|
|
1076
|
+
// PP-03: widen to docsBundle and add stack-agnostic project
|
|
1077
|
+
// directory markers (internal/, pkg/, cmd/, crates/, modules/,
|
|
1078
|
+
// packages/, tests/, docs/, examples/). The previous JS-heavy
|
|
1079
|
+
// regex produced FPs on Rust/Go/Java/Swift/Kotlin repos whose
|
|
1080
|
+
// project layouts never mention src/app/api etc.
|
|
1081
|
+
const content = docsBundle(ctx);
|
|
1001
1082
|
if (!content.trim()) return null;
|
|
1002
1083
|
const pkg = ctx.jsonFile ? ctx.jsonFile('package.json') : null;
|
|
1003
1084
|
const projectName = (pkg && pkg.name) || path.basename(ctx.dir);
|
|
1004
1085
|
const hasSpecific = content.includes(projectName) ||
|
|
1005
|
-
|
|
1086
|
+
/\b(src|app|api|routes|services|components|lib|cmd|internal|pkg|crates|modules|packages|tests?|docs|examples|scripts)\//i.test(content);
|
|
1006
1087
|
return hasSpecific;
|
|
1007
1088
|
},
|
|
1008
1089
|
impact: 'medium',
|
|
@@ -1471,7 +1552,11 @@ const WINDSURF_TECHNIQUES = {
|
|
|
1471
1552
|
id: 'WS-L01',
|
|
1472
1553
|
name: 'Rules mention modern Windsurf features (Steps, Memories, Workflows)',
|
|
1473
1554
|
check: (ctx) => {
|
|
1474
|
-
|
|
1555
|
+
// PP-03: widen to docsBundle; also credit `.windsurf/workflows` or
|
|
1556
|
+
// `.windsurf/skills` directories as structural evidence that the
|
|
1557
|
+
// repo has adopted modern Windsurf features.
|
|
1558
|
+
if (ctx.hasDir && (ctx.hasDir('.windsurf/workflows') || ctx.hasDir('.windsurf/skills'))) return true;
|
|
1559
|
+
const content = docsBundle(ctx);
|
|
1475
1560
|
if (!content.trim()) return null;
|
|
1476
1561
|
return /steps|memories|workflow|cascade|skill|slash command/i.test(content);
|
|
1477
1562
|
},
|
|
@@ -1488,9 +1573,12 @@ const WINDSURF_TECHNIQUES = {
|
|
|
1488
1573
|
id: 'WS-L02',
|
|
1489
1574
|
name: 'No deprecated patterns (.windsurfrules for agent)',
|
|
1490
1575
|
check: (ctx) => {
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1576
|
+
// PP-03: only the raw single-file legacy form is deprecated.
|
|
1577
|
+
// Pointer-style `.windsurfrules` and the `.windsurfrules/`
|
|
1578
|
+
// directory convention are not.
|
|
1579
|
+
const raw = ctx.hasRawLegacyWindsurfrules ? ctx.hasRawLegacyWindsurfrules() : false;
|
|
1580
|
+
if (!raw) return null;
|
|
1581
|
+
return false;
|
|
1494
1582
|
},
|
|
1495
1583
|
impact: 'high',
|
|
1496
1584
|
rating: 4,
|
|
@@ -1556,7 +1644,11 @@ const WINDSURF_TECHNIQUES = {
|
|
|
1556
1644
|
id: 'WS-L06',
|
|
1557
1645
|
name: 'Rules guide Cascade context usage (@-mentions, file refs)',
|
|
1558
1646
|
check: (ctx) => {
|
|
1559
|
-
|
|
1647
|
+
// PP-03: Cascade-specific deep-quality advisory; N/A when no
|
|
1648
|
+
// `.windsurf/rules/` content exists.
|
|
1649
|
+
const rules = allRulesContent(ctx);
|
|
1650
|
+
if (!rules.trim()) return null;
|
|
1651
|
+
const content = docsBundle(ctx);
|
|
1560
1652
|
if (!content.trim()) return null;
|
|
1561
1653
|
return /@|file.*reference|context.*include|codebase|index/i.test(content);
|
|
1562
1654
|
},
|
|
@@ -1573,9 +1665,11 @@ const WINDSURF_TECHNIQUES = {
|
|
|
1573
1665
|
id: 'WS-L07',
|
|
1574
1666
|
name: 'Session drift awareness documented',
|
|
1575
1667
|
check: (ctx) => {
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1668
|
+
// PP-03: Cascade-specific deep-quality advisory; N/A when no
|
|
1669
|
+
// `.windsurf/rules/` content exists.
|
|
1670
|
+
const rules = allRulesContent(ctx);
|
|
1671
|
+
if (!rules.trim()) return null;
|
|
1672
|
+
return /session.*drift|context.*window|long.*session|session.*length|refresh.*context/i.test(docsBundle(ctx));
|
|
1579
1673
|
},
|
|
1580
1674
|
impact: 'low',
|
|
1581
1675
|
rating: 2,
|
|
@@ -1650,9 +1744,13 @@ const WINDSURF_TECHNIQUES = {
|
|
|
1650
1744
|
id: 'WS-M04',
|
|
1651
1745
|
name: 'Windows/WSL usage includes a Windsurf stability caveat',
|
|
1652
1746
|
check: (ctx) => {
|
|
1747
|
+
// PP-03: relevance was keyed off `os.platform()`, which is the
|
|
1748
|
+
// *host* running the audit (always Windows in our environment),
|
|
1749
|
+
// causing a systematic 10/10 fail on every target repo. This
|
|
1750
|
+
// check should only fire when the *target repo* itself documents
|
|
1751
|
+
// Windows/WSL use — otherwise the advisory is not applicable.
|
|
1653
1752
|
const docs = docsBundle(ctx);
|
|
1654
|
-
|
|
1655
|
-
if (!relevant) return null;
|
|
1753
|
+
if (!/\bwsl\b|\bnative windows\b|\bwindows subsystem\b/i.test(docs)) return null;
|
|
1656
1754
|
return /\bwsl\b.{0,40}\b(crash|unstable|avoid|native windows)\b|\bnative windows\b|\bavoid wsl\b/i.test(docs);
|
|
1657
1755
|
},
|
|
1658
1756
|
impact: 'medium',
|
|
@@ -1672,8 +1770,23 @@ const WINDSURF_TECHNIQUES = {
|
|
|
1672
1770
|
id: 'WS-N01',
|
|
1673
1771
|
name: 'Domain pack detection returns relevant results',
|
|
1674
1772
|
check: (ctx) => {
|
|
1773
|
+
// PP-03: expand stack markers so we also recognise Kotlin/Java
|
|
1774
|
+
// (build.gradle, build.gradle.kts, pom.xml), Swift
|
|
1775
|
+
// (Package.swift, *.xcodeproj), .NET (*.csproj / *.sln), Ruby
|
|
1776
|
+
// (Gemfile), PHP (composer.json), requirements.txt and
|
|
1777
|
+
// Pipfile/poetry. Without these the check FP'd on every
|
|
1778
|
+
// non-JS/Go/Rust/Python repo.
|
|
1675
1779
|
const pkg = ctx.jsonFile ? ctx.jsonFile('package.json') : null;
|
|
1676
|
-
|
|
1780
|
+
if (pkg) return true;
|
|
1781
|
+
const simple = [
|
|
1782
|
+
'go.mod', 'Cargo.toml', 'pyproject.toml', 'requirements.txt',
|
|
1783
|
+
'Pipfile', 'poetry.lock', 'Gemfile', 'composer.json', 'pom.xml',
|
|
1784
|
+
'build.gradle', 'build.gradle.kts', 'Package.swift', 'mix.exs',
|
|
1785
|
+
];
|
|
1786
|
+
if (simple.some(f => ctx.fileContent(f))) return true;
|
|
1787
|
+
const files = ctx.files || [];
|
|
1788
|
+
if (files.some(f => /\.(csproj|sln|fsproj|vbproj|xcodeproj|xcworkspace)\/?$/i.test(f))) return true;
|
|
1789
|
+
return false;
|
|
1677
1790
|
},
|
|
1678
1791
|
impact: 'low',
|
|
1679
1792
|
rating: 2,
|
|
@@ -1688,9 +1801,18 @@ const WINDSURF_TECHNIQUES = {
|
|
|
1688
1801
|
id: 'WS-N02',
|
|
1689
1802
|
name: 'MCP packs recommended based on project signals',
|
|
1690
1803
|
check: (ctx) => {
|
|
1804
|
+
// PP-03: only relevant when the repo actually opts in to MCP
|
|
1805
|
+
// (either documents it or ships a project-local `.windsurf/mcp.json`).
|
|
1806
|
+
// Previously fired on every repo without global MCP config, which
|
|
1807
|
+
// is 10/10 FP against real Windsurf repos that don't use MCP.
|
|
1691
1808
|
const mcp = mcpJsonData(ctx);
|
|
1692
1809
|
const servers = mcp && mcp.mcpServers ? mcp.mcpServers : {};
|
|
1693
|
-
|
|
1810
|
+
if (Object.keys(servers).length > 0) return true;
|
|
1811
|
+
const projectMcp = ctx.mcpConfig ? ctx.mcpConfig() : null;
|
|
1812
|
+
if (projectMcp && projectMcp.ok) return true;
|
|
1813
|
+
const docs = docsBundle(ctx);
|
|
1814
|
+
if (!/\bmcp\b/i.test(docs)) return null;
|
|
1815
|
+
return false;
|
|
1694
1816
|
},
|
|
1695
1817
|
impact: 'low',
|
|
1696
1818
|
rating: 2,
|