@nerviq/cli 0.0.1 → 0.9.0-beta.2

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.
Files changed (148) hide show
  1. package/CHANGELOG.md +181 -0
  2. package/LICENSE +21 -0
  3. package/README.md +447 -0
  4. package/bin/cli.js +749 -0
  5. package/content/case-study-template.md +91 -0
  6. package/content/claims-governance.md +37 -0
  7. package/content/claude-code/audit-repo/SKILL.md +20 -0
  8. package/content/claude-native-integration.md +60 -0
  9. package/content/devto-article.json +9 -0
  10. package/content/launch-posts.md +226 -0
  11. package/content/pilot-rollout-kit.md +30 -0
  12. package/content/release-checklist.md +31 -0
  13. package/package.json +53 -4
  14. package/src/activity.js +529 -0
  15. package/src/aider/activity.js +226 -0
  16. package/src/aider/config-parser.js +166 -0
  17. package/src/aider/context.js +158 -0
  18. package/src/aider/deep-review.js +316 -0
  19. package/src/aider/domain-packs.js +278 -0
  20. package/src/aider/freshness.js +168 -0
  21. package/src/aider/governance.js +253 -0
  22. package/src/aider/interactive.js +334 -0
  23. package/src/aider/mcp-packs.js +98 -0
  24. package/src/aider/patch.js +214 -0
  25. package/src/aider/plans.js +186 -0
  26. package/src/aider/premium.js +360 -0
  27. package/src/aider/setup.js +404 -0
  28. package/src/aider/techniques.js +1323 -0
  29. package/src/analyze.js +821 -0
  30. package/src/audit.js +1003 -0
  31. package/src/badge.js +13 -0
  32. package/src/benchmark.js +339 -0
  33. package/src/claudex-sync.json +7 -0
  34. package/src/codex/activity.js +324 -0
  35. package/src/codex/config-parser.js +183 -0
  36. package/src/codex/context.js +221 -0
  37. package/src/codex/deep-review.js +493 -0
  38. package/src/codex/domain-packs.js +372 -0
  39. package/src/codex/freshness.js +167 -0
  40. package/src/codex/governance.js +192 -0
  41. package/src/codex/interactive.js +618 -0
  42. package/src/codex/mcp-packs.js +660 -0
  43. package/src/codex/patch.js +209 -0
  44. package/src/codex/plans.js +251 -0
  45. package/src/codex/premium.js +614 -0
  46. package/src/codex/setup.js +603 -0
  47. package/src/codex/techniques.js +2649 -0
  48. package/src/context.js +272 -0
  49. package/src/copilot/activity.js +309 -0
  50. package/src/copilot/config-parser.js +226 -0
  51. package/src/copilot/context.js +197 -0
  52. package/src/copilot/deep-review.js +346 -0
  53. package/src/copilot/domain-packs.js +350 -0
  54. package/src/copilot/freshness.js +197 -0
  55. package/src/copilot/governance.js +222 -0
  56. package/src/copilot/interactive.js +406 -0
  57. package/src/copilot/mcp-packs.js +572 -0
  58. package/src/copilot/patch.js +238 -0
  59. package/src/copilot/plans.js +253 -0
  60. package/src/copilot/premium.js +450 -0
  61. package/src/copilot/setup.js +488 -0
  62. package/src/copilot/techniques.js +1822 -0
  63. package/src/cursor/activity.js +301 -0
  64. package/src/cursor/config-parser.js +265 -0
  65. package/src/cursor/context.js +236 -0
  66. package/src/cursor/deep-review.js +334 -0
  67. package/src/cursor/domain-packs.js +346 -0
  68. package/src/cursor/freshness.js +214 -0
  69. package/src/cursor/governance.js +229 -0
  70. package/src/cursor/interactive.js +391 -0
  71. package/src/cursor/mcp-packs.js +571 -0
  72. package/src/cursor/patch.js +243 -0
  73. package/src/cursor/plans.js +254 -0
  74. package/src/cursor/premium.js +468 -0
  75. package/src/cursor/setup.js +488 -0
  76. package/src/cursor/techniques.js +1786 -0
  77. package/src/deep-review.js +345 -0
  78. package/src/domain-packs.js +364 -0
  79. package/src/formatters/sarif.js +115 -0
  80. package/src/gemini/activity.js +402 -0
  81. package/src/gemini/config-parser.js +275 -0
  82. package/src/gemini/context.js +221 -0
  83. package/src/gemini/deep-review.js +559 -0
  84. package/src/gemini/domain-packs.js +371 -0
  85. package/src/gemini/freshness.js +204 -0
  86. package/src/gemini/governance.js +201 -0
  87. package/src/gemini/interactive.js +860 -0
  88. package/src/gemini/mcp-packs.js +658 -0
  89. package/src/gemini/patch.js +229 -0
  90. package/src/gemini/plans.js +269 -0
  91. package/src/gemini/premium.js +759 -0
  92. package/src/gemini/setup.js +692 -0
  93. package/src/gemini/techniques.js +2084 -0
  94. package/src/governance.js +523 -0
  95. package/src/harmony/advisor.js +383 -0
  96. package/src/harmony/audit.js +303 -0
  97. package/src/harmony/canon.js +444 -0
  98. package/src/harmony/cli.js +331 -0
  99. package/src/harmony/drift.js +401 -0
  100. package/src/harmony/governance.js +313 -0
  101. package/src/harmony/memory.js +238 -0
  102. package/src/harmony/sync.js +458 -0
  103. package/src/harmony/watch.js +336 -0
  104. package/src/index.js +256 -0
  105. package/src/insights.js +119 -0
  106. package/src/interactive.js +118 -0
  107. package/src/mcp-packs.js +597 -0
  108. package/src/opencode/activity.js +286 -0
  109. package/src/opencode/config-parser.js +109 -0
  110. package/src/opencode/context.js +247 -0
  111. package/src/opencode/deep-review.js +313 -0
  112. package/src/opencode/domain-packs.js +240 -0
  113. package/src/opencode/freshness.js +158 -0
  114. package/src/opencode/governance.js +159 -0
  115. package/src/opencode/interactive.js +392 -0
  116. package/src/opencode/mcp-packs.js +474 -0
  117. package/src/opencode/patch.js +184 -0
  118. package/src/opencode/plans.js +231 -0
  119. package/src/opencode/premium.js +413 -0
  120. package/src/opencode/setup.js +449 -0
  121. package/src/opencode/techniques.js +1713 -0
  122. package/src/plans.js +655 -0
  123. package/src/secret-patterns.js +30 -0
  124. package/src/setup.js +1274 -0
  125. package/src/synergy/adaptive.js +261 -0
  126. package/src/synergy/compensation.js +156 -0
  127. package/src/synergy/evidence.js +193 -0
  128. package/src/synergy/learning.js +184 -0
  129. package/src/synergy/patterns.js +227 -0
  130. package/src/synergy/ranking.js +83 -0
  131. package/src/synergy/report.js +163 -0
  132. package/src/synergy/routing.js +152 -0
  133. package/src/techniques.js +1354 -0
  134. package/src/watch.js +229 -0
  135. package/src/windsurf/activity.js +302 -0
  136. package/src/windsurf/config-parser.js +267 -0
  137. package/src/windsurf/context.js +249 -0
  138. package/src/windsurf/deep-review.js +337 -0
  139. package/src/windsurf/domain-packs.js +348 -0
  140. package/src/windsurf/freshness.js +215 -0
  141. package/src/windsurf/governance.js +231 -0
  142. package/src/windsurf/interactive.js +388 -0
  143. package/src/windsurf/mcp-packs.js +535 -0
  144. package/src/windsurf/patch.js +231 -0
  145. package/src/windsurf/plans.js +247 -0
  146. package/src/windsurf/premium.js +467 -0
  147. package/src/windsurf/setup.js +471 -0
  148. package/src/windsurf/techniques.js +1758 -0
package/src/context.js ADDED
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Project context scanner - reads project files to evaluate against techniques.
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+
8
+ /**
9
+ * Scans and caches project files to provide fast lookups for technique checks.
10
+ * Reads the project directory on construction and exposes helpers for file content, JSON, and stack detection.
11
+ */
12
+ class ProjectContext {
13
+ constructor(dir) {
14
+ this.dir = dir;
15
+ this.files = [];
16
+ this._cache = {};
17
+ this._dependencyCache = null;
18
+ this._scan();
19
+ }
20
+
21
+ _scan() {
22
+ try {
23
+ const entries = fs.readdirSync(this.dir, { withFileTypes: true });
24
+ for (const entry of entries) {
25
+ if (entry.isFile()) {
26
+ if (entry.name === '.DS_Store') continue;
27
+ this.files.push(entry.name);
28
+ } else if (entry.isDirectory()) {
29
+ if (entry.name.startsWith('.') && entry.name !== '.claude') continue;
30
+ if (entry.name === 'node_modules' || entry.name === '__pycache__') continue;
31
+ this.files.push(entry.name + '/');
32
+ // Scan .claude/ subdirectories
33
+ if (entry.name === '.claude') {
34
+ this._scanSubdir('.claude');
35
+ }
36
+ }
37
+ }
38
+ } catch (err) {
39
+ // Directory might not be readable
40
+ }
41
+ }
42
+
43
+ _scanSubdir(subdir) {
44
+ try {
45
+ const fullPath = path.join(this.dir, subdir);
46
+ const entries = fs.readdirSync(fullPath, { withFileTypes: true });
47
+ for (const entry of entries) {
48
+ if (entry.isDirectory()) {
49
+ this._scanSubdir(path.join(subdir, entry.name));
50
+ }
51
+ }
52
+ } catch (err) {
53
+ // Subdirectory might not exist
54
+ }
55
+ }
56
+
57
+ hasDir(dirPath) {
58
+ const fullPath = path.join(this.dir, dirPath);
59
+ try {
60
+ return fs.statSync(fullPath).isDirectory();
61
+ } catch {
62
+ return false;
63
+ }
64
+ }
65
+
66
+ dirFiles(dirPath) {
67
+ const fullPath = path.join(this.dir, dirPath);
68
+ try {
69
+ return fs.readdirSync(fullPath).filter(f => !f.startsWith('.'));
70
+ } catch {
71
+ return [];
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Return the contents of the project's CLAUDE.md (root or .claude/ location).
77
+ * @returns {string|null} File content or null if not found.
78
+ */
79
+ claudeMdContent() {
80
+ return this.fileContent('CLAUDE.md') || this.fileContent('.claude/CLAUDE.md');
81
+ }
82
+
83
+ /**
84
+ * Read and cache the content of a file relative to the project root.
85
+ * @param {string} filePath - Relative path from the project root.
86
+ * @returns {string|null} File content or null if not readable.
87
+ */
88
+ fileContent(filePath) {
89
+ if (this._cache[filePath] !== undefined) return this._cache[filePath];
90
+ const fullPath = path.join(this.dir, filePath);
91
+ try {
92
+ const content = fs.readFileSync(fullPath, 'utf8');
93
+ this._cache[filePath] = content;
94
+ return content;
95
+ } catch {
96
+ this._cache[filePath] = null;
97
+ return null;
98
+ }
99
+ }
100
+
101
+ fileSizeBytes(filePath) {
102
+ const fullPath = path.join(this.dir, filePath);
103
+ try {
104
+ return fs.statSync(fullPath).size;
105
+ } catch {
106
+ return null;
107
+ }
108
+ }
109
+
110
+ lineNumber(filePath, matcher) {
111
+ const content = this.fileContent(filePath);
112
+ if (!content) return null;
113
+
114
+ const lines = content.split(/\r?\n/);
115
+ for (let index = 0; index < lines.length; index++) {
116
+ const line = lines[index];
117
+ if (typeof matcher === 'string' && line.includes(matcher)) {
118
+ return index + 1;
119
+ }
120
+ if (matcher instanceof RegExp && matcher.test(line)) {
121
+ matcher.lastIndex = 0;
122
+ return index + 1;
123
+ }
124
+ if (typeof matcher === 'function' && matcher(line, index + 1)) {
125
+ return index + 1;
126
+ }
127
+ }
128
+
129
+ return null;
130
+ }
131
+
132
+ jsonFile(filePath) {
133
+ const content = this.fileContent(filePath);
134
+ if (!content) return null;
135
+ try {
136
+ return JSON.parse(content);
137
+ } catch {
138
+ return null;
139
+ }
140
+ }
141
+
142
+ projectDependencies() {
143
+ if (this._dependencyCache) return this._dependencyCache;
144
+
145
+ const deps = {};
146
+ const addDependency = (name, source) => {
147
+ if (!name) return;
148
+ const normalized = `${name}`.trim().toLowerCase().replace(/\[.*\]$/, '');
149
+ if (!normalized || normalized === 'python') return;
150
+ if (!deps[normalized]) {
151
+ deps[normalized] = source || true;
152
+ }
153
+ };
154
+
155
+ const pkg = this.jsonFile('package.json') || {};
156
+ for (const source of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) {
157
+ for (const name of Object.keys(pkg[source] || {})) {
158
+ addDependency(name, 'package.json');
159
+ }
160
+ }
161
+
162
+ const pyproject = this.fileContent('pyproject.toml') || '';
163
+ for (const name of extractPyprojectDependencies(pyproject)) {
164
+ addDependency(name, 'pyproject.toml');
165
+ }
166
+
167
+ const requirementFiles = [
168
+ 'requirements.txt',
169
+ 'requirements-dev.txt',
170
+ 'requirements-dev.in',
171
+ 'requirements-prod.txt',
172
+ 'requirements/base.txt',
173
+ 'requirements/dev.txt',
174
+ 'requirements/test.txt',
175
+ ];
176
+ for (const filePath of requirementFiles) {
177
+ const content = this.fileContent(filePath);
178
+ if (!content) continue;
179
+ for (const name of extractRequirementsDependencies(content)) {
180
+ addDependency(name, filePath);
181
+ }
182
+ }
183
+
184
+ this._dependencyCache = deps;
185
+ return deps;
186
+ }
187
+
188
+ detectStacks(STACKS) {
189
+ const detected = [];
190
+ for (const [key, stack] of Object.entries(STACKS)) {
191
+ const hasFile = stack.files.some(f => {
192
+ return this.files.some(pf => pf.startsWith(f));
193
+ });
194
+ if (!hasFile) continue;
195
+
196
+ let contentMatch = true;
197
+ for (const [file, needle] of Object.entries(stack.content)) {
198
+ const content = this.fileContent(file);
199
+ if (!content || !content.includes(needle)) {
200
+ contentMatch = false;
201
+ break;
202
+ }
203
+ }
204
+
205
+ if (hasFile && contentMatch) {
206
+ detected.push({ key, label: stack.label });
207
+ }
208
+ }
209
+ return detected;
210
+ }
211
+ }
212
+
213
+ function extractPyprojectDependencies(content) {
214
+ if (!content) return [];
215
+
216
+ const deps = new Set();
217
+ const add = (value) => {
218
+ if (!value) return;
219
+ deps.add(value.trim().toLowerCase().replace(/\[.*\]$/, ''));
220
+ };
221
+
222
+ const extractSection = (sectionName) => {
223
+ const escaped = sectionName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
224
+ const pattern = new RegExp(`\\[${escaped}\\]([\\s\\S]*?)(?:\\n\\s*\\[|$)`);
225
+ const match = content.match(pattern);
226
+ return match ? match[1] : '';
227
+ };
228
+
229
+ const poetryDeps = extractSection('tool.poetry.dependencies');
230
+ for (const match of poetryDeps.matchAll(/^\s*([A-Za-z0-9_.-]+)\s*=/gm)) {
231
+ add(match[1]);
232
+ }
233
+
234
+ const projectDeps = extractSection('project');
235
+ const projectDepsArrayMatch = projectDeps.match(/dependencies\s*=\s*\[([\s\S]*?)\]/m);
236
+ if (projectDepsArrayMatch) {
237
+ for (const item of projectDepsArrayMatch[1].matchAll(/["']([^"']+)["']/g)) {
238
+ const name = item[1].split(/[<>=!~ ]/)[0];
239
+ add(name);
240
+ }
241
+ }
242
+
243
+ const optionalDepsSection = extractSection('project.optional-dependencies');
244
+ for (const item of optionalDepsSection.matchAll(/["']([^"']+)["']/g)) {
245
+ const name = item[1].split(/[<>=!~ ]/)[0];
246
+ add(name);
247
+ }
248
+
249
+ const dependencyGroupsSection = extractSection('dependency-groups');
250
+ for (const item of dependencyGroupsSection.matchAll(/["']([^"']+)["']/g)) {
251
+ const name = item[1].split(/[<>=!~ ]/)[0];
252
+ add(name);
253
+ }
254
+
255
+ return [...deps].filter(Boolean);
256
+ }
257
+
258
+ function extractRequirementsDependencies(content) {
259
+ if (!content) return [];
260
+
261
+ const deps = new Set();
262
+ for (const rawLine of content.split(/\r?\n/)) {
263
+ const line = rawLine.replace(/#.*$/, '').trim();
264
+ if (!line || line.startsWith('-')) continue;
265
+ const match = line.match(/^([A-Za-z0-9_.-]+)/);
266
+ if (!match) continue;
267
+ deps.add(match[1].toLowerCase().replace(/\[.*\]$/, ''));
268
+ }
269
+ return [...deps];
270
+ }
271
+
272
+ module.exports = { ProjectContext };
@@ -0,0 +1,309 @@
1
+ /**
2
+ * Copilot Repeat-Usage Surfaces
3
+ *
4
+ * Adapts the shared activity/snapshot backend for Copilot platform.
5
+ * Provides: history, compare, trend, feedback, insights.
6
+ *
7
+ * 6 repeat-usage surfaces filtered by platform='copilot':
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 (VS Code/cloud/CLI) 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 getCopilotHistory(dir, limit = 20) {
28
+ const entries = readSnapshotIndex(dir);
29
+ return entries
30
+ .filter(e => e.snapshotKind === 'audit' && (e.platform === 'copilot' || e.summary?.platform === 'copilot'))
31
+ .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
32
+ .slice(0, limit);
33
+ }
34
+
35
+ function formatCopilotHistory(dir) {
36
+ const history = getCopilotHistory(dir, 10);
37
+ if (history.length === 0) {
38
+ return 'No Copilot snapshots found. Run `npx nerviq --platform copilot --snapshot` to save one.';
39
+ }
40
+
41
+ const lines = ['Copilot 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.vscode ? 'VS Code' : null,
50
+ surfaces.cloudAgent ? 'Cloud' : null,
51
+ surfaces.cli ? 'CLI' : null,
52
+ ].filter(Boolean).join('+') || 'unknown';
53
+ lines.push(` ${date} ${score}/100 (${passed}/${total} passing) [${surfaceStr}]`);
54
+ }
55
+
56
+ const comparison = compareCopilotLatest(dir);
57
+ if (comparison) {
58
+ lines.push('');
59
+ const sign = comparison.delta.score >= 0 ? '+' : '';
60
+ lines.push(` Trend: ${comparison.trend} (${sign}${comparison.delta.score} since previous)`);
61
+ if (comparison.improvements.length > 0) lines.push(` Fixed: ${comparison.improvements.join(', ')}`);
62
+ if (comparison.regressions.length > 0) lines.push(` New gaps: ${comparison.regressions.join(', ')}`);
63
+ }
64
+
65
+ return lines.join('\n');
66
+ }
67
+
68
+ // --- Compare ---
69
+
70
+ function compareCopilotLatest(dir) {
71
+ const audits = getCopilotHistory(dir, 2);
72
+ if (audits.length < 2) return null;
73
+
74
+ const current = audits[0];
75
+ const previous = audits[1];
76
+
77
+ const delta = {
78
+ score: (current.summary?.score || 0) - (previous.summary?.score || 0),
79
+ organic: (current.summary?.organicScore || 0) - (previous.summary?.organicScore || 0),
80
+ passed: (current.summary?.passed || 0) - (previous.summary?.passed || 0),
81
+ };
82
+
83
+ const regressions = [];
84
+ const improvements = [];
85
+ const prevKeys = new Set(previous.summary?.topActionKeys || []);
86
+ const currKeys = new Set(current.summary?.topActionKeys || []);
87
+
88
+ for (const key of currKeys) { if (!prevKeys.has(key)) regressions.push(key); }
89
+ for (const key of prevKeys) { if (!currKeys.has(key)) improvements.push(key); }
90
+
91
+ return {
92
+ platform: 'copilot',
93
+ current: { date: current.createdAt, score: current.summary?.score, passed: current.summary?.passed },
94
+ previous: { date: previous.createdAt, score: previous.summary?.score, passed: previous.summary?.passed },
95
+ delta,
96
+ regressions,
97
+ improvements,
98
+ trend: delta.score > 0 ? 'improving' : delta.score < 0 ? 'regressing' : 'stable',
99
+ };
100
+ }
101
+
102
+ // --- Trend ---
103
+
104
+ function exportCopilotTrendReport(dir) {
105
+ const history = getCopilotHistory(dir, 50);
106
+ if (history.length === 0) return null;
107
+
108
+ const comparison = compareCopilotLatest(dir);
109
+ const lines = [
110
+ '# GitHub Copilot Setup Trend Report',
111
+ '',
112
+ `**Project:** ${path.basename(dir)}`,
113
+ `**Platform:** GitHub Copilot`,
114
+ `**Generated:** ${new Date().toISOString().split('T')[0]}`,
115
+ `**Snapshots:** ${history.length}`,
116
+ '',
117
+ '## Score History',
118
+ '',
119
+ '| Date | Score | Passed | Checks | Surfaces |',
120
+ '|------|-------|--------|--------|----------|',
121
+ ];
122
+
123
+ for (const entry of history) {
124
+ const date = entry.createdAt?.split('T')[0] || '?';
125
+ const surfaces = entry.summary?.surfaces || {};
126
+ const surfaceStr = [surfaces.vscode ? 'VS' : null, surfaces.cloudAgent ? 'Cloud' : null, surfaces.cli ? 'CLI' : null].filter(Boolean).join('+') || '?';
127
+ lines.push(`| ${date} | ${entry.summary?.score ?? '?'}/100 | ${entry.summary?.passed ?? '?'} | ${entry.summary?.checkCount ?? '?'} | ${surfaceStr} |`);
128
+ }
129
+
130
+ if (comparison) {
131
+ lines.push('', '## Latest Comparison', '');
132
+ lines.push(`- **Previous:** ${comparison.previous.score}/100 (${comparison.previous.date?.split('T')[0]})`);
133
+ lines.push(`- **Current:** ${comparison.current.score}/100 (${comparison.current.date?.split('T')[0]})`);
134
+ lines.push(`- **Delta:** ${comparison.delta.score >= 0 ? '+' : ''}${comparison.delta.score} points`);
135
+ lines.push(`- **Trend:** ${comparison.trend}`);
136
+ if (comparison.improvements.length > 0) lines.push(`- **Fixed:** ${comparison.improvements.join(', ')}`);
137
+ if (comparison.regressions.length > 0) lines.push(`- **New gaps:** ${comparison.regressions.join(', ')}`);
138
+ }
139
+
140
+ if (history.length >= 3) {
141
+ lines.push('', '## Trend Chart', '', '```');
142
+ const scores = history.slice().reverse().map(e => e.summary?.score ?? 0);
143
+ const max = Math.max(...scores, 100);
144
+ const chartHeight = 10;
145
+ for (let row = chartHeight; row >= 0; row--) {
146
+ const threshold = (row / chartHeight) * max;
147
+ const rowLabel = String(Math.round(threshold)).padStart(3);
148
+ const bar = scores.map(s => s >= threshold ? '#' : ' ').join('');
149
+ lines.push(`${rowLabel} |${bar}`);
150
+ }
151
+ lines.push(` +${'─'.repeat(scores.length)}`);
152
+ lines.push('```');
153
+ }
154
+
155
+ lines.push('', '---', `*Generated by nerviq v${version} for GitHub Copilot*`);
156
+ return lines.join('\n');
157
+ }
158
+
159
+ // --- Feedback ---
160
+
161
+ function recordCopilotFeedback(dir, payload) {
162
+ return recordRecommendationOutcome(dir, {
163
+ ...payload,
164
+ source: payload.source || 'copilot',
165
+ platform: 'copilot',
166
+ });
167
+ }
168
+
169
+ function getCopilotFeedbackSummary(dir) {
170
+ const entries = readOutcomeIndex(dir)
171
+ .filter(e => e.source === 'copilot' || e.platform === 'copilot');
172
+ return summarizeOutcomeEntries(entries);
173
+ }
174
+
175
+ function formatCopilotFeedback(dir) {
176
+ const summary = getCopilotFeedbackSummary(dir);
177
+ if (!summary || Object.keys(summary).length === 0) {
178
+ return 'No Copilot feedback recorded yet. Use `npx nerviq --platform copilot feedback` to rate recommendations.';
179
+ }
180
+ const lines = ['Copilot Recommendation Feedback:', ''];
181
+ const entries = Array.isArray(summary) ? summary : Object.values(summary);
182
+ for (const entry of entries) {
183
+ lines.push(` ${entry.key || 'unknown'}: ${entry.accepted || 0} accepted, ${entry.rejected || 0} rejected (${entry.total || 0} total)`);
184
+ }
185
+ return lines.join('\n');
186
+ }
187
+
188
+ // --- Insights ---
189
+
190
+ function generateCopilotInsights(dir) {
191
+ const history = getCopilotHistory(dir, 50);
192
+ const feedback = getCopilotFeedbackSummary(dir);
193
+ const insights = [];
194
+
195
+ // Pattern 1: Persistent failures
196
+ if (history.length >= 3) {
197
+ const recentFailKeys = new Map();
198
+ for (const entry of history.slice(0, 5)) {
199
+ for (const key of (entry.summary?.topActionKeys || [])) {
200
+ recentFailKeys.set(key, (recentFailKeys.get(key) || 0) + 1);
201
+ }
202
+ }
203
+ for (const [key, count] of recentFailKeys) {
204
+ if (count >= 3) {
205
+ insights.push({ type: 'persistent-failure', severity: 'high', key, message: `Check ${key} has failed in ${count} of the last ${Math.min(history.length, 5)} audits.` });
206
+ }
207
+ }
208
+ }
209
+
210
+ // Pattern 2: Score regression
211
+ if (history.length >= 2) {
212
+ const scores = history.map(e => e.summary?.score ?? 0);
213
+ if (scores[0] < scores[1]) {
214
+ 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] });
215
+ }
216
+ }
217
+
218
+ // Pattern 3: Velocity stall
219
+ if (history.length >= 5) {
220
+ const recentScores = history.slice(0, 5).map(e => e.summary?.score ?? 0);
221
+ const range = Math.max(...recentScores) - Math.min(...recentScores);
222
+ if (range <= 2) {
223
+ insights.push({ type: 'velocity-stall', severity: 'low', message: `Score flat (range: ${range}) over last 5 audits.` });
224
+ }
225
+ }
226
+
227
+ // Copilot-specific Pattern 4: Surface coverage stagnation
228
+ if (history.length >= 3) {
229
+ const surfaceKeys = [];
230
+ for (const entry of history.slice(0, 5)) {
231
+ for (const key of (entry.summary?.topActionKeys || [])) {
232
+ if (key.includes('cloud') || key.includes('surface') || key.includes('CLI')) {
233
+ surfaceKeys.push(key);
234
+ }
235
+ }
236
+ }
237
+ if (surfaceKeys.length >= 2) {
238
+ insights.push({ type: 'surface-coverage-gap', severity: 'medium', message: `Surface coverage checks have appeared in ${surfaceKeys.length} recent audits. Consider configuring additional Copilot surfaces.`, keys: [...new Set(surfaceKeys)] });
239
+ }
240
+ }
241
+
242
+ // Copilot-specific Pattern 5: Content exclusion gap persistence
243
+ if (history.length >= 3) {
244
+ const exclusionKeys = new Map();
245
+ for (const entry of history.slice(0, 5)) {
246
+ for (const key of (entry.summary?.topActionKeys || [])) {
247
+ if (key.includes('exclusion') || key.includes('content')) {
248
+ exclusionKeys.set(key, (exclusionKeys.get(key) || 0) + 1);
249
+ }
250
+ }
251
+ }
252
+ for (const [key, count] of exclusionKeys) {
253
+ if (count >= 3) {
254
+ insights.push({ type: 'content-exclusion-gap', severity: 'high', key, message: `Content exclusion check ${key} has persisted across ${count} audits. The cloud agent content exclusion gap may need team-level documentation.` });
255
+ }
256
+ }
257
+ }
258
+
259
+ // Copilot-specific Pattern 6: Windows sandbox awareness
260
+ if (history.length >= 2) {
261
+ const latest = history[0];
262
+ const windowsKeys = (latest.summary?.topActionKeys || []).filter(k => k.includes('windows') || k.includes('sandbox'));
263
+ if (windowsKeys.length >= 1) {
264
+ insights.push({ type: 'windows-sandbox-gap', severity: 'medium', message: `Windows sandbox-related checks failing. Terminal sandbox is unavailable on native Windows.`, keys: windowsKeys });
265
+ }
266
+ }
267
+
268
+ // Feedback signals
269
+ const feedbackEntries = Array.isArray(feedback) ? feedback : Object.values(feedback || {});
270
+ for (const entry of feedbackEntries) {
271
+ if (entry.rejected > entry.accepted && entry.total >= 2) {
272
+ insights.push({ type: 'feedback-signal', severity: 'medium', key: entry.key, message: `Recommendation ${entry.key} has been rejected more than accepted (${entry.rejected}/${entry.total}).` });
273
+ }
274
+ }
275
+
276
+ return {
277
+ platform: 'copilot',
278
+ generatedAt: new Date().toISOString(),
279
+ snapshotCount: history.length,
280
+ feedbackCount: feedbackEntries.length,
281
+ insights,
282
+ summary: insights.length === 0
283
+ ? 'No actionable insights detected. Keep running audits to build pattern data.'
284
+ : `${insights.length} insight(s) detected across ${history.length} snapshots.`,
285
+ };
286
+ }
287
+
288
+ function formatCopilotInsights(dir) {
289
+ const result = generateCopilotInsights(dir);
290
+ if (result.insights.length === 0) return result.summary;
291
+ const lines = ['Copilot Insights:', ''];
292
+ for (const insight of result.insights) {
293
+ lines.push(` [${insight.severity.toUpperCase()}] ${insight.message}`);
294
+ }
295
+ lines.push('', result.summary);
296
+ return lines.join('\n');
297
+ }
298
+
299
+ module.exports = {
300
+ getCopilotHistory,
301
+ formatCopilotHistory,
302
+ compareCopilotLatest,
303
+ exportCopilotTrendReport,
304
+ recordCopilotFeedback,
305
+ getCopilotFeedbackSummary,
306
+ formatCopilotFeedback,
307
+ generateCopilotInsights,
308
+ formatCopilotInsights,
309
+ };