@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.
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
@@ -0,0 +1,529 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { version } = require('../package.json');
4
+
5
+ let _lastTimestamp = '';
6
+ let _counter = 0;
7
+
8
+ function timestampId() {
9
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
10
+ if (ts === _lastTimestamp) {
11
+ _counter++;
12
+ return `${ts}-${_counter}`;
13
+ }
14
+ _lastTimestamp = ts;
15
+ _counter = 0;
16
+ return ts;
17
+ }
18
+
19
+ function ensureArtifactDirs(dir) {
20
+ const root = path.join(dir, '.claude', 'claudex-setup');
21
+ const activityDir = path.join(root, 'activity');
22
+ const rollbackDir = path.join(root, 'rollbacks');
23
+ const snapshotDir = path.join(root, 'snapshots');
24
+ const outcomesDir = path.join(root, 'outcomes');
25
+ fs.mkdirSync(activityDir, { recursive: true });
26
+ fs.mkdirSync(rollbackDir, { recursive: true });
27
+ fs.mkdirSync(snapshotDir, { recursive: true });
28
+ fs.mkdirSync(outcomesDir, { recursive: true });
29
+ return { root, activityDir, rollbackDir, snapshotDir, outcomesDir };
30
+ }
31
+
32
+ function writeJson(filePath, payload) {
33
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
34
+ fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), 'utf8');
35
+ }
36
+
37
+ function writeActivityArtifact(dir, type, payload) {
38
+ const id = timestampId();
39
+ const { activityDir } = ensureArtifactDirs(dir);
40
+ const filePath = path.join(activityDir, `${id}-${type}.json`);
41
+ writeJson(filePath, {
42
+ id,
43
+ type,
44
+ createdAt: new Date().toISOString(),
45
+ ...payload,
46
+ });
47
+ return {
48
+ id,
49
+ filePath,
50
+ relativePath: path.relative(dir, filePath),
51
+ };
52
+ }
53
+
54
+ function writeRollbackArtifact(dir, payload) {
55
+ const id = timestampId();
56
+ const { rollbackDir } = ensureArtifactDirs(dir);
57
+ const filePath = path.join(rollbackDir, `${id}.json`);
58
+ writeJson(filePath, {
59
+ id,
60
+ createdAt: new Date().toISOString(),
61
+ rollbackType: 'delete-created-files',
62
+ ...payload,
63
+ });
64
+ return {
65
+ id,
66
+ filePath,
67
+ relativePath: path.relative(dir, filePath),
68
+ };
69
+ }
70
+
71
+ function summarizeSnapshot(snapshotKind, payload) {
72
+ if (snapshotKind === 'audit') {
73
+ return {
74
+ score: payload.score,
75
+ organicScore: payload.organicScore,
76
+ passed: payload.passed,
77
+ failed: payload.failed,
78
+ checkCount: payload.checkCount,
79
+ suggestedNextCommand: payload.suggestedNextCommand,
80
+ topActionKeys: Array.isArray(payload.topNextActions)
81
+ ? payload.topNextActions.slice(0, 3).map(item => item.key)
82
+ : [],
83
+ };
84
+ }
85
+
86
+ if (snapshotKind === 'augment' || snapshotKind === 'suggest-only') {
87
+ return {
88
+ score: payload.projectSummary?.score,
89
+ organicScore: payload.projectSummary?.organicScore,
90
+ maturity: payload.projectSummary?.maturity,
91
+ domains: payload.projectSummary?.domains || [],
92
+ topActionKeys: Array.isArray(payload.topNextActions)
93
+ ? payload.topNextActions.slice(0, 3).map(item => item.key)
94
+ : [],
95
+ };
96
+ }
97
+
98
+ if (snapshotKind === 'benchmark') {
99
+ return {
100
+ beforeScore: payload.before?.score,
101
+ afterScore: payload.after?.score,
102
+ scoreDelta: payload.delta?.score,
103
+ organicDelta: payload.delta?.organicScore,
104
+ decisionGuidance: payload.executiveSummary?.decisionGuidance || null,
105
+ };
106
+ }
107
+
108
+ if (snapshotKind === 'governance') {
109
+ return {
110
+ permissionProfiles: Array.isArray(payload.permissionProfiles) ? payload.permissionProfiles.length : 0,
111
+ hooks: Array.isArray(payload.hookRegistry) ? payload.hookRegistry.length : 0,
112
+ policyPacks: Array.isArray(payload.policyPacks) ? payload.policyPacks.length : 0,
113
+ domainPacks: Array.isArray(payload.domainPacks) ? payload.domainPacks.length : 0,
114
+ mcpPacks: Array.isArray(payload.mcpPacks) ? payload.mcpPacks.length : 0,
115
+ };
116
+ }
117
+
118
+ return {};
119
+ }
120
+
121
+ function updateSnapshotIndex(snapshotDir, record) {
122
+ const indexPath = path.join(snapshotDir, 'index.json');
123
+ let entries = [];
124
+
125
+ if (fs.existsSync(indexPath)) {
126
+ try {
127
+ entries = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
128
+ if (!Array.isArray(entries)) {
129
+ entries = [];
130
+ }
131
+ } catch {
132
+ entries = [];
133
+ }
134
+ }
135
+
136
+ entries.push(record);
137
+ // Prune to keep last 200 entries
138
+ const MAX_INDEX_ENTRIES = 200;
139
+ if (entries.length > MAX_INDEX_ENTRIES) {
140
+ entries = entries.slice(entries.length - MAX_INDEX_ENTRIES);
141
+ }
142
+ fs.writeFileSync(indexPath, JSON.stringify(entries, null, 2), 'utf8');
143
+ }
144
+
145
+ /**
146
+ * Write a normalized snapshot artifact to .claude/claudex-setup/snapshots/ and update the index.
147
+ * @param {string} dir - Project root directory.
148
+ * @param {string} snapshotKind - Snapshot type ('audit', 'benchmark', 'governance', 'augment', 'suggest-only').
149
+ * @param {Object} payload - Full result payload to persist.
150
+ * @param {Object} [meta={}] - Optional metadata fields merged into the envelope.
151
+ * @returns {Object} Artifact record with id, filePath, relativePath, indexPath, and summary.
152
+ */
153
+ function writeSnapshotArtifact(dir, snapshotKind, payload, meta = {}) {
154
+ const id = timestampId();
155
+ const { snapshotDir } = ensureArtifactDirs(dir);
156
+ const filePath = path.join(snapshotDir, `${id}-${snapshotKind}.json`);
157
+ const summary = summarizeSnapshot(snapshotKind, payload);
158
+ const envelope = {
159
+ schemaVersion: 1,
160
+ artifactType: 'snapshot',
161
+ snapshotKind,
162
+ id,
163
+ createdAt: new Date().toISOString(),
164
+ generatedBy: `nerviq@${version}`,
165
+ directory: dir,
166
+ summary,
167
+ ...meta,
168
+ payload,
169
+ };
170
+
171
+ writeJson(filePath, envelope);
172
+
173
+ const record = {
174
+ id,
175
+ snapshotKind,
176
+ createdAt: envelope.createdAt,
177
+ relativePath: path.relative(dir, filePath),
178
+ summary,
179
+ };
180
+ updateSnapshotIndex(snapshotDir, record);
181
+
182
+ return {
183
+ id,
184
+ filePath,
185
+ relativePath: path.relative(dir, filePath),
186
+ indexPath: path.relative(dir, path.join(snapshotDir, 'index.json')),
187
+ summary,
188
+ };
189
+ }
190
+
191
+ function readSnapshotIndex(dir) {
192
+ const indexPath = path.join(dir, '.claude', 'claudex-setup', 'snapshots', 'index.json');
193
+ if (!fs.existsSync(indexPath)) return [];
194
+ try {
195
+ const entries = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
196
+ return Array.isArray(entries) ? entries : [];
197
+ } catch {
198
+ return [];
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Get the audit score history from saved snapshots, most recent first.
204
+ * @param {string} dir - Project root directory.
205
+ * @param {number} [limit=20] - Maximum number of entries to return.
206
+ * @returns {Object[]} Array of snapshot index entries for audit snapshots.
207
+ */
208
+ function getHistory(dir, limit = 20) {
209
+ const entries = readSnapshotIndex(dir);
210
+ return entries
211
+ .filter(e => e.snapshotKind === 'audit')
212
+ .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
213
+ .slice(0, limit);
214
+ }
215
+
216
+ /**
217
+ * Compare the two most recent audit snapshots and return the delta.
218
+ * @param {string} dir - Project root directory.
219
+ * @returns {Object|null} Comparison with current/previous scores, delta, regressions, improvements, and trend. Null if fewer than 2 snapshots.
220
+ */
221
+ function compareLatest(dir) {
222
+ const audits = getHistory(dir, 2);
223
+ if (audits.length < 2) return null;
224
+
225
+ const current = audits[0];
226
+ const previous = audits[1];
227
+
228
+ const delta = {
229
+ score: (current.summary?.score || 0) - (previous.summary?.score || 0),
230
+ organic: (current.summary?.organicScore || 0) - (previous.summary?.organicScore || 0),
231
+ passed: (current.summary?.passed || 0) - (previous.summary?.passed || 0),
232
+ };
233
+
234
+ const regressions = [];
235
+ const improvements = [];
236
+
237
+ const prevKeys = new Set(previous.summary?.topActionKeys || []);
238
+ const currKeys = new Set(current.summary?.topActionKeys || []);
239
+
240
+ for (const key of currKeys) {
241
+ if (!prevKeys.has(key)) regressions.push(key);
242
+ }
243
+ for (const key of prevKeys) {
244
+ if (!currKeys.has(key)) improvements.push(key);
245
+ }
246
+
247
+ return {
248
+ current: { date: current.createdAt, score: current.summary?.score, passed: current.summary?.passed },
249
+ previous: { date: previous.createdAt, score: previous.summary?.score, passed: previous.summary?.passed },
250
+ delta,
251
+ regressions,
252
+ improvements,
253
+ trend: delta.score > 0 ? 'improving' : delta.score < 0 ? 'regressing' : 'stable',
254
+ };
255
+ }
256
+
257
+ function formatHistory(dir) {
258
+ const history = getHistory(dir, 10);
259
+ if (history.length === 0) return 'No snapshots found. Run `npx nerviq --snapshot` to save one.';
260
+
261
+ const lines = ['Score history (most recent first):', ''];
262
+ for (const entry of history) {
263
+ const date = entry.createdAt?.split('T')[0] || 'unknown';
264
+ const score = entry.summary?.score ?? '?';
265
+ const passed = entry.summary?.passed ?? '?';
266
+ const total = entry.summary?.checkCount ?? '?';
267
+ lines.push(` ${date} ${score}/100 (${passed}/${total} passing)`);
268
+ }
269
+
270
+ const comparison = compareLatest(dir);
271
+ if (comparison) {
272
+ lines.push('');
273
+ const sign = comparison.delta.score >= 0 ? '+' : '';
274
+ lines.push(` Trend: ${comparison.trend} (${sign}${comparison.delta.score} since previous)`);
275
+ if (comparison.improvements.length > 0) {
276
+ lines.push(` Fixed: ${comparison.improvements.join(', ')}`);
277
+ }
278
+ if (comparison.regressions.length > 0) {
279
+ lines.push(` New gaps: ${comparison.regressions.join(', ')}`);
280
+ }
281
+ }
282
+
283
+ return lines.join('\n');
284
+ }
285
+
286
+ function exportTrendReport(dir) {
287
+ const history = getHistory(dir, 50);
288
+ if (history.length === 0) return null;
289
+
290
+ const comparison = compareLatest(dir);
291
+ const lines = [
292
+ '# Claude Code Setup Trend Report',
293
+ '',
294
+ `**Project:** ${path.basename(dir)}`,
295
+ `**Generated:** ${new Date().toISOString().split('T')[0]}`,
296
+ `**Snapshots:** ${history.length}`,
297
+ '',
298
+ '## Score History',
299
+ '',
300
+ '| Date | Score | Passed | Checks |',
301
+ '|------|-------|--------|--------|',
302
+ ];
303
+
304
+ for (const entry of history) {
305
+ const date = entry.createdAt?.split('T')[0] || '?';
306
+ lines.push(`| ${date} | ${entry.summary?.score ?? '?'}/100 | ${entry.summary?.passed ?? '?'} | ${entry.summary?.checkCount ?? '?'} |`);
307
+ }
308
+
309
+ if (comparison) {
310
+ lines.push('');
311
+ lines.push('## Latest Comparison');
312
+ lines.push('');
313
+ lines.push(`- **Previous:** ${comparison.previous.score}/100 (${comparison.previous.date?.split('T')[0]})`);
314
+ lines.push(`- **Current:** ${comparison.current.score}/100 (${comparison.current.date?.split('T')[0]})`);
315
+ lines.push(`- **Delta:** ${comparison.delta.score >= 0 ? '+' : ''}${comparison.delta.score} points`);
316
+ lines.push(`- **Trend:** ${comparison.trend}`);
317
+ if (comparison.improvements.length > 0) lines.push(`- **Fixed:** ${comparison.improvements.join(', ')}`);
318
+ if (comparison.regressions.length > 0) lines.push(`- **New gaps:** ${comparison.regressions.join(', ')}`);
319
+ }
320
+
321
+ lines.push('');
322
+ lines.push(`---`);
323
+ lines.push(`*Generated by nerviq v${version}*`);
324
+ return lines.join('\n');
325
+ }
326
+
327
+ function readOutcomeIndex(dir) {
328
+ const indexPath = path.join(dir, '.claude', 'claudex-setup', 'outcomes', 'index.json');
329
+ if (!fs.existsSync(indexPath)) return [];
330
+ try {
331
+ const entries = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
332
+ return Array.isArray(entries) ? entries : [];
333
+ } catch {
334
+ return [];
335
+ }
336
+ }
337
+
338
+ function updateOutcomeIndex(outcomesDir, record) {
339
+ const indexPath = path.join(outcomesDir, 'index.json');
340
+ let entries = [];
341
+
342
+ if (fs.existsSync(indexPath)) {
343
+ try {
344
+ entries = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
345
+ if (!Array.isArray(entries)) entries = [];
346
+ } catch {
347
+ entries = [];
348
+ }
349
+ }
350
+
351
+ entries.push(record);
352
+ const MAX_INDEX_ENTRIES = 500;
353
+ if (entries.length > MAX_INDEX_ENTRIES) {
354
+ entries = entries.slice(entries.length - MAX_INDEX_ENTRIES);
355
+ }
356
+ fs.writeFileSync(indexPath, JSON.stringify(entries, null, 2), 'utf8');
357
+ }
358
+
359
+ function normalizeOutcomeStatus(value) {
360
+ const normalized = `${value || ''}`.trim().toLowerCase();
361
+ if (!['accepted', 'rejected', 'deferred'].includes(normalized)) {
362
+ throw new Error('feedback status must be one of: accepted, rejected, deferred');
363
+ }
364
+ return normalized;
365
+ }
366
+
367
+ function normalizeOutcomeEffect(value) {
368
+ const normalized = `${value || ''}`.trim().toLowerCase();
369
+ if (!['positive', 'neutral', 'negative'].includes(normalized)) {
370
+ throw new Error('feedback effect must be one of: positive, neutral, negative');
371
+ }
372
+ return normalized;
373
+ }
374
+
375
+ function recordRecommendationOutcome(dir, payload) {
376
+ const key = `${payload.key || ''}`.trim();
377
+ if (!key) {
378
+ throw new Error('feedback requires a recommendation key');
379
+ }
380
+
381
+ const status = normalizeOutcomeStatus(payload.status);
382
+ const effect = normalizeOutcomeEffect(payload.effect || 'neutral');
383
+ const scoreDelta = Number.isFinite(payload.scoreDelta) ? payload.scoreDelta : (
384
+ payload.scoreDelta === null || payload.scoreDelta === undefined || payload.scoreDelta === ''
385
+ ? null
386
+ : Number(payload.scoreDelta)
387
+ );
388
+
389
+ if (scoreDelta !== null && !Number.isFinite(scoreDelta)) {
390
+ throw new Error('feedback scoreDelta must be a number when provided');
391
+ }
392
+
393
+ const id = timestampId();
394
+ const { outcomesDir } = ensureArtifactDirs(dir);
395
+ const filePath = path.join(outcomesDir, `${id}.json`);
396
+ const record = {
397
+ id,
398
+ createdAt: new Date().toISOString(),
399
+ key,
400
+ status,
401
+ effect,
402
+ source: `${payload.source || 'manual-cli'}`.trim() || 'manual-cli',
403
+ notes: `${payload.notes || ''}`.trim(),
404
+ scoreDelta,
405
+ };
406
+
407
+ writeJson(filePath, record);
408
+ updateOutcomeIndex(outcomesDir, {
409
+ ...record,
410
+ relativePath: path.relative(dir, filePath),
411
+ });
412
+
413
+ return {
414
+ id,
415
+ filePath,
416
+ relativePath: path.relative(dir, filePath),
417
+ record,
418
+ };
419
+ }
420
+
421
+ function summarizeOutcomeEntries(entries = []) {
422
+ const byKey = {};
423
+
424
+ for (const entry of entries) {
425
+ if (!entry || !entry.key) continue;
426
+ const bucket = byKey[entry.key] || {
427
+ key: entry.key,
428
+ total: 0,
429
+ accepted: 0,
430
+ rejected: 0,
431
+ deferred: 0,
432
+ positive: 0,
433
+ neutral: 0,
434
+ negative: 0,
435
+ scoreDeltaTotal: 0,
436
+ scoreDeltaCount: 0,
437
+ latestAt: null,
438
+ };
439
+
440
+ bucket.total += 1;
441
+ if (bucket[entry.status] !== undefined) bucket[entry.status] += 1;
442
+ if (bucket[entry.effect] !== undefined) bucket[entry.effect] += 1;
443
+ if (Number.isFinite(entry.scoreDelta)) {
444
+ bucket.scoreDeltaTotal += entry.scoreDelta;
445
+ bucket.scoreDeltaCount += 1;
446
+ }
447
+ if (!bucket.latestAt || new Date(entry.createdAt) > new Date(bucket.latestAt)) {
448
+ bucket.latestAt = entry.createdAt;
449
+ }
450
+
451
+ byKey[entry.key] = bucket;
452
+ }
453
+
454
+ for (const bucket of Object.values(byKey)) {
455
+ bucket.avgScoreDelta = bucket.scoreDeltaCount > 0
456
+ ? Number((bucket.scoreDeltaTotal / bucket.scoreDeltaCount).toFixed(2))
457
+ : null;
458
+ bucket.evidenceClass = bucket.total > 0 ? 'measured' : 'estimated';
459
+ }
460
+
461
+ return {
462
+ totalEntries: entries.length,
463
+ byKey,
464
+ keys: Object.keys(byKey).sort(),
465
+ };
466
+ }
467
+
468
+ function getRecommendationOutcomeSummary(dir) {
469
+ return summarizeOutcomeEntries(readOutcomeIndex(dir));
470
+ }
471
+
472
+ function getRecommendationAdjustment(summaryByKey, key) {
473
+ const bucket = summaryByKey && summaryByKey[key];
474
+ if (!bucket) return 0;
475
+
476
+ let adjustment = 0;
477
+ adjustment += bucket.accepted * 2;
478
+ adjustment += bucket.positive * 3;
479
+ adjustment -= bucket.rejected * 3;
480
+ adjustment -= bucket.negative * 4;
481
+
482
+ if (Number.isFinite(bucket.avgScoreDelta)) {
483
+ if (bucket.avgScoreDelta > 0) adjustment += Math.min(4, Math.round(bucket.avgScoreDelta / 4));
484
+ if (bucket.avgScoreDelta < 0) adjustment -= Math.min(4, Math.round(Math.abs(bucket.avgScoreDelta) / 4));
485
+ }
486
+
487
+ if (adjustment > 8) return 8;
488
+ if (adjustment < -8) return -8;
489
+ return adjustment;
490
+ }
491
+
492
+ function formatRecommendationOutcomeSummary(dir) {
493
+ const summary = getRecommendationOutcomeSummary(dir);
494
+ if (summary.totalEntries === 0) {
495
+ return 'No recommendation outcomes recorded yet. Use `npx nerviq feedback --key permissionDeny --status accepted --effect positive` after a real run.';
496
+ }
497
+
498
+ const lines = [
499
+ 'Recommendation outcome summary:',
500
+ '',
501
+ ];
502
+
503
+ for (const key of summary.keys) {
504
+ const bucket = summary.byKey[key];
505
+ const avg = Number.isFinite(bucket.avgScoreDelta) ? ` | avg score delta ${bucket.avgScoreDelta >= 0 ? '+' : ''}${bucket.avgScoreDelta}` : '';
506
+ const adjustment = getRecommendationAdjustment(summary.byKey, key);
507
+ lines.push(` ${key}: total ${bucket.total} | accepted ${bucket.accepted} | rejected ${bucket.rejected} | deferred ${bucket.deferred} | positive ${bucket.positive} | negative ${bucket.negative}${avg} | ranking ${adjustment >= 0 ? '+' : ''}${adjustment}`);
508
+ }
509
+
510
+ return lines.join('\n');
511
+ }
512
+
513
+ module.exports = {
514
+ ensureArtifactDirs,
515
+ writeActivityArtifact,
516
+ writeRollbackArtifact,
517
+ writeSnapshotArtifact,
518
+ readSnapshotIndex,
519
+ getHistory,
520
+ compareLatest,
521
+ formatHistory,
522
+ exportTrendReport,
523
+ readOutcomeIndex,
524
+ recordRecommendationOutcome,
525
+ summarizeOutcomeEntries,
526
+ getRecommendationOutcomeSummary,
527
+ getRecommendationAdjustment,
528
+ formatRecommendationOutcomeSummary,
529
+ };