@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.
- package/README.md +97 -19
- package/bin/cli.js +618 -182
- package/package.json +2 -2
- package/src/activity.js +49 -9
- package/src/adoption-advisor.js +299 -0
- package/src/aider/techniques.js +16 -11
- package/src/analyze.js +128 -0
- package/src/anti-patterns.js +13 -0
- package/src/audit.js +97 -22
- package/src/behavioral-drift.js +801 -0
- package/src/continuous-ops.js +681 -0
- package/src/cost-tracking.js +61 -0
- package/src/cursor/techniques.js +17 -12
- package/src/deep-review.js +83 -0
- package/src/diff-only.js +280 -0
- package/src/doctor.js +118 -55
- package/src/governance.js +59 -43
- package/src/hook-validation.js +342 -0
- package/src/index.js +5 -0
- package/src/integrations.js +42 -5
- package/src/mcp-validation.js +337 -0
- package/src/opencode/techniques.js +12 -7
- package/src/operating-profile.js +574 -0
- package/src/org.js +97 -13
- package/src/plans.js +192 -8
- package/src/platform-change-manifest.js +86 -0
- package/src/policy-layers.js +210 -0
- package/src/profiles.js +4 -1
- package/src/prompt-injection.js +74 -0
- package/src/repo-archetype.js +386 -0
- package/src/setup.js +34 -0
- package/src/source-urls.js +132 -132
- package/src/supplemental-checks.js +13 -12
- package/src/techniques/api.js +407 -0
- package/src/techniques/automation.js +316 -0
- package/src/techniques/compliance.js +257 -0
- package/src/techniques/hygiene.js +294 -0
- package/src/techniques/instructions.js +243 -0
- package/src/techniques/observability.js +226 -0
- package/src/techniques/optimization.js +142 -0
- package/src/techniques/quality.js +317 -0
- package/src/techniques/security.js +237 -0
- package/src/techniques/shared.js +443 -0
- package/src/techniques/stacks.js +2294 -0
- package/src/techniques/tools.js +106 -0
- package/src/techniques/workflow.js +413 -0
- package/src/techniques.js +78 -5607
- package/src/watch.js +18 -0
- 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.
|
|
4
|
-
"description": "The intelligent nervous system for AI coding agents — 2,
|
|
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
|
+
};
|
package/src/aider/techniques.js
CHANGED
|
@@ -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
|
|
1729
|
-
check: (ctx) => {
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
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') ===============
|