@nerviq/cli 0.0.1 → 0.9.0-beta.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/CHANGELOG.md +181 -0
- package/LICENSE +21 -0
- package/README.md +447 -0
- package/bin/cli.js +749 -0
- package/content/case-study-template.md +91 -0
- package/content/claims-governance.md +37 -0
- package/content/claude-code/audit-repo/SKILL.md +20 -0
- package/content/claude-native-integration.md +60 -0
- package/content/devto-article.json +9 -0
- package/content/launch-posts.md +226 -0
- package/content/pilot-rollout-kit.md +30 -0
- package/content/release-checklist.md +31 -0
- package/package.json +53 -4
- package/src/activity.js +529 -0
- package/src/aider/activity.js +226 -0
- package/src/aider/config-parser.js +166 -0
- package/src/aider/context.js +158 -0
- package/src/aider/deep-review.js +316 -0
- package/src/aider/domain-packs.js +278 -0
- package/src/aider/freshness.js +168 -0
- package/src/aider/governance.js +253 -0
- package/src/aider/interactive.js +334 -0
- package/src/aider/mcp-packs.js +98 -0
- package/src/aider/patch.js +214 -0
- package/src/aider/plans.js +186 -0
- package/src/aider/premium.js +360 -0
- package/src/aider/setup.js +404 -0
- package/src/aider/techniques.js +1323 -0
- package/src/analyze.js +821 -0
- package/src/audit.js +1003 -0
- package/src/badge.js +13 -0
- package/src/benchmark.js +339 -0
- package/src/claudex-sync.json +7 -0
- package/src/codex/activity.js +324 -0
- package/src/codex/config-parser.js +183 -0
- package/src/codex/context.js +221 -0
- package/src/codex/deep-review.js +493 -0
- package/src/codex/domain-packs.js +372 -0
- package/src/codex/freshness.js +167 -0
- package/src/codex/governance.js +192 -0
- package/src/codex/interactive.js +618 -0
- package/src/codex/mcp-packs.js +660 -0
- package/src/codex/patch.js +209 -0
- package/src/codex/plans.js +251 -0
- package/src/codex/premium.js +614 -0
- package/src/codex/setup.js +603 -0
- package/src/codex/techniques.js +2649 -0
- package/src/context.js +272 -0
- package/src/copilot/activity.js +309 -0
- package/src/copilot/config-parser.js +226 -0
- package/src/copilot/context.js +197 -0
- package/src/copilot/deep-review.js +346 -0
- package/src/copilot/domain-packs.js +350 -0
- package/src/copilot/freshness.js +197 -0
- package/src/copilot/governance.js +222 -0
- package/src/copilot/interactive.js +406 -0
- package/src/copilot/mcp-packs.js +572 -0
- package/src/copilot/patch.js +238 -0
- package/src/copilot/plans.js +253 -0
- package/src/copilot/premium.js +450 -0
- package/src/copilot/setup.js +488 -0
- package/src/copilot/techniques.js +1822 -0
- package/src/cursor/activity.js +301 -0
- package/src/cursor/config-parser.js +265 -0
- package/src/cursor/context.js +236 -0
- package/src/cursor/deep-review.js +334 -0
- package/src/cursor/domain-packs.js +346 -0
- package/src/cursor/freshness.js +214 -0
- package/src/cursor/governance.js +229 -0
- package/src/cursor/interactive.js +391 -0
- package/src/cursor/mcp-packs.js +571 -0
- package/src/cursor/patch.js +243 -0
- package/src/cursor/plans.js +254 -0
- package/src/cursor/premium.js +468 -0
- package/src/cursor/setup.js +488 -0
- package/src/cursor/techniques.js +1786 -0
- package/src/deep-review.js +345 -0
- package/src/domain-packs.js +364 -0
- package/src/formatters/sarif.js +115 -0
- package/src/gemini/activity.js +402 -0
- package/src/gemini/config-parser.js +275 -0
- package/src/gemini/context.js +221 -0
- package/src/gemini/deep-review.js +559 -0
- package/src/gemini/domain-packs.js +371 -0
- package/src/gemini/freshness.js +204 -0
- package/src/gemini/governance.js +201 -0
- package/src/gemini/interactive.js +860 -0
- package/src/gemini/mcp-packs.js +658 -0
- package/src/gemini/patch.js +229 -0
- package/src/gemini/plans.js +269 -0
- package/src/gemini/premium.js +759 -0
- package/src/gemini/setup.js +692 -0
- package/src/gemini/techniques.js +2084 -0
- package/src/governance.js +523 -0
- package/src/harmony/advisor.js +383 -0
- package/src/harmony/audit.js +303 -0
- package/src/harmony/canon.js +444 -0
- package/src/harmony/cli.js +331 -0
- package/src/harmony/drift.js +401 -0
- package/src/harmony/governance.js +313 -0
- package/src/harmony/memory.js +238 -0
- package/src/harmony/sync.js +458 -0
- package/src/harmony/watch.js +336 -0
- package/src/index.js +256 -0
- package/src/insights.js +119 -0
- package/src/interactive.js +118 -0
- package/src/mcp-packs.js +597 -0
- package/src/opencode/activity.js +286 -0
- package/src/opencode/config-parser.js +109 -0
- package/src/opencode/context.js +247 -0
- package/src/opencode/deep-review.js +313 -0
- package/src/opencode/domain-packs.js +240 -0
- package/src/opencode/freshness.js +158 -0
- package/src/opencode/governance.js +159 -0
- package/src/opencode/interactive.js +392 -0
- package/src/opencode/mcp-packs.js +474 -0
- package/src/opencode/patch.js +184 -0
- package/src/opencode/plans.js +231 -0
- package/src/opencode/premium.js +413 -0
- package/src/opencode/setup.js +449 -0
- package/src/opencode/techniques.js +1713 -0
- package/src/plans.js +655 -0
- package/src/secret-patterns.js +30 -0
- package/src/setup.js +1274 -0
- package/src/synergy/adaptive.js +261 -0
- package/src/synergy/compensation.js +156 -0
- package/src/synergy/evidence.js +193 -0
- package/src/synergy/learning.js +184 -0
- package/src/synergy/patterns.js +227 -0
- package/src/synergy/ranking.js +83 -0
- package/src/synergy/report.js +163 -0
- package/src/synergy/routing.js +152 -0
- package/src/techniques.js +1354 -0
- package/src/watch.js +229 -0
- package/src/windsurf/activity.js +302 -0
- package/src/windsurf/config-parser.js +267 -0
- package/src/windsurf/context.js +249 -0
- package/src/windsurf/deep-review.js +337 -0
- package/src/windsurf/domain-packs.js +348 -0
- package/src/windsurf/freshness.js +215 -0
- package/src/windsurf/governance.js +231 -0
- package/src/windsurf/interactive.js +388 -0
- package/src/windsurf/mcp-packs.js +535 -0
- package/src/windsurf/patch.js +231 -0
- package/src/windsurf/plans.js +247 -0
- package/src/windsurf/premium.js +467 -0
- package/src/windsurf/setup.js +471 -0
- package/src/windsurf/techniques.js +1758 -0
package/src/watch.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watch mode - monitors project for Claude Code config changes and re-audits.
|
|
3
|
+
* Uses Node.js fs.watch (zero dependencies) with a recursive-directory fallback
|
|
4
|
+
* on platforms where native recursive watch is not reliable.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { audit } = require('./audit');
|
|
10
|
+
|
|
11
|
+
const COLORS = {
|
|
12
|
+
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
13
|
+
green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[36m',
|
|
14
|
+
};
|
|
15
|
+
const c = (text, color) => `${COLORS[color] || ''}${text}${COLORS.reset}`;
|
|
16
|
+
|
|
17
|
+
const FILE_WATCH_PATHS = [
|
|
18
|
+
'CLAUDE.md',
|
|
19
|
+
'.gitignore',
|
|
20
|
+
'package.json',
|
|
21
|
+
'tsconfig.json',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const DIRECTORY_WATCH_PATHS = [
|
|
25
|
+
'.claude',
|
|
26
|
+
'.github',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
function supportsNativeRecursiveWatch(platform = process.platform) {
|
|
30
|
+
return platform === 'win32' || platform === 'darwin';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function statIfExists(fullPath) {
|
|
34
|
+
try {
|
|
35
|
+
return fs.statSync(fullPath);
|
|
36
|
+
} catch (e) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function listRecursiveDirectories(dir) {
|
|
42
|
+
const directories = [dir];
|
|
43
|
+
let entries = [];
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
47
|
+
} catch (e) {
|
|
48
|
+
return directories;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
if (entry.isDirectory()) {
|
|
53
|
+
directories.push(...listRecursiveDirectories(path.join(dir, entry.name)));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return directories;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function buildWatchPlan(rootDir, platform = process.platform) {
|
|
61
|
+
const plan = [];
|
|
62
|
+
const seen = new Set();
|
|
63
|
+
const recursiveSupported = supportsNativeRecursiveWatch(platform);
|
|
64
|
+
|
|
65
|
+
const addTarget = (fullPath, recursive, source) => {
|
|
66
|
+
const resolved = path.resolve(fullPath);
|
|
67
|
+
const key = `${resolved}|${recursive}`;
|
|
68
|
+
if (seen.has(key)) return;
|
|
69
|
+
seen.add(key);
|
|
70
|
+
plan.push({ path: resolved, recursive, source });
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
addTarget(rootDir, false, 'repo-root');
|
|
74
|
+
|
|
75
|
+
for (const watchPath of FILE_WATCH_PATHS) {
|
|
76
|
+
const fullPath = path.join(rootDir, watchPath);
|
|
77
|
+
const stat = statIfExists(fullPath);
|
|
78
|
+
if (stat && stat.isFile()) {
|
|
79
|
+
addTarget(fullPath, false, watchPath);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for (const watchPath of DIRECTORY_WATCH_PATHS) {
|
|
84
|
+
const fullPath = path.join(rootDir, watchPath);
|
|
85
|
+
const stat = statIfExists(fullPath);
|
|
86
|
+
if (!stat || !stat.isDirectory()) continue;
|
|
87
|
+
|
|
88
|
+
if (recursiveSupported) {
|
|
89
|
+
addTarget(fullPath, true, watchPath);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const dir of listRecursiveDirectories(fullPath)) {
|
|
94
|
+
addTarget(dir, false, watchPath);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return plan;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function registerWatchers(rootDir, watchers, onChange, platform = process.platform) {
|
|
102
|
+
const plan = buildWatchPlan(rootDir, platform);
|
|
103
|
+
|
|
104
|
+
for (const item of plan) {
|
|
105
|
+
const key = `${item.path}|${item.recursive}`;
|
|
106
|
+
if (watchers.has(key)) continue;
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const watcher = fs.watch(item.path, { recursive: item.recursive }, (eventType, filename) => {
|
|
110
|
+
onChange(item, eventType, filename);
|
|
111
|
+
});
|
|
112
|
+
watchers.set(key, watcher);
|
|
113
|
+
} catch (e) {
|
|
114
|
+
// Ignore unsupported or transient watch registration failures.
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return watchers.size;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function closeWatchers(watchers) {
|
|
122
|
+
for (const watcher of watchers.values()) {
|
|
123
|
+
try {
|
|
124
|
+
watcher.close();
|
|
125
|
+
} catch (e) {
|
|
126
|
+
// Ignore close errors during shutdown.
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
watchers.clear();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function watch(options) {
|
|
133
|
+
const recursiveSupported = supportsNativeRecursiveWatch();
|
|
134
|
+
|
|
135
|
+
console.log('');
|
|
136
|
+
console.log(c(' nerviq watch mode', 'bold'));
|
|
137
|
+
console.log(c(' ═══════════════════════════════════════', 'dim'));
|
|
138
|
+
console.log(c(` Watching: ${options.dir}`, 'dim'));
|
|
139
|
+
console.log(c(` Mode: ${recursiveSupported ? 'native recursive directories' : 'expanded directory fallback (cross-platform safe)'}`, 'dim'));
|
|
140
|
+
console.log(c(' Press Ctrl+C to stop', 'dim'));
|
|
141
|
+
console.log('');
|
|
142
|
+
|
|
143
|
+
// Initial audit
|
|
144
|
+
let lastScore = null;
|
|
145
|
+
try {
|
|
146
|
+
const result = await audit({ ...options, silent: true });
|
|
147
|
+
lastScore = result.score;
|
|
148
|
+
console.log(` ${c('Initial score:', 'bold')} ${scoreColor(result.score)}`);
|
|
149
|
+
console.log(` ${result.passed} / ${result.passed + result.failed} checks passing`);
|
|
150
|
+
console.log('');
|
|
151
|
+
} catch (e) {
|
|
152
|
+
console.log(c(` Initial audit failed: ${e.message}`, 'dim'));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Watch relevant paths
|
|
156
|
+
const watchers = new Map();
|
|
157
|
+
let debounceTimer = null;
|
|
158
|
+
let shuttingDown = false;
|
|
159
|
+
|
|
160
|
+
const cleanupAndExit = () => {
|
|
161
|
+
if (shuttingDown) return;
|
|
162
|
+
shuttingDown = true;
|
|
163
|
+
clearTimeout(debounceTimer);
|
|
164
|
+
closeWatchers(watchers);
|
|
165
|
+
console.log('');
|
|
166
|
+
console.log(c(' Watch mode stopped.', 'dim'));
|
|
167
|
+
process.exit(0);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const handleChange = (item, eventType, filename) => {
|
|
171
|
+
clearTimeout(debounceTimer);
|
|
172
|
+
debounceTimer = setTimeout(async () => {
|
|
173
|
+
const changedLabel = filename
|
|
174
|
+
? String(filename)
|
|
175
|
+
: path.relative(options.dir, item.path) || path.basename(item.path);
|
|
176
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
177
|
+
|
|
178
|
+
// Pick up newly created directories or newly materialized watch paths.
|
|
179
|
+
registerWatchers(options.dir, watchers, handleChange);
|
|
180
|
+
|
|
181
|
+
console.log(c(` [${timestamp}] Change detected: ${changedLabel}`, 'dim'));
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const result = await audit({ ...options, silent: true });
|
|
185
|
+
const delta = lastScore !== null ? result.score - lastScore : 0;
|
|
186
|
+
const arrow = delta > 0 ? c(`+${delta}`, 'green') : delta < 0 ? c(String(delta), 'yellow') : '';
|
|
187
|
+
|
|
188
|
+
console.log(` Score: ${scoreColor(result.score)} ${arrow} (${result.passed}/${result.passed + result.failed} passing)`);
|
|
189
|
+
|
|
190
|
+
if (lastScore !== null && result.score > lastScore) {
|
|
191
|
+
console.log(c(' Nice improvement!', 'green'));
|
|
192
|
+
} else if (lastScore !== null && result.score < lastScore) {
|
|
193
|
+
console.log(c(' Score dropped - check what changed.', 'yellow'));
|
|
194
|
+
}
|
|
195
|
+
lastScore = result.score;
|
|
196
|
+
console.log('');
|
|
197
|
+
} catch (e) {
|
|
198
|
+
// Ignore transient errors during file saves.
|
|
199
|
+
}
|
|
200
|
+
}, 500);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
registerWatchers(options.dir, watchers, handleChange);
|
|
204
|
+
|
|
205
|
+
if (watchers.size === 0) {
|
|
206
|
+
console.log(c(' Could not register any filesystem watchers in this environment.', 'yellow'));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
process.once('SIGINT', cleanupAndExit);
|
|
211
|
+
process.once('SIGTERM', cleanupAndExit);
|
|
212
|
+
|
|
213
|
+
console.log(c(` Watching ${watchers.size} targets for changes...`, 'dim'));
|
|
214
|
+
console.log('');
|
|
215
|
+
|
|
216
|
+
// Keep alive
|
|
217
|
+
await new Promise(() => {});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function scoreColor(score) {
|
|
221
|
+
const color = score >= 70 ? 'green' : score >= 40 ? 'yellow' : 'dim';
|
|
222
|
+
return c(`${score}/100`, color);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
module.exports = {
|
|
226
|
+
watch,
|
|
227
|
+
buildWatchPlan,
|
|
228
|
+
supportsNativeRecursiveWatch,
|
|
229
|
+
};
|
|
@@ -0,0 +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
|
+
};
|