@nerviq/cli 1.11.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 (49) hide show
  1. package/README.md +97 -19
  2. package/bin/cli.js +618 -182
  3. package/package.json +2 -2
  4. package/src/activity.js +49 -9
  5. package/src/adoption-advisor.js +299 -0
  6. package/src/aider/techniques.js +16 -11
  7. package/src/analyze.js +128 -0
  8. package/src/anti-patterns.js +13 -0
  9. package/src/audit.js +97 -22
  10. package/src/behavioral-drift.js +801 -0
  11. package/src/continuous-ops.js +681 -0
  12. package/src/cost-tracking.js +61 -0
  13. package/src/cursor/techniques.js +17 -12
  14. package/src/deep-review.js +83 -0
  15. package/src/diff-only.js +280 -0
  16. package/src/doctor.js +118 -55
  17. package/src/governance.js +59 -43
  18. package/src/hook-validation.js +342 -0
  19. package/src/index.js +5 -0
  20. package/src/integrations.js +42 -5
  21. package/src/mcp-validation.js +337 -0
  22. package/src/opencode/techniques.js +12 -7
  23. package/src/operating-profile.js +574 -0
  24. package/src/org.js +97 -13
  25. package/src/plans.js +192 -8
  26. package/src/platform-change-manifest.js +86 -0
  27. package/src/policy-layers.js +210 -0
  28. package/src/profiles.js +4 -1
  29. package/src/prompt-injection.js +74 -0
  30. package/src/repo-archetype.js +386 -0
  31. package/src/setup.js +34 -0
  32. package/src/source-urls.js +132 -132
  33. package/src/supplemental-checks.js +13 -12
  34. package/src/techniques/api.js +407 -0
  35. package/src/techniques/automation.js +316 -0
  36. package/src/techniques/compliance.js +257 -0
  37. package/src/techniques/hygiene.js +294 -0
  38. package/src/techniques/instructions.js +243 -0
  39. package/src/techniques/observability.js +226 -0
  40. package/src/techniques/optimization.js +142 -0
  41. package/src/techniques/quality.js +317 -0
  42. package/src/techniques/security.js +237 -0
  43. package/src/techniques/shared.js +443 -0
  44. package/src/techniques/stacks.js +2294 -0
  45. package/src/techniques/tools.js +106 -0
  46. package/src/techniques/workflow.js +413 -0
  47. package/src/techniques.js +78 -5607
  48. package/src/watch.js +18 -0
  49. package/src/windsurf/techniques.js +17 -12
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nerviq/cli",
3
- "version": "1.11.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,6 +135,17 @@ 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
 
@@ -167,6 +180,21 @@ function formatSnapshotTags(tags = []) {
167
180
  return ` [${normalized.join(', ')}]`;
168
181
  }
169
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
+
170
198
  function updateSnapshotIndex(snapshotDir, record) {
171
199
  const indexPath = path.join(snapshotDir, 'index.json');
172
200
  let entries = [];
@@ -208,6 +236,7 @@ function writeSnapshotArtifact(dir, snapshotKind, payload, meta = {}) {
208
236
  ...(Array.isArray(meta.tags) ? meta.tags : (meta.tags ? [meta.tags] : [])),
209
237
  ...(meta.tag ? [meta.tag] : []),
210
238
  ]);
239
+ const milestone = normalizeSnapshotMilestone(meta.milestone);
211
240
  const { tags: _ignoredTags, tag: _ignoredTag, ...restMeta } = meta;
212
241
  const envelope = {
213
242
  schemaVersion: 1,
@@ -220,6 +249,7 @@ function writeSnapshotArtifact(dir, snapshotKind, payload, meta = {}) {
220
249
  directory: dir,
221
250
  summary,
222
251
  tags: metaTags,
252
+ milestone,
223
253
  ...restMeta,
224
254
  payload,
225
255
  };
@@ -232,6 +262,7 @@ function writeSnapshotArtifact(dir, snapshotKind, payload, meta = {}) {
232
262
  createdAt: envelope.createdAt,
233
263
  relativePath: path.relative(dir, filePath),
234
264
  tags: metaTags,
265
+ milestone,
235
266
  summary,
236
267
  };
237
268
  updateSnapshotIndex(snapshotDir, record);
@@ -406,6 +437,7 @@ function compareLatest(dir) {
406
437
  score: current.summary?.score,
407
438
  passed: current.summary?.passed,
408
439
  tags: current.tags || [],
440
+ milestone: current.milestone || null,
409
441
  scoreType: 'audit-snapshot-score',
410
442
  },
411
443
  previous: {
@@ -413,6 +445,7 @@ function compareLatest(dir) {
413
445
  score: previous.summary?.score,
414
446
  passed: previous.summary?.passed,
415
447
  tags: previous.tags || [],
448
+ milestone: previous.milestone || null,
416
449
  scoreType: 'audit-snapshot-score',
417
450
  },
418
451
  delta,
@@ -454,13 +487,13 @@ function formatSnapshotBootstrap(dir, goal = 'history') {
454
487
 
455
488
  if (snapshotCount === 0) {
456
489
  lines.push(' Bootstrap it with:');
457
- lines.push(' 1. Run `nerviq audit --snapshot --tag "baseline"` to save the baseline.');
490
+ lines.push(' 1. Run `nerviq audit --snapshot --milestone baseline --tag "baseline"` to save the baseline.');
458
491
  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.');
492
+ lines.push(' 3. Run `nerviq audit --snapshot --milestone post-fix --tag "after-change"` to capture the next state.');
460
493
  } else {
461
494
  lines.push(' Next:');
462
495
  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.');
496
+ lines.push(' 2. Run `nerviq audit --snapshot --milestone post-fix --tag "after-change"` again.');
464
497
  }
465
498
 
466
499
  if (goal === 'compare') {
@@ -491,7 +524,7 @@ function formatHistory(dir) {
491
524
  const score = entry.summary?.score ?? '?';
492
525
  const passed = entry.summary?.passed ?? '?';
493
526
  const total = entry.summary?.checkCount ?? '?';
494
- lines.push(` ${dateDisplay} snapshot${formatSnapshotTags(entry.tags)} ${score}/100 (${passed}/${total} checks passing)`);
527
+ lines.push(` ${dateDisplay} snapshot${formatSnapshotMilestone(entry.milestone)}${formatSnapshotTags(entry.tags)} ${score}/100 (${passed}/${total} checks passing)`);
495
528
  }
496
529
 
497
530
  const comparison = compareLatest(dir);
@@ -502,6 +535,9 @@ function formatHistory(dir) {
502
535
  if ((comparison.previous.tags || []).length > 0 || (comparison.current.tags || []).length > 0) {
503
536
  lines.push(` Snapshot tags: previous${formatSnapshotTags(comparison.previous.tags)} -> current${formatSnapshotTags(comparison.current.tags)}`);
504
537
  }
538
+ if (comparison.previous.milestone || comparison.current.milestone) {
539
+ lines.push(` Lifecycle: previous${formatSnapshotMilestone(comparison.previous.milestone)} -> current${formatSnapshotMilestone(comparison.current.milestone)}`);
540
+ }
505
541
  if (comparison.improvements.length > 0) {
506
542
  lines.push(` Fixed: ${comparison.improvements.join(', ')}`);
507
543
  }
@@ -532,22 +568,23 @@ function exportTrendReport(dir) {
532
568
  '',
533
569
  '## Audit Snapshot History',
534
570
  '',
535
- '| Date | Tags | Score | Passed | Checks |',
536
- '|------|------|-------|--------|--------|',
571
+ '| Date | Milestone | Tags | Score | Passed | Checks |',
572
+ '|------|-----------|------|-------|--------|--------|',
537
573
  ];
538
574
 
539
575
  for (const entry of history) {
540
576
  const date = entry.createdAt?.split('T')[0] || '?';
577
+ const milestone = entry.milestone || '-';
541
578
  const tags = (entry.tags || []).length > 0 ? entry.tags.join(', ') : '-';
542
- lines.push(`| ${date} | ${tags} | ${entry.summary?.score ?? '?'}/100 | ${entry.summary?.passed ?? '?'} | ${entry.summary?.checkCount ?? '?'} |`);
579
+ lines.push(`| ${date} | ${milestone} | ${tags} | ${entry.summary?.score ?? '?'}/100 | ${entry.summary?.passed ?? '?'} | ${entry.summary?.checkCount ?? '?'} |`);
543
580
  }
544
581
 
545
582
  if (comparison) {
546
583
  lines.push('');
547
584
  lines.push('## Latest Comparison');
548
585
  lines.push('');
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)}`);
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)}`);
551
588
  lines.push(`- **Snapshot delta:** ${comparison.delta.score >= 0 ? '+' : ''}${comparison.delta.score} points`);
552
589
  lines.push(`- **Trend:** ${comparison.trend}`);
553
590
  if (comparison.improvements.length > 0) lines.push(`- **Fixed:** ${comparison.improvements.join(', ')}`);
@@ -981,6 +1018,9 @@ module.exports = {
981
1018
  writeSnapshotArtifact,
982
1019
  normalizeSnapshotTags,
983
1020
  formatSnapshotTags,
1021
+ normalizeSnapshotMilestone,
1022
+ formatSnapshotMilestone,
1023
+ SNAPSHOT_MILESTONES,
984
1024
  readSnapshotIndex,
985
1025
  getHistory,
986
1026
  compareLatest,
@@ -0,0 +1,299 @@
1
+ 'use strict';
2
+
3
+ const { getMcpPackPreflight } = require('./mcp-packs');
4
+
5
+ const DECISION_ORDER = { adopt: 0, defer: 1, ignore: 2 };
6
+
7
+ function unique(values) {
8
+ return [...new Set((values || []).filter(Boolean))];
9
+ }
10
+
11
+ function makeItem(item) {
12
+ return {
13
+ decision: item.decision,
14
+ kind: item.kind,
15
+ key: item.key,
16
+ label: item.label,
17
+ why: item.why,
18
+ evidence: unique(item.evidence),
19
+ prerequisites: unique(item.prerequisites),
20
+ expectedBenefit: item.expectedBenefit,
21
+ rollbackSafety: item.rollbackSafety,
22
+ };
23
+ }
24
+
25
+ function summarize(items = []) {
26
+ const counts = items.reduce((acc, item) => {
27
+ acc[item.decision] = (acc[item.decision] || 0) + 1;
28
+ return acc;
29
+ }, { adopt: 0, defer: 0, ignore: 0 });
30
+
31
+ return {
32
+ adopt: counts.adopt || 0,
33
+ defer: counts.defer || 0,
34
+ ignore: counts.ignore || 0,
35
+ label: `${counts.adopt || 0} adopt now / ${counts.defer || 0} defer / ${counts.ignore || 0} ignore`,
36
+ };
37
+ }
38
+
39
+ function inferMcpPackPrerequisites(pack, preflightEntry) {
40
+ const prerequisites = [];
41
+ if (preflightEntry && Array.isArray(preflightEntry.missingEnvVars) && preflightEntry.missingEnvVars.length > 0) {
42
+ prerequisites.push(`Provide required env vars: ${preflightEntry.missingEnvVars.join(', ')}`);
43
+ }
44
+
45
+ if (/Pass connection string as CLI argument/i.test(pack.adoption || '')) {
46
+ prerequisites.push('Provide a live connection string or DATABASE_URL before enabling this MCP pack.');
47
+ }
48
+
49
+ if (/Docker running locally/i.test(pack.adoption || '')) {
50
+ prerequisites.push('Docker must be running locally before this MCP pack is useful.');
51
+ }
52
+
53
+ if (/OAuth/i.test(pack.adoption || '')) {
54
+ prerequisites.push('Complete the OAuth or external auth setup before enabling this MCP pack in shared flows.');
55
+ }
56
+
57
+ if (/community-maintained/i.test(pack.adoption || '')) {
58
+ prerequisites.push('Review the package trust/update posture before enabling it in a wider team baseline.');
59
+ }
60
+
61
+ return unique(prerequisites);
62
+ }
63
+
64
+ function buildIgnoreItems(repoArchetype, operatingProfile, recommendedDomainPacks = []) {
65
+ const items = [];
66
+ const domainKeys = new Set((recommendedDomainPacks || []).map((pack) => pack.key));
67
+
68
+ if (operatingProfile.platformSupport.strategy === 'single-platform-baseline') {
69
+ items.push(makeItem({
70
+ decision: 'ignore',
71
+ kind: 'platform-expansion',
72
+ key: 'broad-platform-expansion',
73
+ label: 'Broad multi-platform expansion',
74
+ why: 'This repo should stabilize one governed primary platform before adding more AI surfaces.',
75
+ evidence: [
76
+ `Platform strategy: ${operatingProfile.platformSupport.strategy}`,
77
+ `Archetype: ${repoArchetype.label}`,
78
+ ],
79
+ prerequisites: [],
80
+ expectedBenefit: 'Prevents governance drift caused by widening the tool surface too early.',
81
+ rollbackSafety: 'Nothing is applied here; this is an intentional skip until the baseline matures.',
82
+ }));
83
+ }
84
+
85
+ if (repoArchetype.topology.key !== 'monorepo') {
86
+ items.push(makeItem({
87
+ decision: 'ignore',
88
+ kind: 'ci-shape',
89
+ key: 'workspace-pr-gate',
90
+ label: 'Workspace-aware PR gate',
91
+ why: 'Workspace-specific CI complexity does not match a non-monorepo repo shape.',
92
+ evidence: [
93
+ `Topology: ${repoArchetype.topology.label}`,
94
+ 'No monorepo-specific governance surface is required.',
95
+ ],
96
+ prerequisites: [],
97
+ expectedBenefit: 'Keeps the operating model lean and aligned to the real repo topology.',
98
+ rollbackSafety: 'This is a deliberate skip; no cleanup is required.',
99
+ }));
100
+ }
101
+
102
+ if (repoArchetype.riskProfile.key !== 'regulated' && !domainKeys.has('regulated-lite') && !domainKeys.has('security-focused')) {
103
+ items.push(makeItem({
104
+ decision: 'ignore',
105
+ kind: 'governance-pack',
106
+ key: 'regulated-lite',
107
+ label: 'Regulated-lite governance overhead',
108
+ why: 'The repo does not show regulated or security-heavy signals strong enough to justify this heavier rollout pack.',
109
+ evidence: [
110
+ `Risk posture: ${repoArchetype.riskProfile.label}`,
111
+ ],
112
+ prerequisites: [],
113
+ expectedBenefit: 'Preserves a lighter rollout model and avoids over-governing normal product repos.',
114
+ rollbackSafety: 'This is a no-op skip; the repo can adopt a heavier pack later if evidence changes.',
115
+ }));
116
+ }
117
+
118
+ if (repoArchetype.stackFamily.key !== 'mobile' && !domainKeys.has('mobile')) {
119
+ items.push(makeItem({
120
+ decision: 'ignore',
121
+ kind: 'verification',
122
+ key: 'mobile-release-loop',
123
+ label: 'Mobile release workflow extras',
124
+ why: 'Mobile-only analyze/build workflow overhead should not be forced onto a non-mobile repo.',
125
+ evidence: [
126
+ `Stack family: ${repoArchetype.stackFamily.label}`,
127
+ ],
128
+ prerequisites: [],
129
+ expectedBenefit: 'Avoids irrelevant workflow ceremony and keeps verification recommendations credible.',
130
+ rollbackSafety: 'No rollback is needed because the capability is being intentionally skipped.',
131
+ }));
132
+ }
133
+
134
+ return items.slice(0, 3);
135
+ }
136
+
137
+ function buildAdoptionAdvisor(options) {
138
+ const {
139
+ platform,
140
+ repoArchetype,
141
+ recommendedOperatingProfile,
142
+ recommendedDomainPacks = [],
143
+ recommendedMcpPacks = [],
144
+ env = {},
145
+ } = options || {};
146
+
147
+ const items = [];
148
+ const mcpPreflightByKey = new Map(
149
+ getMcpPackPreflight((recommendedMcpPacks || []).map((pack) => pack.key), env).map((entry) => [entry.key, entry])
150
+ );
151
+
152
+ items.push(makeItem({
153
+ decision: 'adopt',
154
+ kind: 'platform-strategy',
155
+ key: recommendedOperatingProfile.platformSupport.strategy,
156
+ label: `Platform strategy: ${recommendedOperatingProfile.platformSupport.strategy}`,
157
+ why: recommendedOperatingProfile.platformSupport.why,
158
+ evidence: recommendedOperatingProfile.platformSupport.evidence,
159
+ prerequisites: recommendedOperatingProfile.platformSupport.prerequisites,
160
+ expectedBenefit: recommendedOperatingProfile.platformSupport.expectedBenefit,
161
+ rollbackSafety: recommendedOperatingProfile.platformSupport.rollbackSafety,
162
+ }));
163
+
164
+ if (recommendedOperatingProfile.platformSupport.optionalExpansion) {
165
+ items.push(makeItem({
166
+ decision: 'defer',
167
+ kind: 'platform-expansion',
168
+ key: `secondary-${recommendedOperatingProfile.platformSupport.optionalExpansion}`,
169
+ label: `Add ${recommendedOperatingProfile.platformSupport.optionalExpansion} as a secondary review surface`,
170
+ why: 'A secondary platform could help later, but the repo should stabilize its primary governed posture first.',
171
+ evidence: recommendedOperatingProfile.platformSupport.evidence,
172
+ prerequisites: [
173
+ 'Capture tagged baseline and post-fix snapshots first.',
174
+ 'Keep Harmony stable across the currently active platform surface.',
175
+ ],
176
+ expectedBenefit: 'Adds a complementary advisory surface only after the core posture is already reliable.',
177
+ rollbackSafety: 'Secondary platform expansion is optional and can be removed without changing the app codebase.',
178
+ }));
179
+ }
180
+
181
+ items.push(makeItem({
182
+ decision: 'adopt',
183
+ kind: 'permission-profile',
184
+ key: recommendedOperatingProfile.permissionProfile.key,
185
+ label: `Permission profile: ${recommendedOperatingProfile.permissionProfile.label}`,
186
+ why: recommendedOperatingProfile.permissionProfile.why,
187
+ evidence: recommendedOperatingProfile.permissionProfile.evidence,
188
+ prerequisites: recommendedOperatingProfile.permissionProfile.prerequisites,
189
+ expectedBenefit: recommendedOperatingProfile.permissionProfile.expectedBenefit,
190
+ rollbackSafety: recommendedOperatingProfile.permissionProfile.rollbackSafety,
191
+ }));
192
+
193
+ items.push(makeItem({
194
+ decision: 'adopt',
195
+ kind: 'governance-pack',
196
+ key: recommendedOperatingProfile.governancePack.key,
197
+ label: `Governance pack: ${recommendedOperatingProfile.governancePack.label}`,
198
+ why: recommendedOperatingProfile.governancePack.why,
199
+ evidence: recommendedOperatingProfile.governancePack.evidence,
200
+ prerequisites: recommendedOperatingProfile.governancePack.prerequisites,
201
+ expectedBenefit: recommendedOperatingProfile.governancePack.expectedBenefit,
202
+ rollbackSafety: recommendedOperatingProfile.governancePack.rollbackSafety,
203
+ }));
204
+
205
+ items.push(makeItem({
206
+ decision: 'adopt',
207
+ kind: 'hook-set',
208
+ key: 'starter-hooks',
209
+ label: `Starter hook set: ${recommendedOperatingProfile.hooks.map((hook) => hook.key).join(', ')}`,
210
+ why: 'These hooks are the lowest-friction set that matches the repo trust boundary, logging needs, and review posture.',
211
+ evidence: unique(recommendedOperatingProfile.hooks.flatMap((hook) => hook.evidence || []).slice(0, 5)),
212
+ prerequisites: unique(recommendedOperatingProfile.hooks.flatMap((hook) => hook.prerequisites || [])),
213
+ expectedBenefit: 'Adds secret protection, trust-boundary checks, and durable change evidence without widening the product surface.',
214
+ rollbackSafety: 'Each hook can be removed independently from repo settings if it proves noisy.',
215
+ }));
216
+
217
+ items.push(makeItem({
218
+ decision: 'adopt',
219
+ kind: 'verification-loop',
220
+ key: recommendedOperatingProfile.verification.key,
221
+ label: `Verification loop: ${recommendedOperatingProfile.verification.label}`,
222
+ why: recommendedOperatingProfile.verification.why,
223
+ evidence: recommendedOperatingProfile.verification.evidence,
224
+ prerequisites: recommendedOperatingProfile.verification.prerequisites,
225
+ expectedBenefit: recommendedOperatingProfile.verification.expectedBenefit,
226
+ rollbackSafety: recommendedOperatingProfile.verification.rollbackSafety,
227
+ }));
228
+
229
+ items.push(makeItem({
230
+ decision: 'adopt',
231
+ kind: 'ci-shape',
232
+ key: recommendedOperatingProfile.ciShape.key,
233
+ label: `CI shape: ${recommendedOperatingProfile.ciShape.label}`,
234
+ why: recommendedOperatingProfile.ciShape.why,
235
+ evidence: recommendedOperatingProfile.ciShape.evidence,
236
+ prerequisites: recommendedOperatingProfile.ciShape.prerequisites,
237
+ expectedBenefit: recommendedOperatingProfile.ciShape.expectedBenefit,
238
+ rollbackSafety: recommendedOperatingProfile.ciShape.rollbackSafety,
239
+ }));
240
+
241
+ for (const pack of recommendedDomainPacks) {
242
+ items.push(makeItem({
243
+ decision: 'adopt',
244
+ kind: 'domain-pack',
245
+ key: pack.key,
246
+ label: `Domain pack: ${pack.label}`,
247
+ why: pack.useWhen,
248
+ evidence: unique([...(pack.matchReasons || []), `Archetype: ${repoArchetype.label}`]).slice(0, 5),
249
+ prerequisites: [
250
+ `Review the pack modules before applying them: ${(pack.recommendedModules || []).slice(0, 4).join(', ') || 'no starter modules listed'}`,
251
+ ],
252
+ expectedBenefit: (pack.benchmarkFocus || []).length > 0
253
+ ? `Improves Nerviq's relevance around ${pack.benchmarkFocus.slice(0, 3).join(', ')}.`
254
+ : 'Makes Nerviq recommendations more domain-aware for this repo.',
255
+ rollbackSafety: 'Domain packs are additive guidance. You can remove a pack from the recommended stack without rewriting the repo.',
256
+ }));
257
+ }
258
+
259
+ if (platform === 'claude') {
260
+ for (const pack of recommendedMcpPacks) {
261
+ const preflight = mcpPreflightByKey.get(pack.key);
262
+ const prerequisites = inferMcpPackPrerequisites(pack, preflight);
263
+ const decision = prerequisites.length > 0 ? 'defer' : 'adopt';
264
+ items.push(makeItem({
265
+ decision,
266
+ kind: 'mcp-pack',
267
+ key: pack.key,
268
+ label: `MCP pack: ${pack.label}`,
269
+ why: pack.useWhen,
270
+ evidence: unique([pack.adoption, `Repo archetype: ${repoArchetype.label}`]).slice(0, 4),
271
+ prerequisites,
272
+ expectedBenefit: pack.adoption,
273
+ rollbackSafety: 'MCP packs are optional integrations; they can be added or removed from settings without changing application source code.',
274
+ }));
275
+ }
276
+ }
277
+
278
+ items.push(...buildIgnoreItems(repoArchetype, recommendedOperatingProfile, recommendedDomainPacks));
279
+
280
+ const sorted = items
281
+ .sort((a, b) => {
282
+ const decisionDiff = (DECISION_ORDER[a.decision] || 99) - (DECISION_ORDER[b.decision] || 99);
283
+ if (decisionDiff !== 0) return decisionDiff;
284
+ return a.label.localeCompare(b.label);
285
+ })
286
+ .map((item, index) => ({
287
+ priority: index + 1,
288
+ ...item,
289
+ }));
290
+
291
+ return {
292
+ summary: summarize(sorted),
293
+ items: sorted,
294
+ };
295
+ }
296
+
297
+ module.exports = {
298
+ buildAdoptionAdvisor,
299
+ };
@@ -19,10 +19,11 @@
19
19
  * Check ID prefix: AD-
20
20
  */
21
21
 
22
- const { containsEmbeddedSecret } = require('../secret-patterns');
23
- const { attachSourceUrls } = require('../source-urls');
24
- const { buildStackChecks } = require('../stack-checks');
25
- const { isApiProject, isDatabaseProject, isAuthProject, isMonitoringRelevant } = require('../supplemental-checks');
22
+ const { containsEmbeddedSecret } = require('../secret-patterns');
23
+ const { attachSourceUrls } = require('../source-urls');
24
+ const { buildStackChecks } = require('../stack-checks');
25
+ const { isApiProject, isDatabaseProject, isAuthProject, isMonitoringRelevant } = require('../supplemental-checks');
26
+ const { hasCostBudgetOrUsageTracking } = require('../cost-tracking');
26
27
 
27
28
  const FILLER_PATTERNS = [
28
29
  /\bbe helpful\b/i,
@@ -1724,13 +1725,17 @@ const AIDER_TECHNIQUES = {
1724
1725
  fix: 'Set `cache-prompts: true` in .aider.conf.yml to reduce API costs.',
1725
1726
  template: 'aider-conf-yml', file: () => '.aider.conf.yml', line: () => null,
1726
1727
  },
1727
- aiderCostBudgetDefined: {
1728
- id: 'AD-T48', name: 'AI cost budget or usage limits documented',
1729
- check: (ctx) => { const docs = conventionContent(ctx) + (ctx.fileContent('README.md') || ''); if (!docs.trim()) return null; return /cost.{0,15}budget|spending.{0,15}limit|usage.{0,15}limit/i.test(docs); },
1730
- impact: 'low', rating: 2, category: 'cost-optimization',
1731
- fix: 'Document AI cost budget in README.md.',
1732
- template: null, file: () => 'README.md', line: () => null,
1733
- },
1728
+ aiderCostBudgetDefined: {
1729
+ id: 'AD-T48', name: 'AI cost budget or per-run usage tracking documented',
1730
+ check: (ctx) => {
1731
+ const docs = conventionContent(ctx) + (ctx.fileContent('README.md') || '');
1732
+ if (!docs.trim() && !hasCostBudgetOrUsageTracking('', ctx)) return null;
1733
+ return hasCostBudgetOrUsageTracking(docs, ctx);
1734
+ },
1735
+ impact: 'low', rating: 2, category: 'cost-optimization',
1736
+ fix: 'Document AI cost guardrails or per-run usage tracking so Aider usage is visible run by run.',
1737
+ template: null, file: () => 'README.md', line: () => null,
1738
+ },
1734
1739
 
1735
1740
  // ============================================================
1736
1741
  // === PYTHON STACK CHECKS (category: 'python') ===============