@nerviq/cli 1.10.0 → 1.12.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.
Files changed (57) hide show
  1. package/README.md +176 -47
  2. package/bin/cli.js +842 -287
  3. package/package.json +2 -2
  4. package/src/activity.js +225 -59
  5. package/src/adoption-advisor.js +299 -0
  6. package/src/aider/freshness.js +28 -25
  7. package/src/aider/techniques.js +16 -11
  8. package/src/analyze.js +131 -1
  9. package/src/anti-patterns.js +17 -2
  10. package/src/audit.js +197 -96
  11. package/src/behavioral-drift.js +801 -0
  12. package/src/benchmark.js +15 -10
  13. package/src/continuous-ops.js +681 -0
  14. package/src/cost-tracking.js +61 -0
  15. package/src/cursor/techniques.js +17 -12
  16. package/src/deep-review.js +83 -0
  17. package/src/diff-only.js +280 -0
  18. package/src/doctor.js +118 -55
  19. package/src/governance.js +72 -50
  20. package/src/hook-validation.js +342 -0
  21. package/src/index.js +7 -1
  22. package/src/integrations.js +144 -60
  23. package/src/mcp-validation.js +337 -0
  24. package/src/opencode/techniques.js +12 -7
  25. package/src/operating-profile.js +574 -0
  26. package/src/org.js +97 -13
  27. package/src/permission-rules.js +218 -0
  28. package/src/plans.js +192 -8
  29. package/src/platform-change-manifest.js +86 -0
  30. package/src/policy-layers.js +210 -0
  31. package/src/profiles.js +4 -1
  32. package/src/prompt-injection.js +74 -0
  33. package/src/repo-archetype.js +386 -0
  34. package/src/secret-patterns.js +9 -0
  35. package/src/server.js +398 -3
  36. package/src/setup.js +36 -2
  37. package/src/source-urls.js +132 -132
  38. package/src/supplemental-checks.js +13 -12
  39. package/src/techniques/api.js +407 -0
  40. package/src/techniques/automation.js +316 -0
  41. package/src/techniques/compliance.js +257 -0
  42. package/src/techniques/hygiene.js +294 -0
  43. package/src/techniques/instructions.js +243 -0
  44. package/src/techniques/observability.js +226 -0
  45. package/src/techniques/optimization.js +142 -0
  46. package/src/techniques/quality.js +317 -0
  47. package/src/techniques/security.js +237 -0
  48. package/src/techniques/shared.js +443 -0
  49. package/src/techniques/stacks.js +2294 -0
  50. package/src/techniques/tools.js +106 -0
  51. package/src/techniques/workflow.js +413 -0
  52. package/src/techniques.js +78 -5611
  53. package/src/terminology.js +73 -0
  54. package/src/token-estimate.js +35 -0
  55. package/src/watch.js +18 -0
  56. package/src/windsurf/techniques.js +17 -12
  57. package/src/workspace.js +105 -8
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nerviq/cli",
3
- "version": "1.10.0",
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.",
3
+ "version": "1.12.0",
4
+ "description": "The intelligent nervous system for AI coding agents — 2,441 checks (8 platforms × ~300 governance rules), 10 languages, 62 domain packs. Audit, align, and amplify.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
7
7
  "nerviq": "bin/cli.js",
package/src/activity.js CHANGED
@@ -25,6 +25,8 @@ function getUserId() {
25
25
  let _lastTimestamp = '';
26
26
  let _counter = 0;
27
27
 
28
+ const SNAPSHOT_MILESTONES = ['baseline', 'post-fix', 'pre-upgrade', 'release'];
29
+
28
30
  function timestampId() {
29
31
  const ts = new Date().toISOString().replace(/[:.]/g, '-');
30
32
  if (ts === _lastTimestamp) {
@@ -133,9 +135,66 @@ function summarizeSnapshot(snapshotKind, payload) {
133
135
  };
134
136
  }
135
137
 
138
+ if (snapshotKind === 'behavioral-drift') {
139
+ return {
140
+ score: payload.score,
141
+ sourceFiles: payload.repoSummary?.sourceFiles ?? 0,
142
+ findingCount: Array.isArray(payload.findings) ? payload.findings.length : 0,
143
+ driftLabels: Array.isArray(payload.driftLabels) ? payload.driftLabels.slice(0, 5) : [],
144
+ utilityShare: payload.structuralSignals?.utilityBalance?.utilityShare ?? null,
145
+ layerBreaks: payload.structuralSignals?.layering?.count ?? 0,
146
+ };
147
+ }
148
+
136
149
  return {};
137
150
  }
138
151
 
152
+ function normalizeSnapshotTags(input) {
153
+ const values = Array.isArray(input) ? input : (input ? [input] : []);
154
+ const seen = new Set();
155
+ const tags = [];
156
+
157
+ for (const value of values) {
158
+ const parts = `${value || ''}`
159
+ .split(',')
160
+ .map((item) => item.replace(/\s+/g, ' ').trim())
161
+ .filter(Boolean);
162
+
163
+ for (const part of parts) {
164
+ const key = part.toLowerCase();
165
+ if (seen.has(key)) continue;
166
+ seen.add(key);
167
+ tags.push(part.slice(0, 48));
168
+ if (tags.length >= 8) {
169
+ return tags;
170
+ }
171
+ }
172
+ }
173
+
174
+ return tags;
175
+ }
176
+
177
+ function formatSnapshotTags(tags = []) {
178
+ const normalized = normalizeSnapshotTags(tags);
179
+ if (normalized.length === 0) return '';
180
+ return ` [${normalized.join(', ')}]`;
181
+ }
182
+
183
+ function normalizeSnapshotMilestone(value) {
184
+ if (value === null || value === undefined || value === '') return null;
185
+ const normalized = `${value}`.trim().toLowerCase();
186
+ if (!SNAPSHOT_MILESTONES.includes(normalized)) {
187
+ throw new Error(`snapshot milestone must be one of: ${SNAPSHOT_MILESTONES.join(', ')}`);
188
+ }
189
+ return normalized;
190
+ }
191
+
192
+ function formatSnapshotMilestone(value) {
193
+ const milestone = normalizeSnapshotMilestone(value);
194
+ if (!milestone) return '';
195
+ return ` (${milestone})`;
196
+ }
197
+
139
198
  function updateSnapshotIndex(snapshotDir, record) {
140
199
  const indexPath = path.join(snapshotDir, 'index.json');
141
200
  let entries = [];
@@ -173,6 +232,12 @@ function writeSnapshotArtifact(dir, snapshotKind, payload, meta = {}) {
173
232
  const { snapshotDir } = ensureArtifactDirs(dir);
174
233
  const filePath = path.join(snapshotDir, `${id}-${snapshotKind}.json`);
175
234
  const summary = summarizeSnapshot(snapshotKind, payload);
235
+ const metaTags = normalizeSnapshotTags([
236
+ ...(Array.isArray(meta.tags) ? meta.tags : (meta.tags ? [meta.tags] : [])),
237
+ ...(meta.tag ? [meta.tag] : []),
238
+ ]);
239
+ const milestone = normalizeSnapshotMilestone(meta.milestone);
240
+ const { tags: _ignoredTags, tag: _ignoredTag, ...restMeta } = meta;
176
241
  const envelope = {
177
242
  schemaVersion: 1,
178
243
  artifactType: 'snapshot',
@@ -183,7 +248,9 @@ function writeSnapshotArtifact(dir, snapshotKind, payload, meta = {}) {
183
248
  generatedBy: `nerviq@${version}`,
184
249
  directory: dir,
185
250
  summary,
186
- ...meta,
251
+ tags: metaTags,
252
+ milestone,
253
+ ...restMeta,
187
254
  payload,
188
255
  };
189
256
 
@@ -194,6 +261,8 @@ function writeSnapshotArtifact(dir, snapshotKind, payload, meta = {}) {
194
261
  snapshotKind,
195
262
  createdAt: envelope.createdAt,
196
263
  relativePath: path.relative(dir, filePath),
264
+ tags: metaTags,
265
+ milestone,
197
266
  summary,
198
267
  };
199
268
  updateSnapshotIndex(snapshotDir, record);
@@ -236,6 +305,79 @@ function getHistory(dir, limit = 20) {
236
305
  .slice(0, limit);
237
306
  }
238
307
 
308
+ function buildCheckDiffDetail(previousResult, currentResult) {
309
+ const source = currentResult || previousResult || {};
310
+ const previousState = previousResult ? previousResult.passed : undefined;
311
+ const currentState = currentResult ? currentResult.passed : undefined;
312
+ return {
313
+ key: source.key,
314
+ name: source.name || source.key,
315
+ impact: source.impact || null,
316
+ category: source.category || null,
317
+ previousState,
318
+ currentState,
319
+ };
320
+ }
321
+
322
+ function collectCheckDiff(previousResults = [], currentResults = []) {
323
+ const prevMap = new Map();
324
+ const currMap = new Map();
325
+
326
+ for (const result of previousResults) {
327
+ if (result && result.key) prevMap.set(result.key, result);
328
+ }
329
+ for (const result of currentResults) {
330
+ if (result && result.key) currMap.set(result.key, result);
331
+ }
332
+
333
+ const regressions = [];
334
+ const improvements = [];
335
+ const newlyApplicable = [];
336
+ const noLongerApplicable = [];
337
+ const newChecks = [];
338
+ const removedChecks = [];
339
+
340
+ const allKeys = [...new Set([...prevMap.keys(), ...currMap.keys()])].sort();
341
+ for (const key of allKeys) {
342
+ const previousResult = prevMap.get(key);
343
+ const currentResult = currMap.get(key);
344
+ const previousState = previousResult ? previousResult.passed : undefined;
345
+ const currentState = currentResult ? currentResult.passed : undefined;
346
+ const detail = buildCheckDiffDetail(previousResult, currentResult);
347
+
348
+ if (!previousResult) {
349
+ if (currentState !== undefined) {
350
+ newChecks.push(detail);
351
+ }
352
+ continue;
353
+ }
354
+
355
+ if (!currentResult) {
356
+ removedChecks.push(detail);
357
+ continue;
358
+ }
359
+
360
+ if (previousState === true && currentState === false) {
361
+ regressions.push(detail);
362
+ } else if (previousState === false && currentState === true) {
363
+ improvements.push(detail);
364
+ } else if ((previousState === null || previousState === undefined) && (currentState === true || currentState === false)) {
365
+ newlyApplicable.push(detail);
366
+ } else if ((currentState === null || currentState === undefined) && (previousState === true || previousState === false)) {
367
+ noLongerApplicable.push(detail);
368
+ }
369
+ }
370
+
371
+ return {
372
+ regressions,
373
+ improvements,
374
+ newlyApplicable,
375
+ noLongerApplicable,
376
+ newChecks,
377
+ removedChecks,
378
+ };
379
+ }
380
+
239
381
  /**
240
382
  * Compare the two most recent audit snapshots and return the delta.
241
383
  * @param {string} dir - Project root directory.
@@ -247,6 +389,8 @@ function compareLatest(dir) {
247
389
 
248
390
  const current = audits[0];
249
391
  const previous = audits[1];
392
+ const currentPayload = loadSnapshotPayload(dir, current);
393
+ const previousPayload = loadSnapshotPayload(dir, previous);
250
394
 
251
395
  const delta = {
252
396
  score: (current.summary?.score || 0) - (previous.summary?.score || 0),
@@ -254,26 +398,66 @@ function compareLatest(dir) {
254
398
  passed: (current.summary?.passed || 0) - (previous.summary?.passed || 0),
255
399
  };
256
400
 
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);
401
+ let regressionDetails = [];
402
+ let improvementDetails = [];
403
+ let newlyApplicableDetails = [];
404
+ let noLongerApplicableDetails = [];
405
+ let newChecks = [];
406
+ let removedChecks = [];
407
+ let regressions = [];
408
+ let improvements = [];
409
+ let detailedDiffAvailable = false;
410
+
411
+ if (currentPayload && previousPayload && Array.isArray(currentPayload.results) && Array.isArray(previousPayload.results)) {
412
+ const diff = collectCheckDiff(previousPayload.results, currentPayload.results);
413
+ regressionDetails = diff.regressions;
414
+ improvementDetails = diff.improvements;
415
+ newlyApplicableDetails = diff.newlyApplicable;
416
+ noLongerApplicableDetails = diff.noLongerApplicable;
417
+ newChecks = diff.newChecks;
418
+ removedChecks = diff.removedChecks;
419
+ regressions = regressionDetails.map((item) => item.key);
420
+ improvements = improvementDetails.map((item) => item.key);
421
+ detailedDiffAvailable = true;
422
+ } else {
423
+ const prevKeys = new Set(previous.summary?.topActionKeys || []);
424
+ const currKeys = new Set(current.summary?.topActionKeys || []);
425
+ for (const key of currKeys) {
426
+ if (!prevKeys.has(key)) regressions.push(key);
427
+ }
428
+ for (const key of prevKeys) {
429
+ if (!currKeys.has(key)) improvements.push(key);
430
+ }
268
431
  }
269
432
 
270
433
  return {
271
434
  scoreType: 'audit-snapshot-score',
272
- current: { date: current.createdAt, score: current.summary?.score, passed: current.summary?.passed, scoreType: 'audit-snapshot-score' },
273
- previous: { date: previous.createdAt, score: previous.summary?.score, passed: previous.summary?.passed, scoreType: 'audit-snapshot-score' },
435
+ current: {
436
+ date: current.createdAt,
437
+ score: current.summary?.score,
438
+ passed: current.summary?.passed,
439
+ tags: current.tags || [],
440
+ milestone: current.milestone || null,
441
+ scoreType: 'audit-snapshot-score',
442
+ },
443
+ previous: {
444
+ date: previous.createdAt,
445
+ score: previous.summary?.score,
446
+ passed: previous.summary?.passed,
447
+ tags: previous.tags || [],
448
+ milestone: previous.milestone || null,
449
+ scoreType: 'audit-snapshot-score',
450
+ },
274
451
  delta,
275
452
  regressions,
276
453
  improvements,
454
+ regressionDetails,
455
+ improvementDetails,
456
+ newlyApplicableDetails,
457
+ noLongerApplicableDetails,
458
+ newChecks,
459
+ removedChecks,
460
+ detailedDiffAvailable,
277
461
  trend: delta.score > 0 ? 'improving' : delta.score < 0 ? 'regressing' : 'stable',
278
462
  };
279
463
  }
@@ -303,13 +487,13 @@ function formatSnapshotBootstrap(dir, goal = 'history') {
303
487
 
304
488
  if (snapshotCount === 0) {
305
489
  lines.push(' Bootstrap it with:');
306
- lines.push(' 1. Run `nerviq audit --snapshot` to save the baseline.');
490
+ lines.push(' 1. Run `nerviq audit --snapshot --milestone baseline --tag "baseline"` to save the baseline.');
307
491
  lines.push(' 2. Make a meaningful repo change (`nerviq setup --auto` or `nerviq fix --all-critical --auto`).');
308
- lines.push(' 3. Run `nerviq audit --snapshot` again to capture the next state.');
492
+ lines.push(' 3. Run `nerviq audit --snapshot --milestone post-fix --tag "after-change"` to capture the next state.');
309
493
  } else {
310
494
  lines.push(' Next:');
311
495
  lines.push(' 1. Make a meaningful repo change (`nerviq setup --auto` or `nerviq fix --all-critical --auto`).');
312
- lines.push(' 2. Run `nerviq audit --snapshot` again.');
496
+ lines.push(' 2. Run `nerviq audit --snapshot --milestone post-fix --tag "after-change"` again.');
313
497
  }
314
498
 
315
499
  if (goal === 'compare') {
@@ -340,7 +524,7 @@ function formatHistory(dir) {
340
524
  const score = entry.summary?.score ?? '?';
341
525
  const passed = entry.summary?.passed ?? '?';
342
526
  const total = entry.summary?.checkCount ?? '?';
343
- lines.push(` ${dateDisplay} snapshot ${score}/100 (${passed}/${total} checks passing)`);
527
+ lines.push(` ${dateDisplay} snapshot${formatSnapshotMilestone(entry.milestone)}${formatSnapshotTags(entry.tags)} ${score}/100 (${passed}/${total} checks passing)`);
344
528
  }
345
529
 
346
530
  const comparison = compareLatest(dir);
@@ -348,6 +532,12 @@ function formatHistory(dir) {
348
532
  lines.push('');
349
533
  const sign = comparison.delta.score >= 0 ? '+' : '';
350
534
  lines.push(` Latest snapshot trend: ${comparison.trend} (${sign}${comparison.delta.score} since previous snapshot)`);
535
+ if ((comparison.previous.tags || []).length > 0 || (comparison.current.tags || []).length > 0) {
536
+ lines.push(` Snapshot tags: previous${formatSnapshotTags(comparison.previous.tags)} -> current${formatSnapshotTags(comparison.current.tags)}`);
537
+ }
538
+ if (comparison.previous.milestone || comparison.current.milestone) {
539
+ lines.push(` Lifecycle: previous${formatSnapshotMilestone(comparison.previous.milestone)} -> current${formatSnapshotMilestone(comparison.current.milestone)}`);
540
+ }
351
541
  if (comparison.improvements.length > 0) {
352
542
  lines.push(` Fixed: ${comparison.improvements.join(', ')}`);
353
543
  }
@@ -378,21 +568,23 @@ function exportTrendReport(dir) {
378
568
  '',
379
569
  '## Audit Snapshot History',
380
570
  '',
381
- '| Date | Score | Passed | Checks |',
382
- '|------|-------|--------|--------|',
571
+ '| Date | Milestone | Tags | Score | Passed | Checks |',
572
+ '|------|-----------|------|-------|--------|--------|',
383
573
  ];
384
574
 
385
575
  for (const entry of history) {
386
576
  const date = entry.createdAt?.split('T')[0] || '?';
387
- lines.push(`| ${date} | ${entry.summary?.score ?? '?'}/100 | ${entry.summary?.passed ?? '?'} | ${entry.summary?.checkCount ?? '?'} |`);
577
+ const milestone = entry.milestone || '-';
578
+ const tags = (entry.tags || []).length > 0 ? entry.tags.join(', ') : '-';
579
+ lines.push(`| ${date} | ${milestone} | ${tags} | ${entry.summary?.score ?? '?'}/100 | ${entry.summary?.passed ?? '?'} | ${entry.summary?.checkCount ?? '?'} |`);
388
580
  }
389
581
 
390
582
  if (comparison) {
391
583
  lines.push('');
392
584
  lines.push('## Latest Comparison');
393
585
  lines.push('');
394
- lines.push(`- **Previous snapshot score:** ${comparison.previous.score}/100 (${comparison.previous.date?.split('T')[0]})`);
395
- lines.push(`- **Current snapshot score:** ${comparison.current.score}/100 (${comparison.current.date?.split('T')[0]})`);
586
+ lines.push(`- **Previous snapshot score:** ${comparison.previous.score}/100 (${comparison.previous.date?.split('T')[0]})${formatSnapshotMilestone(comparison.previous.milestone)}${formatSnapshotTags(comparison.previous.tags)}`);
587
+ lines.push(`- **Current snapshot score:** ${comparison.current.score}/100 (${comparison.current.date?.split('T')[0]})${formatSnapshotMilestone(comparison.current.milestone)}${formatSnapshotTags(comparison.current.tags)}`);
396
588
  lines.push(`- **Snapshot delta:** ${comparison.delta.score >= 0 ? '+' : ''}${comparison.delta.score} points`);
397
589
  lines.push(`- **Trend:** ${comparison.trend}`);
398
590
  if (comparison.improvements.length > 0) lines.push(`- **Fixed:** ${comparison.improvements.join(', ')}`);
@@ -627,42 +819,11 @@ function checkHealth(dir) {
627
819
 
628
820
  const currentResults = currentPayload.results || [];
629
821
  const previousResults = previousPayload.results || [];
630
-
631
- // Build maps: key → passed (true/false/null)
632
- const prevMap = {};
633
- for (const r of previousResults) {
634
- if (r.key) prevMap[r.key] = r.passed;
635
- }
636
- const currMap = {};
637
- for (const r of currentResults) {
638
- if (r.key) currMap[r.key] = r.passed;
639
- }
640
-
641
- const regressions = []; // was passing → now failing
642
- const improvements = []; // was failing → now passing
643
- const newChecks = []; // not in previous
644
- const removedChecks = []; // not in current
645
-
646
- for (const r of currentResults) {
647
- if (!r.key) continue;
648
- const prev = prevMap[r.key];
649
- const curr = r.passed;
650
- if (prev === undefined) {
651
- if (curr !== null) newChecks.push({ key: r.key, name: r.name, impact: r.impact, passed: curr });
652
- continue;
653
- }
654
- if (prev === true && curr === false) {
655
- regressions.push({ key: r.key, name: r.name, impact: r.impact, category: r.category });
656
- } else if (prev === false && curr === true) {
657
- improvements.push({ key: r.key, name: r.name, impact: r.impact, category: r.category });
658
- }
659
- }
660
-
661
- for (const r of previousResults) {
662
- if (r.key && currMap[r.key] === undefined) {
663
- removedChecks.push({ key: r.key, name: r.name });
664
- }
665
- }
822
+ const diff = collectCheckDiff(previousResults, currentResults);
823
+ const regressions = diff.regressions;
824
+ const improvements = diff.improvements;
825
+ const newChecks = diff.newChecks;
826
+ const removedChecks = diff.removedChecks;
666
827
 
667
828
  // Detect potential platform format changes:
668
829
  // If 3+ checks in the same category regressed, flag it
@@ -855,6 +1016,11 @@ module.exports = {
855
1016
  writeActivityArtifact,
856
1017
  writeRollbackArtifact,
857
1018
  writeSnapshotArtifact,
1019
+ normalizeSnapshotTags,
1020
+ formatSnapshotTags,
1021
+ normalizeSnapshotMilestone,
1022
+ formatSnapshotMilestone,
1023
+ SNAPSHOT_MILESTONES,
858
1024
  readSnapshotIndex,
859
1025
  getHistory,
860
1026
  compareLatest,