@nerviq/cli 1.9.0 → 1.11.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nerviq/cli",
3
- "version": "1.9.0",
3
+ "version": "1.11.0",
4
4
  "description": "The intelligent nervous system for AI coding agents — 2,438 checks (8 platforms × ~300 governance rules), 10 languages, 62 domain packs. Audit, align, and amplify.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/activity.js CHANGED
@@ -136,6 +136,37 @@ function summarizeSnapshot(snapshotKind, payload) {
136
136
  return {};
137
137
  }
138
138
 
139
+ function normalizeSnapshotTags(input) {
140
+ const values = Array.isArray(input) ? input : (input ? [input] : []);
141
+ const seen = new Set();
142
+ const tags = [];
143
+
144
+ for (const value of values) {
145
+ const parts = `${value || ''}`
146
+ .split(',')
147
+ .map((item) => item.replace(/\s+/g, ' ').trim())
148
+ .filter(Boolean);
149
+
150
+ for (const part of parts) {
151
+ const key = part.toLowerCase();
152
+ if (seen.has(key)) continue;
153
+ seen.add(key);
154
+ tags.push(part.slice(0, 48));
155
+ if (tags.length >= 8) {
156
+ return tags;
157
+ }
158
+ }
159
+ }
160
+
161
+ return tags;
162
+ }
163
+
164
+ function formatSnapshotTags(tags = []) {
165
+ const normalized = normalizeSnapshotTags(tags);
166
+ if (normalized.length === 0) return '';
167
+ return ` [${normalized.join(', ')}]`;
168
+ }
169
+
139
170
  function updateSnapshotIndex(snapshotDir, record) {
140
171
  const indexPath = path.join(snapshotDir, 'index.json');
141
172
  let entries = [];
@@ -173,6 +204,11 @@ function writeSnapshotArtifact(dir, snapshotKind, payload, meta = {}) {
173
204
  const { snapshotDir } = ensureArtifactDirs(dir);
174
205
  const filePath = path.join(snapshotDir, `${id}-${snapshotKind}.json`);
175
206
  const summary = summarizeSnapshot(snapshotKind, payload);
207
+ const metaTags = normalizeSnapshotTags([
208
+ ...(Array.isArray(meta.tags) ? meta.tags : (meta.tags ? [meta.tags] : [])),
209
+ ...(meta.tag ? [meta.tag] : []),
210
+ ]);
211
+ const { tags: _ignoredTags, tag: _ignoredTag, ...restMeta } = meta;
176
212
  const envelope = {
177
213
  schemaVersion: 1,
178
214
  artifactType: 'snapshot',
@@ -183,7 +219,8 @@ function writeSnapshotArtifact(dir, snapshotKind, payload, meta = {}) {
183
219
  generatedBy: `nerviq@${version}`,
184
220
  directory: dir,
185
221
  summary,
186
- ...meta,
222
+ tags: metaTags,
223
+ ...restMeta,
187
224
  payload,
188
225
  };
189
226
 
@@ -194,6 +231,7 @@ function writeSnapshotArtifact(dir, snapshotKind, payload, meta = {}) {
194
231
  snapshotKind,
195
232
  createdAt: envelope.createdAt,
196
233
  relativePath: path.relative(dir, filePath),
234
+ tags: metaTags,
197
235
  summary,
198
236
  };
199
237
  updateSnapshotIndex(snapshotDir, record);
@@ -236,6 +274,79 @@ function getHistory(dir, limit = 20) {
236
274
  .slice(0, limit);
237
275
  }
238
276
 
277
+ function buildCheckDiffDetail(previousResult, currentResult) {
278
+ const source = currentResult || previousResult || {};
279
+ const previousState = previousResult ? previousResult.passed : undefined;
280
+ const currentState = currentResult ? currentResult.passed : undefined;
281
+ return {
282
+ key: source.key,
283
+ name: source.name || source.key,
284
+ impact: source.impact || null,
285
+ category: source.category || null,
286
+ previousState,
287
+ currentState,
288
+ };
289
+ }
290
+
291
+ function collectCheckDiff(previousResults = [], currentResults = []) {
292
+ const prevMap = new Map();
293
+ const currMap = new Map();
294
+
295
+ for (const result of previousResults) {
296
+ if (result && result.key) prevMap.set(result.key, result);
297
+ }
298
+ for (const result of currentResults) {
299
+ if (result && result.key) currMap.set(result.key, result);
300
+ }
301
+
302
+ const regressions = [];
303
+ const improvements = [];
304
+ const newlyApplicable = [];
305
+ const noLongerApplicable = [];
306
+ const newChecks = [];
307
+ const removedChecks = [];
308
+
309
+ const allKeys = [...new Set([...prevMap.keys(), ...currMap.keys()])].sort();
310
+ for (const key of allKeys) {
311
+ const previousResult = prevMap.get(key);
312
+ const currentResult = currMap.get(key);
313
+ const previousState = previousResult ? previousResult.passed : undefined;
314
+ const currentState = currentResult ? currentResult.passed : undefined;
315
+ const detail = buildCheckDiffDetail(previousResult, currentResult);
316
+
317
+ if (!previousResult) {
318
+ if (currentState !== undefined) {
319
+ newChecks.push(detail);
320
+ }
321
+ continue;
322
+ }
323
+
324
+ if (!currentResult) {
325
+ removedChecks.push(detail);
326
+ continue;
327
+ }
328
+
329
+ if (previousState === true && currentState === false) {
330
+ regressions.push(detail);
331
+ } else if (previousState === false && currentState === true) {
332
+ improvements.push(detail);
333
+ } else if ((previousState === null || previousState === undefined) && (currentState === true || currentState === false)) {
334
+ newlyApplicable.push(detail);
335
+ } else if ((currentState === null || currentState === undefined) && (previousState === true || previousState === false)) {
336
+ noLongerApplicable.push(detail);
337
+ }
338
+ }
339
+
340
+ return {
341
+ regressions,
342
+ improvements,
343
+ newlyApplicable,
344
+ noLongerApplicable,
345
+ newChecks,
346
+ removedChecks,
347
+ };
348
+ }
349
+
239
350
  /**
240
351
  * Compare the two most recent audit snapshots and return the delta.
241
352
  * @param {string} dir - Project root directory.
@@ -247,6 +358,8 @@ function compareLatest(dir) {
247
358
 
248
359
  const current = audits[0];
249
360
  const previous = audits[1];
361
+ const currentPayload = loadSnapshotPayload(dir, current);
362
+ const previousPayload = loadSnapshotPayload(dir, previous);
250
363
 
251
364
  const delta = {
252
365
  score: (current.summary?.score || 0) - (previous.summary?.score || 0),
@@ -254,34 +367,122 @@ function compareLatest(dir) {
254
367
  passed: (current.summary?.passed || 0) - (previous.summary?.passed || 0),
255
368
  };
256
369
 
257
- const regressions = [];
258
- const improvements = [];
259
-
260
- const prevKeys = new Set(previous.summary?.topActionKeys || []);
261
- const currKeys = new Set(current.summary?.topActionKeys || []);
262
-
263
- for (const key of currKeys) {
264
- if (!prevKeys.has(key)) regressions.push(key);
265
- }
266
- for (const key of prevKeys) {
267
- if (!currKeys.has(key)) improvements.push(key);
370
+ let regressionDetails = [];
371
+ let improvementDetails = [];
372
+ let newlyApplicableDetails = [];
373
+ let noLongerApplicableDetails = [];
374
+ let newChecks = [];
375
+ let removedChecks = [];
376
+ let regressions = [];
377
+ let improvements = [];
378
+ let detailedDiffAvailable = false;
379
+
380
+ if (currentPayload && previousPayload && Array.isArray(currentPayload.results) && Array.isArray(previousPayload.results)) {
381
+ const diff = collectCheckDiff(previousPayload.results, currentPayload.results);
382
+ regressionDetails = diff.regressions;
383
+ improvementDetails = diff.improvements;
384
+ newlyApplicableDetails = diff.newlyApplicable;
385
+ noLongerApplicableDetails = diff.noLongerApplicable;
386
+ newChecks = diff.newChecks;
387
+ removedChecks = diff.removedChecks;
388
+ regressions = regressionDetails.map((item) => item.key);
389
+ improvements = improvementDetails.map((item) => item.key);
390
+ detailedDiffAvailable = true;
391
+ } else {
392
+ const prevKeys = new Set(previous.summary?.topActionKeys || []);
393
+ const currKeys = new Set(current.summary?.topActionKeys || []);
394
+ for (const key of currKeys) {
395
+ if (!prevKeys.has(key)) regressions.push(key);
396
+ }
397
+ for (const key of prevKeys) {
398
+ if (!currKeys.has(key)) improvements.push(key);
399
+ }
268
400
  }
269
401
 
270
402
  return {
271
- current: { date: current.createdAt, score: current.summary?.score, passed: current.summary?.passed },
272
- previous: { date: previous.createdAt, score: previous.summary?.score, passed: previous.summary?.passed },
403
+ scoreType: 'audit-snapshot-score',
404
+ current: {
405
+ date: current.createdAt,
406
+ score: current.summary?.score,
407
+ passed: current.summary?.passed,
408
+ tags: current.tags || [],
409
+ scoreType: 'audit-snapshot-score',
410
+ },
411
+ previous: {
412
+ date: previous.createdAt,
413
+ score: previous.summary?.score,
414
+ passed: previous.summary?.passed,
415
+ tags: previous.tags || [],
416
+ scoreType: 'audit-snapshot-score',
417
+ },
273
418
  delta,
274
419
  regressions,
275
420
  improvements,
421
+ regressionDetails,
422
+ improvementDetails,
423
+ newlyApplicableDetails,
424
+ noLongerApplicableDetails,
425
+ newChecks,
426
+ removedChecks,
427
+ detailedDiffAvailable,
276
428
  trend: delta.score > 0 ? 'improving' : delta.score < 0 ? 'regressing' : 'stable',
277
429
  };
278
430
  }
279
431
 
432
+ function formatSnapshotBootstrap(dir, goal = 'history') {
433
+ const snapshotCount = getHistory(dir, 50).length;
434
+ const lines = [];
435
+ const snapshotLabel = snapshotCount === 1
436
+ ? '1 saved audit snapshot'
437
+ : `${snapshotCount} saved audit snapshots`;
438
+
439
+ if (goal === 'compare') {
440
+ lines.push(snapshotCount === 0
441
+ ? 'Compare needs 2 audit snapshots.'
442
+ : 'Compare needs one more audit snapshot.');
443
+ } else if (goal === 'trend') {
444
+ lines.push(snapshotCount === 0
445
+ ? 'Trend needs 2 audit snapshots to start.'
446
+ : 'Trend needs one more audit snapshot to become meaningful.');
447
+ } else {
448
+ lines.push(snapshotCount === 0
449
+ ? 'No audit snapshots found yet.'
450
+ : 'History is initialized, but compare/trend still need one more snapshot.');
451
+ }
452
+
453
+ lines.push(` Current state: ${snapshotLabel}.`);
454
+
455
+ if (snapshotCount === 0) {
456
+ lines.push(' Bootstrap it with:');
457
+ lines.push(' 1. Run `nerviq audit --snapshot --tag "baseline"` to save the baseline.');
458
+ lines.push(' 2. Make a meaningful repo change (`nerviq setup --auto` or `nerviq fix --all-critical --auto`).');
459
+ lines.push(' 3. Run `nerviq audit --snapshot --tag "after-change"` to capture the next state.');
460
+ } else {
461
+ lines.push(' Next:');
462
+ lines.push(' 1. Make a meaningful repo change (`nerviq setup --auto` or `nerviq fix --all-critical --auto`).');
463
+ lines.push(' 2. Run `nerviq audit --snapshot --tag "after-change"` again.');
464
+ }
465
+
466
+ if (goal === 'compare') {
467
+ lines.push(' Then rerun `nerviq compare`.');
468
+ } else if (goal === 'trend') {
469
+ lines.push(' Then rerun `nerviq trend`.');
470
+ } else {
471
+ lines.push(' Then rerun `nerviq history`, `nerviq compare`, or `nerviq trend`.');
472
+ }
473
+
474
+ return lines.join('\n');
475
+ }
476
+
280
477
  function formatHistory(dir) {
281
478
  const history = getHistory(dir, 10);
282
- if (history.length === 0) return 'No snapshots found. Run `npx nerviq --snapshot` to save one.';
479
+ if (history.length === 0) return formatSnapshotBootstrap(dir, 'history');
283
480
 
284
- const lines = ['Score history (most recent first):', ''];
481
+ const lines = [
482
+ 'Audit snapshot history (most recent first):',
483
+ ' Score type: saved audit snapshot scores only (not live audits or benchmark projections).',
484
+ '',
485
+ ];
285
486
  for (const entry of history) {
286
487
  const dateStr = entry.createdAt || 'unknown';
287
488
  const date = dateStr.split('T')[0] || 'unknown';
@@ -290,14 +491,17 @@ function formatHistory(dir) {
290
491
  const score = entry.summary?.score ?? '?';
291
492
  const passed = entry.summary?.passed ?? '?';
292
493
  const total = entry.summary?.checkCount ?? '?';
293
- lines.push(` ${dateDisplay} ${score}/100 (${passed}/${total} passing)`);
494
+ lines.push(` ${dateDisplay} snapshot${formatSnapshotTags(entry.tags)} ${score}/100 (${passed}/${total} checks passing)`);
294
495
  }
295
496
 
296
497
  const comparison = compareLatest(dir);
297
498
  if (comparison) {
298
499
  lines.push('');
299
500
  const sign = comparison.delta.score >= 0 ? '+' : '';
300
- lines.push(` Trend: ${comparison.trend} (${sign}${comparison.delta.score} since previous)`);
501
+ lines.push(` Latest snapshot trend: ${comparison.trend} (${sign}${comparison.delta.score} since previous snapshot)`);
502
+ if ((comparison.previous.tags || []).length > 0 || (comparison.current.tags || []).length > 0) {
503
+ lines.push(` Snapshot tags: previous${formatSnapshotTags(comparison.previous.tags)} -> current${formatSnapshotTags(comparison.current.tags)}`);
504
+ }
301
505
  if (comparison.improvements.length > 0) {
302
506
  lines.push(` Fixed: ${comparison.improvements.join(', ')}`);
303
507
  }
@@ -306,6 +510,11 @@ function formatHistory(dir) {
306
510
  }
307
511
  }
308
512
 
513
+ if (history.length === 1) {
514
+ lines.push('');
515
+ lines.push(formatSnapshotBootstrap(dir, 'history'));
516
+ }
517
+
309
518
  return lines.join('\n');
310
519
  }
311
520
 
@@ -315,30 +524,31 @@ function exportTrendReport(dir) {
315
524
 
316
525
  const comparison = compareLatest(dir);
317
526
  const lines = [
318
- '# Claude Code Setup Trend Report',
527
+ '# Nerviq Audit Snapshot Trend Report',
319
528
  '',
320
529
  `**Project:** ${path.basename(dir)}`,
321
530
  `**Generated:** ${new Date().toISOString().split('T')[0]}`,
322
- `**Snapshots:** ${history.length}`,
531
+ `**Audit snapshots:** ${history.length}`,
323
532
  '',
324
- '## Score History',
533
+ '## Audit Snapshot History',
325
534
  '',
326
- '| Date | Score | Passed | Checks |',
327
- '|------|-------|--------|--------|',
535
+ '| Date | Tags | Score | Passed | Checks |',
536
+ '|------|------|-------|--------|--------|',
328
537
  ];
329
538
 
330
539
  for (const entry of history) {
331
540
  const date = entry.createdAt?.split('T')[0] || '?';
332
- lines.push(`| ${date} | ${entry.summary?.score ?? '?'}/100 | ${entry.summary?.passed ?? '?'} | ${entry.summary?.checkCount ?? '?'} |`);
541
+ const tags = (entry.tags || []).length > 0 ? entry.tags.join(', ') : '-';
542
+ lines.push(`| ${date} | ${tags} | ${entry.summary?.score ?? '?'}/100 | ${entry.summary?.passed ?? '?'} | ${entry.summary?.checkCount ?? '?'} |`);
333
543
  }
334
544
 
335
545
  if (comparison) {
336
546
  lines.push('');
337
547
  lines.push('## Latest Comparison');
338
548
  lines.push('');
339
- lines.push(`- **Previous:** ${comparison.previous.score}/100 (${comparison.previous.date?.split('T')[0]})`);
340
- lines.push(`- **Current:** ${comparison.current.score}/100 (${comparison.current.date?.split('T')[0]})`);
341
- lines.push(`- **Delta:** ${comparison.delta.score >= 0 ? '+' : ''}${comparison.delta.score} points`);
549
+ lines.push(`- **Previous snapshot score:** ${comparison.previous.score}/100 (${comparison.previous.date?.split('T')[0]})${formatSnapshotTags(comparison.previous.tags)}`);
550
+ lines.push(`- **Current snapshot score:** ${comparison.current.score}/100 (${comparison.current.date?.split('T')[0]})${formatSnapshotTags(comparison.current.tags)}`);
551
+ lines.push(`- **Snapshot delta:** ${comparison.delta.score >= 0 ? '+' : ''}${comparison.delta.score} points`);
342
552
  lines.push(`- **Trend:** ${comparison.trend}`);
343
553
  if (comparison.improvements.length > 0) lines.push(`- **Fixed:** ${comparison.improvements.join(', ')}`);
344
554
  if (comparison.regressions.length > 0) lines.push(`- **New gaps:** ${comparison.regressions.join(', ')}`);
@@ -572,42 +782,11 @@ function checkHealth(dir) {
572
782
 
573
783
  const currentResults = currentPayload.results || [];
574
784
  const previousResults = previousPayload.results || [];
575
-
576
- // Build maps: key → passed (true/false/null)
577
- const prevMap = {};
578
- for (const r of previousResults) {
579
- if (r.key) prevMap[r.key] = r.passed;
580
- }
581
- const currMap = {};
582
- for (const r of currentResults) {
583
- if (r.key) currMap[r.key] = r.passed;
584
- }
585
-
586
- const regressions = []; // was passing → now failing
587
- const improvements = []; // was failing → now passing
588
- const newChecks = []; // not in previous
589
- const removedChecks = []; // not in current
590
-
591
- for (const r of currentResults) {
592
- if (!r.key) continue;
593
- const prev = prevMap[r.key];
594
- const curr = r.passed;
595
- if (prev === undefined) {
596
- if (curr !== null) newChecks.push({ key: r.key, name: r.name, impact: r.impact, passed: curr });
597
- continue;
598
- }
599
- if (prev === true && curr === false) {
600
- regressions.push({ key: r.key, name: r.name, impact: r.impact, category: r.category });
601
- } else if (prev === false && curr === true) {
602
- improvements.push({ key: r.key, name: r.name, impact: r.impact, category: r.category });
603
- }
604
- }
605
-
606
- for (const r of previousResults) {
607
- if (r.key && currMap[r.key] === undefined) {
608
- removedChecks.push({ key: r.key, name: r.name });
609
- }
610
- }
785
+ const diff = collectCheckDiff(previousResults, currentResults);
786
+ const regressions = diff.regressions;
787
+ const improvements = diff.improvements;
788
+ const newChecks = diff.newChecks;
789
+ const removedChecks = diff.removedChecks;
611
790
 
612
791
  // Detect potential platform format changes:
613
792
  // If 3+ checks in the same category regressed, flag it
@@ -800,9 +979,12 @@ module.exports = {
800
979
  writeActivityArtifact,
801
980
  writeRollbackArtifact,
802
981
  writeSnapshotArtifact,
982
+ normalizeSnapshotTags,
983
+ formatSnapshotTags,
803
984
  readSnapshotIndex,
804
985
  getHistory,
805
986
  compareLatest,
987
+ formatSnapshotBootstrap,
806
988
  formatHistory,
807
989
  exportTrendReport,
808
990
  readOutcomeIndex,