@nerviq/cli 1.18.0 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -23
- package/README.md +2 -2
- package/bin/cli.js +130 -130
- package/package.json +1 -1
- package/src/activity.js +1039 -1039
- package/src/adoption-advisor.js +299 -299
- package/src/aider/config-parser.js +166 -166
- package/src/aider/context.js +158 -158
- package/src/aider/deep-review.js +316 -316
- package/src/aider/domain-packs.js +303 -303
- package/src/aider/freshness.js +93 -93
- package/src/aider/governance.js +253 -253
- package/src/aider/interactive.js +334 -334
- package/src/aider/mcp-packs.js +329 -329
- package/src/aider/patch.js +214 -214
- package/src/aider/plans.js +186 -186
- package/src/aider/premium.js +360 -360
- package/src/aider/setup.js +404 -404
- package/src/aider/techniques.js +16 -16
- package/src/analyze.js +951 -951
- package/src/anti-patterns.js +485 -485
- package/src/audit/instruction-files.js +180 -180
- package/src/audit/recommendations.js +577 -577
- package/src/auto-suggest.js +154 -154
- package/src/badge.js +13 -13
- package/src/behavioral-drift.js +801 -801
- package/src/benchmark.js +67 -67
- package/src/catalog.js +103 -103
- package/src/certification.js +128 -128
- package/src/codex/config-parser.js +183 -183
- package/src/codex/context.js +223 -223
- package/src/codex/deep-review.js +493 -493
- package/src/codex/domain-packs.js +394 -394
- package/src/codex/freshness.js +84 -84
- package/src/codex/governance.js +192 -192
- package/src/codex/interactive.js +618 -618
- package/src/codex/mcp-packs.js +914 -914
- package/src/codex/patch.js +209 -209
- package/src/codex/plans.js +251 -251
- package/src/codex/premium.js +614 -614
- package/src/codex/setup.js +591 -591
- package/src/context.js +320 -320
- package/src/continuous-ops.js +681 -681
- package/src/copilot/activity.js +309 -309
- package/src/copilot/deep-review.js +346 -346
- package/src/copilot/domain-packs.js +372 -372
- package/src/copilot/freshness.js +57 -57
- package/src/copilot/governance.js +222 -222
- package/src/copilot/interactive.js +406 -406
- package/src/copilot/mcp-packs.js +826 -826
- package/src/copilot/plans.js +253 -253
- package/src/copilot/premium.js +451 -451
- package/src/copilot/setup.js +488 -488
- package/src/cost-tracking.js +61 -61
- package/src/cursor/activity.js +301 -301
- package/src/cursor/config-parser.js +265 -265
- package/src/cursor/context.js +256 -256
- package/src/cursor/deep-review.js +334 -334
- package/src/cursor/domain-packs.js +368 -368
- package/src/cursor/freshness.js +65 -65
- package/src/cursor/governance.js +229 -229
- package/src/cursor/interactive.js +391 -391
- package/src/cursor/mcp-packs.js +828 -828
- package/src/cursor/plans.js +254 -254
- package/src/cursor/premium.js +469 -469
- package/src/cursor/setup.js +488 -488
- package/src/dashboard.js +493 -493
- package/src/deep-review.js +428 -428
- package/src/deprecation.js +98 -98
- package/src/diff-only.js +280 -280
- package/src/doctor.js +119 -119
- package/src/domain-pack-expansion.js +1033 -1033
- package/src/domain-packs.js +387 -387
- package/src/feedback.js +178 -178
- package/src/fix-engine.js +783 -783
- package/src/fix-prompts.js +122 -122
- package/src/formatters/sarif.js +115 -115
- package/src/freshness.js +74 -74
- package/src/gemini/config-parser.js +275 -275
- package/src/gemini/context.js +221 -221
- package/src/gemini/deep-review.js +559 -559
- package/src/gemini/domain-packs.js +393 -393
- package/src/gemini/freshness.js +66 -66
- package/src/gemini/governance.js +201 -201
- package/src/gemini/interactive.js +860 -860
- package/src/gemini/mcp-packs.js +915 -915
- package/src/gemini/plans.js +269 -269
- package/src/gemini/premium.js +760 -760
- package/src/gemini/setup.js +692 -692
- package/src/gemini/techniques.js +14 -14
- package/src/governance.js +72 -72
- package/src/harmony/add.js +68 -68
- package/src/harmony/advisor.js +333 -333
- package/src/harmony/canon.js +565 -565
- package/src/harmony/cli.js +591 -591
- package/src/harmony/drift.js +401 -401
- package/src/harmony/governance.js +313 -313
- package/src/harmony/memory.js +239 -239
- package/src/harmony/sync.js +475 -475
- package/src/harmony/watch.js +370 -370
- package/src/hook-validation.js +342 -342
- package/src/index.js +271 -271
- package/src/init.js +184 -184
- package/src/instruction-surfaces.js +185 -185
- package/src/integrations.js +144 -144
- package/src/interactive.js +118 -118
- package/src/locales/en.json +1 -1
- package/src/locales/es.json +1 -1
- package/src/mcp-packs.js +830 -830
- package/src/mcp-server.js +726 -726
- package/src/mcp-validation.js +337 -337
- package/src/nerviq-sync.json +7 -7
- package/src/opencode/config-parser.js +109 -109
- package/src/opencode/context.js +247 -247
- package/src/opencode/deep-review.js +313 -313
- package/src/opencode/domain-packs.js +262 -262
- package/src/opencode/freshness.js +66 -66
- package/src/opencode/governance.js +159 -159
- package/src/opencode/interactive.js +392 -392
- package/src/opencode/mcp-packs.js +705 -705
- package/src/opencode/patch.js +184 -184
- package/src/opencode/plans.js +231 -231
- package/src/opencode/premium.js +413 -413
- package/src/opencode/setup.js +449 -449
- package/src/opencode/techniques.js +27 -27
- package/src/operating-profile.js +574 -574
- package/src/org.js +152 -152
- package/src/permission-rules.js +218 -218
- package/src/plans.js +839 -839
- package/src/platform-change-manifest.js +86 -86
- package/src/plugins.js +110 -110
- package/src/policy-layers.js +210 -210
- package/src/profiles.js +124 -124
- package/src/prompt-injection.js +74 -74
- package/src/public-api.js +173 -173
- package/src/recommendation-rules.js +84 -84
- package/src/repo-archetype.js +386 -386
- package/src/secret-patterns.js +39 -39
- package/src/server.js +527 -527
- package/src/setup/analysis.js +607 -607
- package/src/setup/runtime.js +172 -172
- package/src/setup.js +677 -677
- package/src/shared/capabilities.js +194 -194
- package/src/source-urls.js +132 -132
- package/src/stack-checks.js +565 -565
- package/src/supplemental-checks.js +13 -13
- package/src/synergy/adaptive.js +261 -261
- package/src/synergy/compensation.js +137 -137
- package/src/synergy/evidence.js +193 -193
- package/src/synergy/learning.js +199 -199
- package/src/synergy/patterns.js +227 -227
- package/src/synergy/ranking.js +83 -83
- package/src/synergy/report.js +165 -165
- package/src/synergy/routing.js +146 -146
- package/src/techniques/api.js +407 -407
- package/src/techniques/automation.js +316 -316
- package/src/techniques/compliance.js +257 -257
- package/src/techniques/hygiene.js +294 -294
- package/src/techniques/instructions.js +243 -243
- package/src/techniques/observability.js +226 -226
- package/src/techniques/optimization.js +142 -142
- package/src/techniques/quality.js +318 -318
- package/src/techniques/security.js +237 -237
- package/src/techniques/shared.js +443 -443
- package/src/techniques/stacks.js +2294 -2294
- package/src/techniques/tools.js +106 -106
- package/src/techniques/workflow.js +413 -413
- package/src/techniques.js +81 -81
- package/src/terminology.js +73 -73
- package/src/token-estimate.js +35 -35
- package/src/usage-patterns.js +99 -99
- package/src/verification-metadata.js +145 -145
- package/src/watch.js +247 -247
- package/src/windsurf/activity.js +302 -302
- package/src/windsurf/config-parser.js +267 -267
- package/src/windsurf/context.js +249 -249
- package/src/windsurf/deep-review.js +337 -337
- package/src/windsurf/domain-packs.js +370 -370
- package/src/windsurf/freshness.js +36 -36
- package/src/windsurf/governance.js +231 -231
- package/src/windsurf/interactive.js +388 -388
- package/src/windsurf/mcp-packs.js +792 -792
- package/src/windsurf/plans.js +247 -247
- package/src/windsurf/premium.js +468 -468
- package/src/windsurf/setup.js +471 -471
- package/src/windsurf/techniques.js +17 -17
- package/src/workspace.js +375 -375
package/src/windsurf/activity.js
CHANGED
|
@@ -1,302 +1,302 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Windsurf Repeat-Usage Surfaces
|
|
3
|
-
*
|
|
4
|
-
* Adapts the shared activity/snapshot backend for Windsurf platform.
|
|
5
|
-
* Provides: history, compare, trend, feedback, insights.
|
|
6
|
-
*
|
|
7
|
-
* 6 repeat-usage surfaces filtered by platform='windsurf':
|
|
8
|
-
* 1. History — audit snapshot history
|
|
9
|
-
* 2. Compare — latest vs previous snapshot
|
|
10
|
-
* 3. Trend — score trend over time
|
|
11
|
-
* 4. Feedback — recommendation outcome tracking
|
|
12
|
-
* 5. Insights — pattern detection from history
|
|
13
|
-
* 6. Surface tracking — per-surface (rules/workflows/memories) progress
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
const path = require('path');
|
|
17
|
-
const {
|
|
18
|
-
readSnapshotIndex,
|
|
19
|
-
recordRecommendationOutcome,
|
|
20
|
-
readOutcomeIndex,
|
|
21
|
-
summarizeOutcomeEntries,
|
|
22
|
-
} = require('../activity');
|
|
23
|
-
const { version } = require('../../package.json');
|
|
24
|
-
|
|
25
|
-
// --- History ---
|
|
26
|
-
|
|
27
|
-
function getWindsurfHistory(dir, limit = 20) {
|
|
28
|
-
const entries = readSnapshotIndex(dir);
|
|
29
|
-
return entries
|
|
30
|
-
.filter(e => e.snapshotKind === 'audit' && (e.platform === 'windsurf' || e.summary?.platform === 'windsurf'))
|
|
31
|
-
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
|
|
32
|
-
.slice(0, limit);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function formatWindsurfHistory(dir) {
|
|
36
|
-
const history = getWindsurfHistory(dir, 10);
|
|
37
|
-
if (history.length === 0) {
|
|
38
|
-
return 'No Windsurf snapshots found. Run `npx nerviq --platform windsurf --snapshot` to save one.';
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const lines = ['Windsurf Score History (most recent first):', ''];
|
|
42
|
-
for (const entry of history) {
|
|
43
|
-
const date = entry.createdAt?.split('T')[0] || 'unknown';
|
|
44
|
-
const score = entry.summary?.score ?? '?';
|
|
45
|
-
const passed = entry.summary?.passed ?? '?';
|
|
46
|
-
const total = entry.summary?.checkCount ?? '?';
|
|
47
|
-
const surfaces = entry.summary?.surfaces || {};
|
|
48
|
-
const surfaceStr = [
|
|
49
|
-
surfaces.foreground ? 'FG' : null,
|
|
50
|
-
surfaces.workflows ? 'WF' : null,
|
|
51
|
-
surfaces.memories ? 'Mem' : null,
|
|
52
|
-
surfaces.cascadeignore ? 'CI' : null,
|
|
53
|
-
].filter(Boolean).join('+') || 'unknown';
|
|
54
|
-
lines.push(` ${date} ${score}/100 (${passed}/${total} passing) [${surfaceStr}]`);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const comparison = compareWindsurfLatest(dir);
|
|
58
|
-
if (comparison) {
|
|
59
|
-
lines.push('');
|
|
60
|
-
const sign = comparison.delta.score >= 0 ? '+' : '';
|
|
61
|
-
lines.push(` Trend: ${comparison.trend} (${sign}${comparison.delta.score} since previous)`);
|
|
62
|
-
if (comparison.improvements.length > 0) lines.push(` Fixed: ${comparison.improvements.join(', ')}`);
|
|
63
|
-
if (comparison.regressions.length > 0) lines.push(` New gaps: ${comparison.regressions.join(', ')}`);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return lines.join('\n');
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// --- Compare ---
|
|
70
|
-
|
|
71
|
-
function compareWindsurfLatest(dir) {
|
|
72
|
-
const audits = getWindsurfHistory(dir, 2);
|
|
73
|
-
if (audits.length < 2) return null;
|
|
74
|
-
|
|
75
|
-
const current = audits[0];
|
|
76
|
-
const previous = audits[1];
|
|
77
|
-
|
|
78
|
-
const delta = {
|
|
79
|
-
score: (current.summary?.score || 0) - (previous.summary?.score || 0),
|
|
80
|
-
organic: (current.summary?.organicScore || 0) - (previous.summary?.organicScore || 0),
|
|
81
|
-
passed: (current.summary?.passed || 0) - (previous.summary?.passed || 0),
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
const regressions = [];
|
|
85
|
-
const improvements = [];
|
|
86
|
-
const prevKeys = new Set(previous.summary?.topActionKeys || []);
|
|
87
|
-
const currKeys = new Set(current.summary?.topActionKeys || []);
|
|
88
|
-
|
|
89
|
-
for (const key of currKeys) { if (!prevKeys.has(key)) regressions.push(key); }
|
|
90
|
-
for (const key of prevKeys) { if (!currKeys.has(key)) improvements.push(key); }
|
|
91
|
-
|
|
92
|
-
return {
|
|
93
|
-
platform: 'windsurf',
|
|
94
|
-
current: { date: current.createdAt, score: current.summary?.score, passed: current.summary?.passed },
|
|
95
|
-
previous: { date: previous.createdAt, score: previous.summary?.score, passed: previous.summary?.passed },
|
|
96
|
-
delta,
|
|
97
|
-
regressions,
|
|
98
|
-
improvements,
|
|
99
|
-
trend: delta.score > 0 ? 'improving' : delta.score < 0 ? 'regressing' : 'stable',
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// --- Trend ---
|
|
104
|
-
|
|
105
|
-
function exportWindsurfTrendReport(dir) {
|
|
106
|
-
const history = getWindsurfHistory(dir, 50);
|
|
107
|
-
if (history.length === 0) return null;
|
|
108
|
-
|
|
109
|
-
const comparison = compareWindsurfLatest(dir);
|
|
110
|
-
const lines = [
|
|
111
|
-
'# Windsurf Setup Trend Report',
|
|
112
|
-
'',
|
|
113
|
-
`**Project:** ${path.basename(dir)}`,
|
|
114
|
-
`**Platform:** Windsurf (Cascade)`,
|
|
115
|
-
`**Generated:** ${new Date().toISOString().split('T')[0]}`,
|
|
116
|
-
`**Snapshots:** ${history.length}`,
|
|
117
|
-
'',
|
|
118
|
-
'## Score History',
|
|
119
|
-
'',
|
|
120
|
-
'| Date | Score | Passed | Checks | Surfaces |',
|
|
121
|
-
'|------|-------|--------|--------|----------|',
|
|
122
|
-
];
|
|
123
|
-
|
|
124
|
-
for (const entry of history) {
|
|
125
|
-
const date = entry.createdAt?.split('T')[0] || '?';
|
|
126
|
-
const surfaces = entry.summary?.surfaces || {};
|
|
127
|
-
const surfaceStr = [surfaces.foreground ? 'FG' : null, surfaces.workflows ? 'WF' : null, surfaces.memories ? 'Mem' : null].filter(Boolean).join('+') || '?';
|
|
128
|
-
lines.push(`| ${date} | ${entry.summary?.score ?? '?'}/100 | ${entry.summary?.passed ?? '?'} | ${entry.summary?.checkCount ?? '?'} | ${surfaceStr} |`);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (comparison) {
|
|
132
|
-
lines.push('', '## Latest Comparison', '');
|
|
133
|
-
lines.push(`- **Previous:** ${comparison.previous.score}/100 (${comparison.previous.date?.split('T')[0]})`);
|
|
134
|
-
lines.push(`- **Current:** ${comparison.current.score}/100 (${comparison.current.date?.split('T')[0]})`);
|
|
135
|
-
lines.push(`- **Delta:** ${comparison.delta.score >= 0 ? '+' : ''}${comparison.delta.score} points`);
|
|
136
|
-
lines.push(`- **Trend:** ${comparison.trend}`);
|
|
137
|
-
if (comparison.improvements.length > 0) lines.push(`- **Fixed:** ${comparison.improvements.join(', ')}`);
|
|
138
|
-
if (comparison.regressions.length > 0) lines.push(`- **New gaps:** ${comparison.regressions.join(', ')}`);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (history.length >= 3) {
|
|
142
|
-
lines.push('', '## Trend Chart', '', '```');
|
|
143
|
-
const scores = history.slice().reverse().map(e => e.summary?.score ?? 0);
|
|
144
|
-
const max = Math.max(...scores, 100);
|
|
145
|
-
const chartHeight = 10;
|
|
146
|
-
for (let row = chartHeight; row >= 0; row--) {
|
|
147
|
-
const threshold = (row / chartHeight) * max;
|
|
148
|
-
const rowLabel = String(Math.round(threshold)).padStart(3);
|
|
149
|
-
const bar = scores.map(s => s >= threshold ? '#' : ' ').join('');
|
|
150
|
-
lines.push(`${rowLabel} |${bar}`);
|
|
151
|
-
}
|
|
152
|
-
lines.push(` +${'─'.repeat(scores.length)}`);
|
|
153
|
-
lines.push('```');
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
lines.push('', '---', `*Generated by nerviq v${version} for Windsurf*`);
|
|
157
|
-
return lines.join('\n');
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// --- Feedback ---
|
|
161
|
-
|
|
162
|
-
function recordWindsurfFeedback(dir, payload) {
|
|
163
|
-
return recordRecommendationOutcome(dir, {
|
|
164
|
-
...payload,
|
|
165
|
-
source: payload.source || 'windsurf',
|
|
166
|
-
platform: 'windsurf',
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function getWindsurfFeedbackSummary(dir) {
|
|
171
|
-
const entries = readOutcomeIndex(dir)
|
|
172
|
-
.filter(e => e.source === 'windsurf' || e.platform === 'windsurf');
|
|
173
|
-
return summarizeOutcomeEntries(entries);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function formatWindsurfFeedback(dir) {
|
|
177
|
-
const summary = getWindsurfFeedbackSummary(dir);
|
|
178
|
-
if (!summary || Object.keys(summary).length === 0) {
|
|
179
|
-
return 'No Windsurf feedback recorded yet. Use `npx nerviq --platform windsurf feedback` to rate recommendations.';
|
|
180
|
-
}
|
|
181
|
-
const lines = ['Windsurf Recommendation Feedback:', ''];
|
|
182
|
-
const entries = Array.isArray(summary) ? summary : Object.values(summary);
|
|
183
|
-
for (const entry of entries) {
|
|
184
|
-
lines.push(` ${entry.key || 'unknown'}: ${entry.accepted || 0} accepted, ${entry.rejected || 0} rejected (${entry.total || 0} total)`);
|
|
185
|
-
}
|
|
186
|
-
return lines.join('\n');
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// --- Insights ---
|
|
190
|
-
|
|
191
|
-
function generateWindsurfInsights(dir) {
|
|
192
|
-
const history = getWindsurfHistory(dir, 50);
|
|
193
|
-
const feedback = getWindsurfFeedbackSummary(dir);
|
|
194
|
-
const insights = [];
|
|
195
|
-
|
|
196
|
-
// Pattern 1: Persistent failures
|
|
197
|
-
if (history.length >= 3) {
|
|
198
|
-
const recentFailKeys = new Map();
|
|
199
|
-
for (const entry of history.slice(0, 5)) {
|
|
200
|
-
for (const key of (entry.summary?.topActionKeys || [])) {
|
|
201
|
-
recentFailKeys.set(key, (recentFailKeys.get(key) || 0) + 1);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
for (const [key, count] of recentFailKeys) {
|
|
205
|
-
if (count >= 3) {
|
|
206
|
-
insights.push({ type: 'persistent-failure', severity: 'high', key, message: `Check ${key} has failed in ${count} of the last ${Math.min(history.length, 5)} audits.` });
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Pattern 2: Score regression
|
|
212
|
-
if (history.length >= 2) {
|
|
213
|
-
const scores = history.map(e => e.summary?.score ?? 0);
|
|
214
|
-
if (scores[0] < scores[1]) {
|
|
215
|
-
insights.push({ type: 'regression-pattern', severity: 'medium', message: `Score dropped from ${scores[1]} to ${scores[0]} in the most recent audit.`, delta: scores[0] - scores[1] });
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Pattern 3: Velocity stall
|
|
220
|
-
if (history.length >= 5) {
|
|
221
|
-
const recentScores = history.slice(0, 5).map(e => e.summary?.score ?? 0);
|
|
222
|
-
const range = Math.max(...recentScores) - Math.min(...recentScores);
|
|
223
|
-
if (range <= 2) {
|
|
224
|
-
insights.push({ type: 'velocity-stall', severity: 'low', message: `Score flat (range: ${range}) over last 5 audits.` });
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Windsurf-specific Pattern 4: Legacy .windsurfrules persistence
|
|
229
|
-
if (history.length >= 3) {
|
|
230
|
-
const legacyKeys = [];
|
|
231
|
-
for (const entry of history.slice(0, 5)) {
|
|
232
|
-
for (const key of (entry.summary?.topActionKeys || [])) {
|
|
233
|
-
if (key.includes('legacy') || key.includes('windsurfrules') || key === 'windsurfNoLegacyWindsurfrules') {
|
|
234
|
-
legacyKeys.push(key);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
if (legacyKeys.length >= 2) {
|
|
239
|
-
insights.push({ type: 'legacy-migration-stall', severity: 'high', message: `Legacy .windsurfrules migration check has persisted across ${legacyKeys.length} audits. Migrate to .windsurf/rules/*.md.`, keys: [...new Set(legacyKeys)] });
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Windsurf-specific Pattern 5: Memories secrets risk
|
|
244
|
-
if (history.length >= 2) {
|
|
245
|
-
const latest = history[0];
|
|
246
|
-
const memoryKeys = (latest.summary?.topActionKeys || []).filter(k => k.includes('memor'));
|
|
247
|
-
if (memoryKeys.length >= 1) {
|
|
248
|
-
insights.push({ type: 'memories-secrets-risk', severity: 'high', message: `Memory-related checks failing. Memories sync across team — check for secrets/PII.`, keys: memoryKeys });
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Windsurf-specific Pattern 6: Missing cascadeignore
|
|
253
|
-
if (history.length >= 2) {
|
|
254
|
-
const latest = history[0];
|
|
255
|
-
const cascadeKeys = (latest.summary?.topActionKeys || []).filter(k => k.includes('cascade') || k.includes('ignore'));
|
|
256
|
-
if (cascadeKeys.length >= 1) {
|
|
257
|
-
insights.push({ type: 'cascadeignore-gap', severity: 'medium', message: `Cascadeignore checks failing. Use .cascadeignore to protect sensitive files.`, keys: cascadeKeys });
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Feedback signals
|
|
262
|
-
const feedbackEntries = Array.isArray(feedback) ? feedback : Object.values(feedback || {});
|
|
263
|
-
for (const entry of feedbackEntries) {
|
|
264
|
-
if (entry.rejected > entry.accepted && entry.total >= 2) {
|
|
265
|
-
insights.push({ type: 'feedback-signal', severity: 'medium', key: entry.key, message: `Recommendation ${entry.key} has been rejected more than accepted (${entry.rejected}/${entry.total}).` });
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
return {
|
|
270
|
-
platform: 'windsurf',
|
|
271
|
-
generatedAt: new Date().toISOString(),
|
|
272
|
-
snapshotCount: history.length,
|
|
273
|
-
feedbackCount: feedbackEntries.length,
|
|
274
|
-
insights,
|
|
275
|
-
summary: insights.length === 0
|
|
276
|
-
? 'No actionable insights detected. Keep running audits to build pattern data.'
|
|
277
|
-
: `${insights.length} insight(s) detected across ${history.length} snapshots.`,
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function formatWindsurfInsights(dir) {
|
|
282
|
-
const result = generateWindsurfInsights(dir);
|
|
283
|
-
if (result.insights.length === 0) return result.summary;
|
|
284
|
-
const lines = ['Windsurf Insights:', ''];
|
|
285
|
-
for (const insight of result.insights) {
|
|
286
|
-
lines.push(` [${insight.severity.toUpperCase()}] ${insight.message}`);
|
|
287
|
-
}
|
|
288
|
-
lines.push('', result.summary);
|
|
289
|
-
return lines.join('\n');
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
module.exports = {
|
|
293
|
-
getWindsurfHistory,
|
|
294
|
-
formatWindsurfHistory,
|
|
295
|
-
compareWindsurfLatest,
|
|
296
|
-
exportWindsurfTrendReport,
|
|
297
|
-
recordWindsurfFeedback,
|
|
298
|
-
getWindsurfFeedbackSummary,
|
|
299
|
-
formatWindsurfFeedback,
|
|
300
|
-
generateWindsurfInsights,
|
|
301
|
-
formatWindsurfInsights,
|
|
302
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Windsurf Repeat-Usage Surfaces
|
|
3
|
+
*
|
|
4
|
+
* Adapts the shared activity/snapshot backend for Windsurf platform.
|
|
5
|
+
* Provides: history, compare, trend, feedback, insights.
|
|
6
|
+
*
|
|
7
|
+
* 6 repeat-usage surfaces filtered by platform='windsurf':
|
|
8
|
+
* 1. History — audit snapshot history
|
|
9
|
+
* 2. Compare — latest vs previous snapshot
|
|
10
|
+
* 3. Trend — score trend over time
|
|
11
|
+
* 4. Feedback — recommendation outcome tracking
|
|
12
|
+
* 5. Insights — pattern detection from history
|
|
13
|
+
* 6. Surface tracking — per-surface (rules/workflows/memories) progress
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const {
|
|
18
|
+
readSnapshotIndex,
|
|
19
|
+
recordRecommendationOutcome,
|
|
20
|
+
readOutcomeIndex,
|
|
21
|
+
summarizeOutcomeEntries,
|
|
22
|
+
} = require('../activity');
|
|
23
|
+
const { version } = require('../../package.json');
|
|
24
|
+
|
|
25
|
+
// --- History ---
|
|
26
|
+
|
|
27
|
+
function getWindsurfHistory(dir, limit = 20) {
|
|
28
|
+
const entries = readSnapshotIndex(dir);
|
|
29
|
+
return entries
|
|
30
|
+
.filter(e => e.snapshotKind === 'audit' && (e.platform === 'windsurf' || e.summary?.platform === 'windsurf'))
|
|
31
|
+
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
|
|
32
|
+
.slice(0, limit);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function formatWindsurfHistory(dir) {
|
|
36
|
+
const history = getWindsurfHistory(dir, 10);
|
|
37
|
+
if (history.length === 0) {
|
|
38
|
+
return 'No Windsurf snapshots found. Run `npx nerviq --platform windsurf --snapshot` to save one.';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const lines = ['Windsurf Score History (most recent first):', ''];
|
|
42
|
+
for (const entry of history) {
|
|
43
|
+
const date = entry.createdAt?.split('T')[0] || 'unknown';
|
|
44
|
+
const score = entry.summary?.score ?? '?';
|
|
45
|
+
const passed = entry.summary?.passed ?? '?';
|
|
46
|
+
const total = entry.summary?.checkCount ?? '?';
|
|
47
|
+
const surfaces = entry.summary?.surfaces || {};
|
|
48
|
+
const surfaceStr = [
|
|
49
|
+
surfaces.foreground ? 'FG' : null,
|
|
50
|
+
surfaces.workflows ? 'WF' : null,
|
|
51
|
+
surfaces.memories ? 'Mem' : null,
|
|
52
|
+
surfaces.cascadeignore ? 'CI' : null,
|
|
53
|
+
].filter(Boolean).join('+') || 'unknown';
|
|
54
|
+
lines.push(` ${date} ${score}/100 (${passed}/${total} passing) [${surfaceStr}]`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const comparison = compareWindsurfLatest(dir);
|
|
58
|
+
if (comparison) {
|
|
59
|
+
lines.push('');
|
|
60
|
+
const sign = comparison.delta.score >= 0 ? '+' : '';
|
|
61
|
+
lines.push(` Trend: ${comparison.trend} (${sign}${comparison.delta.score} since previous)`);
|
|
62
|
+
if (comparison.improvements.length > 0) lines.push(` Fixed: ${comparison.improvements.join(', ')}`);
|
|
63
|
+
if (comparison.regressions.length > 0) lines.push(` New gaps: ${comparison.regressions.join(', ')}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return lines.join('\n');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// --- Compare ---
|
|
70
|
+
|
|
71
|
+
function compareWindsurfLatest(dir) {
|
|
72
|
+
const audits = getWindsurfHistory(dir, 2);
|
|
73
|
+
if (audits.length < 2) return null;
|
|
74
|
+
|
|
75
|
+
const current = audits[0];
|
|
76
|
+
const previous = audits[1];
|
|
77
|
+
|
|
78
|
+
const delta = {
|
|
79
|
+
score: (current.summary?.score || 0) - (previous.summary?.score || 0),
|
|
80
|
+
organic: (current.summary?.organicScore || 0) - (previous.summary?.organicScore || 0),
|
|
81
|
+
passed: (current.summary?.passed || 0) - (previous.summary?.passed || 0),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const regressions = [];
|
|
85
|
+
const improvements = [];
|
|
86
|
+
const prevKeys = new Set(previous.summary?.topActionKeys || []);
|
|
87
|
+
const currKeys = new Set(current.summary?.topActionKeys || []);
|
|
88
|
+
|
|
89
|
+
for (const key of currKeys) { if (!prevKeys.has(key)) regressions.push(key); }
|
|
90
|
+
for (const key of prevKeys) { if (!currKeys.has(key)) improvements.push(key); }
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
platform: 'windsurf',
|
|
94
|
+
current: { date: current.createdAt, score: current.summary?.score, passed: current.summary?.passed },
|
|
95
|
+
previous: { date: previous.createdAt, score: previous.summary?.score, passed: previous.summary?.passed },
|
|
96
|
+
delta,
|
|
97
|
+
regressions,
|
|
98
|
+
improvements,
|
|
99
|
+
trend: delta.score > 0 ? 'improving' : delta.score < 0 ? 'regressing' : 'stable',
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// --- Trend ---
|
|
104
|
+
|
|
105
|
+
function exportWindsurfTrendReport(dir) {
|
|
106
|
+
const history = getWindsurfHistory(dir, 50);
|
|
107
|
+
if (history.length === 0) return null;
|
|
108
|
+
|
|
109
|
+
const comparison = compareWindsurfLatest(dir);
|
|
110
|
+
const lines = [
|
|
111
|
+
'# Windsurf Setup Trend Report',
|
|
112
|
+
'',
|
|
113
|
+
`**Project:** ${path.basename(dir)}`,
|
|
114
|
+
`**Platform:** Windsurf (Cascade)`,
|
|
115
|
+
`**Generated:** ${new Date().toISOString().split('T')[0]}`,
|
|
116
|
+
`**Snapshots:** ${history.length}`,
|
|
117
|
+
'',
|
|
118
|
+
'## Score History',
|
|
119
|
+
'',
|
|
120
|
+
'| Date | Score | Passed | Checks | Surfaces |',
|
|
121
|
+
'|------|-------|--------|--------|----------|',
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
for (const entry of history) {
|
|
125
|
+
const date = entry.createdAt?.split('T')[0] || '?';
|
|
126
|
+
const surfaces = entry.summary?.surfaces || {};
|
|
127
|
+
const surfaceStr = [surfaces.foreground ? 'FG' : null, surfaces.workflows ? 'WF' : null, surfaces.memories ? 'Mem' : null].filter(Boolean).join('+') || '?';
|
|
128
|
+
lines.push(`| ${date} | ${entry.summary?.score ?? '?'}/100 | ${entry.summary?.passed ?? '?'} | ${entry.summary?.checkCount ?? '?'} | ${surfaceStr} |`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (comparison) {
|
|
132
|
+
lines.push('', '## Latest Comparison', '');
|
|
133
|
+
lines.push(`- **Previous:** ${comparison.previous.score}/100 (${comparison.previous.date?.split('T')[0]})`);
|
|
134
|
+
lines.push(`- **Current:** ${comparison.current.score}/100 (${comparison.current.date?.split('T')[0]})`);
|
|
135
|
+
lines.push(`- **Delta:** ${comparison.delta.score >= 0 ? '+' : ''}${comparison.delta.score} points`);
|
|
136
|
+
lines.push(`- **Trend:** ${comparison.trend}`);
|
|
137
|
+
if (comparison.improvements.length > 0) lines.push(`- **Fixed:** ${comparison.improvements.join(', ')}`);
|
|
138
|
+
if (comparison.regressions.length > 0) lines.push(`- **New gaps:** ${comparison.regressions.join(', ')}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (history.length >= 3) {
|
|
142
|
+
lines.push('', '## Trend Chart', '', '```');
|
|
143
|
+
const scores = history.slice().reverse().map(e => e.summary?.score ?? 0);
|
|
144
|
+
const max = Math.max(...scores, 100);
|
|
145
|
+
const chartHeight = 10;
|
|
146
|
+
for (let row = chartHeight; row >= 0; row--) {
|
|
147
|
+
const threshold = (row / chartHeight) * max;
|
|
148
|
+
const rowLabel = String(Math.round(threshold)).padStart(3);
|
|
149
|
+
const bar = scores.map(s => s >= threshold ? '#' : ' ').join('');
|
|
150
|
+
lines.push(`${rowLabel} |${bar}`);
|
|
151
|
+
}
|
|
152
|
+
lines.push(` +${'─'.repeat(scores.length)}`);
|
|
153
|
+
lines.push('```');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
lines.push('', '---', `*Generated by nerviq v${version} for Windsurf*`);
|
|
157
|
+
return lines.join('\n');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// --- Feedback ---
|
|
161
|
+
|
|
162
|
+
function recordWindsurfFeedback(dir, payload) {
|
|
163
|
+
return recordRecommendationOutcome(dir, {
|
|
164
|
+
...payload,
|
|
165
|
+
source: payload.source || 'windsurf',
|
|
166
|
+
platform: 'windsurf',
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function getWindsurfFeedbackSummary(dir) {
|
|
171
|
+
const entries = readOutcomeIndex(dir)
|
|
172
|
+
.filter(e => e.source === 'windsurf' || e.platform === 'windsurf');
|
|
173
|
+
return summarizeOutcomeEntries(entries);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function formatWindsurfFeedback(dir) {
|
|
177
|
+
const summary = getWindsurfFeedbackSummary(dir);
|
|
178
|
+
if (!summary || Object.keys(summary).length === 0) {
|
|
179
|
+
return 'No Windsurf feedback recorded yet. Use `npx nerviq --platform windsurf feedback` to rate recommendations.';
|
|
180
|
+
}
|
|
181
|
+
const lines = ['Windsurf Recommendation Feedback:', ''];
|
|
182
|
+
const entries = Array.isArray(summary) ? summary : Object.values(summary);
|
|
183
|
+
for (const entry of entries) {
|
|
184
|
+
lines.push(` ${entry.key || 'unknown'}: ${entry.accepted || 0} accepted, ${entry.rejected || 0} rejected (${entry.total || 0} total)`);
|
|
185
|
+
}
|
|
186
|
+
return lines.join('\n');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// --- Insights ---
|
|
190
|
+
|
|
191
|
+
function generateWindsurfInsights(dir) {
|
|
192
|
+
const history = getWindsurfHistory(dir, 50);
|
|
193
|
+
const feedback = getWindsurfFeedbackSummary(dir);
|
|
194
|
+
const insights = [];
|
|
195
|
+
|
|
196
|
+
// Pattern 1: Persistent failures
|
|
197
|
+
if (history.length >= 3) {
|
|
198
|
+
const recentFailKeys = new Map();
|
|
199
|
+
for (const entry of history.slice(0, 5)) {
|
|
200
|
+
for (const key of (entry.summary?.topActionKeys || [])) {
|
|
201
|
+
recentFailKeys.set(key, (recentFailKeys.get(key) || 0) + 1);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
for (const [key, count] of recentFailKeys) {
|
|
205
|
+
if (count >= 3) {
|
|
206
|
+
insights.push({ type: 'persistent-failure', severity: 'high', key, message: `Check ${key} has failed in ${count} of the last ${Math.min(history.length, 5)} audits.` });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Pattern 2: Score regression
|
|
212
|
+
if (history.length >= 2) {
|
|
213
|
+
const scores = history.map(e => e.summary?.score ?? 0);
|
|
214
|
+
if (scores[0] < scores[1]) {
|
|
215
|
+
insights.push({ type: 'regression-pattern', severity: 'medium', message: `Score dropped from ${scores[1]} to ${scores[0]} in the most recent audit.`, delta: scores[0] - scores[1] });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Pattern 3: Velocity stall
|
|
220
|
+
if (history.length >= 5) {
|
|
221
|
+
const recentScores = history.slice(0, 5).map(e => e.summary?.score ?? 0);
|
|
222
|
+
const range = Math.max(...recentScores) - Math.min(...recentScores);
|
|
223
|
+
if (range <= 2) {
|
|
224
|
+
insights.push({ type: 'velocity-stall', severity: 'low', message: `Score flat (range: ${range}) over last 5 audits.` });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Windsurf-specific Pattern 4: Legacy .windsurfrules persistence
|
|
229
|
+
if (history.length >= 3) {
|
|
230
|
+
const legacyKeys = [];
|
|
231
|
+
for (const entry of history.slice(0, 5)) {
|
|
232
|
+
for (const key of (entry.summary?.topActionKeys || [])) {
|
|
233
|
+
if (key.includes('legacy') || key.includes('windsurfrules') || key === 'windsurfNoLegacyWindsurfrules') {
|
|
234
|
+
legacyKeys.push(key);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (legacyKeys.length >= 2) {
|
|
239
|
+
insights.push({ type: 'legacy-migration-stall', severity: 'high', message: `Legacy .windsurfrules migration check has persisted across ${legacyKeys.length} audits. Migrate to .windsurf/rules/*.md.`, keys: [...new Set(legacyKeys)] });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Windsurf-specific Pattern 5: Memories secrets risk
|
|
244
|
+
if (history.length >= 2) {
|
|
245
|
+
const latest = history[0];
|
|
246
|
+
const memoryKeys = (latest.summary?.topActionKeys || []).filter(k => k.includes('memor'));
|
|
247
|
+
if (memoryKeys.length >= 1) {
|
|
248
|
+
insights.push({ type: 'memories-secrets-risk', severity: 'high', message: `Memory-related checks failing. Memories sync across team — check for secrets/PII.`, keys: memoryKeys });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Windsurf-specific Pattern 6: Missing cascadeignore
|
|
253
|
+
if (history.length >= 2) {
|
|
254
|
+
const latest = history[0];
|
|
255
|
+
const cascadeKeys = (latest.summary?.topActionKeys || []).filter(k => k.includes('cascade') || k.includes('ignore'));
|
|
256
|
+
if (cascadeKeys.length >= 1) {
|
|
257
|
+
insights.push({ type: 'cascadeignore-gap', severity: 'medium', message: `Cascadeignore checks failing. Use .cascadeignore to protect sensitive files.`, keys: cascadeKeys });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Feedback signals
|
|
262
|
+
const feedbackEntries = Array.isArray(feedback) ? feedback : Object.values(feedback || {});
|
|
263
|
+
for (const entry of feedbackEntries) {
|
|
264
|
+
if (entry.rejected > entry.accepted && entry.total >= 2) {
|
|
265
|
+
insights.push({ type: 'feedback-signal', severity: 'medium', key: entry.key, message: `Recommendation ${entry.key} has been rejected more than accepted (${entry.rejected}/${entry.total}).` });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
platform: 'windsurf',
|
|
271
|
+
generatedAt: new Date().toISOString(),
|
|
272
|
+
snapshotCount: history.length,
|
|
273
|
+
feedbackCount: feedbackEntries.length,
|
|
274
|
+
insights,
|
|
275
|
+
summary: insights.length === 0
|
|
276
|
+
? 'No actionable insights detected. Keep running audits to build pattern data.'
|
|
277
|
+
: `${insights.length} insight(s) detected across ${history.length} snapshots.`,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function formatWindsurfInsights(dir) {
|
|
282
|
+
const result = generateWindsurfInsights(dir);
|
|
283
|
+
if (result.insights.length === 0) return result.summary;
|
|
284
|
+
const lines = ['Windsurf Insights:', ''];
|
|
285
|
+
for (const insight of result.insights) {
|
|
286
|
+
lines.push(` [${insight.severity.toUpperCase()}] ${insight.message}`);
|
|
287
|
+
}
|
|
288
|
+
lines.push('', result.summary);
|
|
289
|
+
return lines.join('\n');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
module.exports = {
|
|
293
|
+
getWindsurfHistory,
|
|
294
|
+
formatWindsurfHistory,
|
|
295
|
+
compareWindsurfLatest,
|
|
296
|
+
exportWindsurfTrendReport,
|
|
297
|
+
recordWindsurfFeedback,
|
|
298
|
+
getWindsurfFeedbackSummary,
|
|
299
|
+
formatWindsurfFeedback,
|
|
300
|
+
generateWindsurfInsights,
|
|
301
|
+
formatWindsurfInsights,
|
|
302
|
+
};
|