@nerviq/cli 1.20.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 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.20.0",
227
- "timestamp": "2026-04-13T12:00:00.000Z"
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nerviq/cli",
3
- "version": "1.20.0",
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": {
@@ -19,6 +19,7 @@
19
19
  "test": "node test/run.js",
20
20
  "verify:release-metadata": "node tools/validate-release-metadata.js",
21
21
  "prepublish:check": "node tools/pre-publish.js",
22
+ "prepublishOnly": "node tools/pre-publish.js",
22
23
  "test:jest": "jest",
23
24
  "test:coverage": "jest --coverage",
24
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",
@@ -39,7 +39,8 @@ function detectAiderVersion() {
39
39
 
40
40
  class AiderProjectContext extends ProjectContext {
41
41
  configContent() {
42
- return this.fileContent('.aider.conf.yml');
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
- // Pattern: a single line that is just a filename (e.g., "AGENTS.md" or "docs/CODING.md")
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
- if (trimmed.length < 200 && /^[a-zA-Z0-9_./-]+\.(md|txt|rst)$/m.test(trimmed)) {
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 (/^[a-zA-Z0-9_./-]+\.(md|txt|rst)$/.test(line)) {
93
- const referenced = this.fileContent(line);
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
  }
@@ -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 dir = path.join(this.dir, '.windsurf', 'rules');
42
- const files = listFiles(dir, f => f.endsWith('.md'));
43
- return files.map(f => {
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) return null;
53
+ if (!content) continue;
47
54
  const parsed = parseWindsurfRule(content);
48
55
  const ruleType = detectRuleType(parsed.frontmatter);
49
- return {
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: (content || '').length,
56
- overLimit: (content || '').length > 10000,
57
- };
58
- }).filter(Boolean);
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
- return this.fileContent('.windsurfrules');
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
- return `${rules}\n${readme}\n${legacy}`;
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
- return rules.length > 0;
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
- const hasLegacy = ctx.hasLegacyRules ? ctx.hasLegacyRules() : Boolean(ctx.fileContent('.windsurfrules'));
277
- return !hasLegacy;
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
- return files.length > 0;
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
- return memories.length > 0;
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
- return rules.length > 0;
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(rules);
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(rules);
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(rules);
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 /skill|capability|tool.*use|cascade.*skill/i.test(rules);
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
- const content = coreRulesContent(ctx) || allRulesContent(ctx);
929
- if (!content.trim()) return null;
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) return /\bverify\b|\btest\b|\blint\b|\bbuild\b/i.test(content);
932
- return expected.every(cat => hasCommandMention(content, cat));
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
- const content = allRulesContent(ctx);
948
- if (!content.trim()) return null;
949
- return hasArchitecture(content);
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
- const content = allRulesContent(ctx);
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
- /src\/|app\/|api\/|routes\/|services\/|components\/|lib\/|cmd\//i.test(content);
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
- const content = allRulesContent(ctx);
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
- const legacy = ctx.legacyWindsurfrules ? ctx.legacyWindsurfrules() : null;
1492
- if (!legacy) return null;
1493
- return false; // Legacy exists = deprecated pattern
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
- const content = allRulesContent(ctx);
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
- const content = allRulesContent(ctx);
1577
- if (!content.trim()) return null;
1578
- return /session.*drift|context.*window|long.*session|session.*length|refresh.*context/i.test(content);
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
- const relevant = os.platform() === 'win32' || /\bwsl\b|\bwindows\b/i.test(docs);
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
- return Boolean(pkg || ctx.fileContent('go.mod') || ctx.fileContent('Cargo.toml') || ctx.fileContent('pyproject.toml'));
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
- return Object.keys(servers).length > 0;
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,