@nerviq/cli 1.11.0 → 1.13.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 +216 -124
- package/bin/cli.js +620 -183
- package/package.json +3 -2
- package/src/activity.js +49 -9
- package/src/adoption-advisor.js +299 -0
- package/src/aider/freshness.js +65 -20
- package/src/aider/techniques.js +16 -11
- package/src/analyze.js +128 -0
- package/src/anti-patterns.js +13 -0
- package/src/audit/instruction-files.js +180 -0
- package/src/audit/recommendations.js +531 -0
- package/src/audit.js +53 -681
- package/src/behavioral-drift.js +801 -0
- package/src/codex/freshness.js +84 -25
- package/src/continuous-ops.js +681 -0
- package/src/copilot/freshness.js +57 -20
- package/src/cost-tracking.js +61 -0
- package/src/cursor/freshness.js +65 -20
- 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/freshness.js +74 -21
- package/src/gemini/freshness.js +66 -21
- 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-server.js +95 -59
- package/src/mcp-validation.js +337 -0
- package/src/opencode/freshness.js +66 -21
- 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/analysis.js +619 -0
- package/src/setup/runtime.js +172 -0
- package/src/setup.js +62 -748
- 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/freshness.js +36 -21
- package/src/windsurf/techniques.js +17 -12
package/src/copilot/freshness.js
CHANGED
|
@@ -27,18 +27,39 @@ const P0_SOURCES = [
|
|
|
27
27
|
stalenessThresholdDays: 30,
|
|
28
28
|
verifiedAt: '2026-04-07',
|
|
29
29
|
},
|
|
30
|
-
{
|
|
31
|
-
key: 'copilot-cloud-agent-docs',
|
|
32
|
-
label: 'Copilot Coding Agent',
|
|
33
|
-
url: 'https://docs.github.com/en/copilot/concepts/agents/coding-agent/about-coding-agent',
|
|
34
|
-
stalenessThresholdDays: 14,
|
|
35
|
-
verifiedAt: '2026-04-07',
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
key: 'copilot-
|
|
39
|
-
label: 'Copilot
|
|
40
|
-
url: 'https://docs.github.com/en/copilot/
|
|
41
|
-
stalenessThresholdDays:
|
|
30
|
+
{
|
|
31
|
+
key: 'copilot-cloud-agent-docs',
|
|
32
|
+
label: 'Copilot Coding Agent',
|
|
33
|
+
url: 'https://docs.github.com/en/copilot/concepts/agents/coding-agent/about-coding-agent',
|
|
34
|
+
stalenessThresholdDays: 14,
|
|
35
|
+
verifiedAt: '2026-04-07',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
key: 'copilot-cli-docs',
|
|
39
|
+
label: 'Copilot CLI Documentation',
|
|
40
|
+
url: 'https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli',
|
|
41
|
+
stalenessThresholdDays: 14,
|
|
42
|
+
verifiedAt: '2026-04-10',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
key: 'copilot-model-lts-docs',
|
|
46
|
+
label: 'Copilot Base & LTS Models',
|
|
47
|
+
url: 'https://docs.github.com/en/copilot/concepts/fallback-and-lts-models',
|
|
48
|
+
stalenessThresholdDays: 14,
|
|
49
|
+
verifiedAt: '2026-04-10',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
key: 'copilot-cli-custom-agents-docs',
|
|
53
|
+
label: 'Copilot CLI Custom Agents',
|
|
54
|
+
url: 'https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/create-custom-agents-for-cli',
|
|
55
|
+
stalenessThresholdDays: 30,
|
|
56
|
+
verifiedAt: '2026-04-10',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
key: 'copilot-prompt-files-docs',
|
|
60
|
+
label: 'Copilot Prompt Files Documentation',
|
|
61
|
+
url: 'https://docs.github.com/en/copilot/customizing-copilot/adding-custom-instructions-for-github-copilot#creating-prompt-files',
|
|
62
|
+
stalenessThresholdDays: 30,
|
|
42
63
|
verifiedAt: '2026-04-07',
|
|
43
64
|
},
|
|
44
65
|
{
|
|
@@ -123,14 +144,30 @@ const PROPAGATION_CHECKLIST = [
|
|
|
123
144
|
'src/copilot/setup.js — update content exclusions guide',
|
|
124
145
|
],
|
|
125
146
|
},
|
|
126
|
-
{
|
|
127
|
-
trigger: 'Organization policy change or new policy type',
|
|
128
|
-
targets: [
|
|
129
|
-
'src/copilot/techniques.js — update organization checks (CP-F01..CP-F05)',
|
|
130
|
-
'src/copilot/governance.js — update enterprise-managed profile',
|
|
131
|
-
],
|
|
132
|
-
},
|
|
133
|
-
|
|
147
|
+
{
|
|
148
|
+
trigger: 'Organization policy change or new policy type',
|
|
149
|
+
targets: [
|
|
150
|
+
'src/copilot/techniques.js — update organization checks (CP-F01..CP-F05)',
|
|
151
|
+
'src/copilot/governance.js — update enterprise-managed profile',
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
trigger: 'Copilot CLI behavior change (CLI custom agents, hooks, memory, prompt layering)',
|
|
156
|
+
targets: [
|
|
157
|
+
'src/copilot/techniques.js — update CLI-agent and instruction-surface checks',
|
|
158
|
+
'src/copilot/setup.js — update starter guidance when CLI agent or hook behavior changes',
|
|
159
|
+
'src/source-urls.js — refresh Copilot source mappings for CLI-specific surfaces',
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
trigger: 'Copilot model / fallback / LTS policy change',
|
|
164
|
+
targets: [
|
|
165
|
+
'src/copilot/governance.js — update model-selection and enterprise policy caveats',
|
|
166
|
+
'src/copilot/techniques.js — update trust/availability assumptions tied to model fallback behavior',
|
|
167
|
+
'src/source-urls.js — refresh Copilot model and maturity source mappings',
|
|
168
|
+
],
|
|
169
|
+
},
|
|
170
|
+
];
|
|
134
171
|
|
|
135
172
|
/**
|
|
136
173
|
* Release gate: check if all P0 sources are within staleness threshold.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const BUDGET_PATTERNS = [
|
|
2
|
+
/\bcost.{0,15}budget\b/i,
|
|
3
|
+
/\bmonthly.{0,15}budget\b/i,
|
|
4
|
+
/\bspending.{0,15}limit\b/i,
|
|
5
|
+
/\busage.{0,15}limit\b/i,
|
|
6
|
+
/\bbudget guardrails?\b/i,
|
|
7
|
+
/\bspend cap\b/i,
|
|
8
|
+
/\bquota\b/i,
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const USAGE_TRACKING_PATTERNS = [
|
|
12
|
+
/\b(per[- ]run|per agent run|per session|per request)\b.{0,40}\b(cost|usage|token|spend|billing)\b/i,
|
|
13
|
+
/\b(cost|usage|token|spend|billing)\b.{0,40}\b(per[- ]run|per agent run|per session|per request)\b/i,
|
|
14
|
+
/\b(track|tracking|monitor|monitoring|log|logging|meter|metering|report|reporting)\b.{0,40}\b(cost|usage|token|spend|billing)\b/i,
|
|
15
|
+
/\b(cost|usage|token|spend|billing)\b.{0,40}\b(track|tracking|monitor|monitoring|log|logging|meter|metering|report|reporting)\b/i,
|
|
16
|
+
/\btoken metrics?\b/i,
|
|
17
|
+
/\busage dashboard\b/i,
|
|
18
|
+
/\bcost dashboard\b/i,
|
|
19
|
+
/\b(prompt|completion|input|output)\s+tokens?\b.{0,20}\b(log|track|monitor|report)\b/i,
|
|
20
|
+
/\b(langfuse|langsmith|helicone|openlit|lunary|braintrust|phoenix)\b/i,
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const USAGE_TRACKING_FILE_PATTERNS = [
|
|
24
|
+
/\blangfuse\b/i,
|
|
25
|
+
/\blangsmith\b/i,
|
|
26
|
+
/\bhelicone\b/i,
|
|
27
|
+
/\bopenlit\b/i,
|
|
28
|
+
/\blunary\b/i,
|
|
29
|
+
/\bbraintrust\b/i,
|
|
30
|
+
/\bphoenix\b/i,
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
function matchesAny(text, patterns) {
|
|
34
|
+
const normalized = String(text || '');
|
|
35
|
+
return patterns.some((pattern) => {
|
|
36
|
+
pattern.lastIndex = 0;
|
|
37
|
+
return pattern.test(normalized);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function hasUsageTrackingDependency(ctx) {
|
|
42
|
+
if (!ctx || typeof ctx.projectDependencies !== 'function') return false;
|
|
43
|
+
const deps = Object.keys(ctx.projectDependencies() || {});
|
|
44
|
+
return deps.some((name) => USAGE_TRACKING_FILE_PATTERNS.some((pattern) => pattern.test(name)));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function hasUsageTrackingArtifacts(ctx) {
|
|
48
|
+
if (!ctx || !Array.isArray(ctx.files)) return false;
|
|
49
|
+
return ctx.files.some((filePath) => USAGE_TRACKING_FILE_PATTERNS.some((pattern) => pattern.test(String(filePath || ''))));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function hasCostBudgetOrUsageTracking(text, ctx = null) {
|
|
53
|
+
return matchesAny(text, BUDGET_PATTERNS) ||
|
|
54
|
+
matchesAny(text, USAGE_TRACKING_PATTERNS) ||
|
|
55
|
+
hasUsageTrackingDependency(ctx) ||
|
|
56
|
+
hasUsageTrackingArtifacts(ctx);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = {
|
|
60
|
+
hasCostBudgetOrUsageTracking,
|
|
61
|
+
};
|
package/src/cursor/freshness.js
CHANGED
|
@@ -41,18 +41,39 @@ const P0_SOURCES = [
|
|
|
41
41
|
stalenessThresholdDays: 14,
|
|
42
42
|
verifiedAt: '2026-04-07',
|
|
43
43
|
},
|
|
44
|
-
{
|
|
45
|
-
key: 'cursor-automations',
|
|
46
|
-
label: 'Automations Documentation',
|
|
47
|
-
url: 'https://cursor.com/docs/cloud-agent/automations',
|
|
48
|
-
stalenessThresholdDays: 14,
|
|
49
|
-
verifiedAt: '2026-04-07',
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
key: 'cursor-
|
|
53
|
-
label: '
|
|
54
|
-
url: 'https://cursor.com/
|
|
55
|
-
stalenessThresholdDays:
|
|
44
|
+
{
|
|
45
|
+
key: 'cursor-automations',
|
|
46
|
+
label: 'Automations Documentation',
|
|
47
|
+
url: 'https://cursor.com/docs/cloud-agent/automations',
|
|
48
|
+
stalenessThresholdDays: 14,
|
|
49
|
+
verifiedAt: '2026-04-07',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
key: 'cursor-agent-modes',
|
|
53
|
+
label: 'Cursor Agent Modes',
|
|
54
|
+
url: 'https://docs.cursor.com/en/chat/agent',
|
|
55
|
+
stalenessThresholdDays: 14,
|
|
56
|
+
verifiedAt: '2026-04-10',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
key: 'cursor-models-docs',
|
|
60
|
+
label: 'Cursor Models & Auto Selection',
|
|
61
|
+
url: 'https://docs.cursor.com/models',
|
|
62
|
+
stalenessThresholdDays: 14,
|
|
63
|
+
verifiedAt: '2026-04-10',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
key: 'cursor-cli-docs',
|
|
67
|
+
label: 'Cursor CLI Usage',
|
|
68
|
+
url: 'https://docs.cursor.com/en/cli/using',
|
|
69
|
+
stalenessThresholdDays: 30,
|
|
70
|
+
verifiedAt: '2026-04-10',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
key: 'cursor-bugbot',
|
|
74
|
+
label: 'BugBot Documentation',
|
|
75
|
+
url: 'https://cursor.com/docs/bugbot',
|
|
76
|
+
stalenessThresholdDays: 30,
|
|
56
77
|
verifiedAt: '2026-04-07',
|
|
57
78
|
},
|
|
58
79
|
{
|
|
@@ -140,14 +161,38 @@ const PROPAGATION_CHECKLIST = [
|
|
|
140
161
|
'src/cursor/deep-review.js — update trust class detection',
|
|
141
162
|
],
|
|
142
163
|
},
|
|
143
|
-
{
|
|
144
|
-
trigger: 'Design Mode feature update',
|
|
145
|
-
targets: [
|
|
146
|
-
'src/cursor/setup.js — update Design Mode guide template',
|
|
147
|
-
'src/cursor/techniques.js — update CU-L01 modern features check',
|
|
148
|
-
],
|
|
149
|
-
},
|
|
150
|
-
|
|
164
|
+
{
|
|
165
|
+
trigger: 'Design Mode feature update',
|
|
166
|
+
targets: [
|
|
167
|
+
'src/cursor/setup.js — update Design Mode guide template',
|
|
168
|
+
'src/cursor/techniques.js — update CU-L01 modern features check',
|
|
169
|
+
],
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
trigger: 'Cursor mode behavior change (Agent/Ask/Custom, auto-run, auto-fix, tool scopes)',
|
|
173
|
+
targets: [
|
|
174
|
+
'src/cursor/techniques.js — update agent-mode and autonomy checks',
|
|
175
|
+
'src/cursor/governance.js — update mode-related permission caveats',
|
|
176
|
+
'src/source-urls.js — refresh Cursor mode source mappings',
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
trigger: 'Cursor model catalog or auto-selection change',
|
|
181
|
+
targets: [
|
|
182
|
+
'src/cursor/techniques.js — update model-awareness and cost/trust assumptions',
|
|
183
|
+
'src/cursor/governance.js — update privacy and model-selection caveats',
|
|
184
|
+
'src/source-urls.js — refresh Cursor model source mappings',
|
|
185
|
+
],
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
trigger: 'Cursor CLI behavior change',
|
|
189
|
+
targets: [
|
|
190
|
+
'src/cursor/setup.js — update CLI-oriented starter guidance',
|
|
191
|
+
'src/cursor/techniques.js — update CLI/rules expectations where relevant',
|
|
192
|
+
'src/source-urls.js — refresh Cursor CLI source mappings',
|
|
193
|
+
],
|
|
194
|
+
},
|
|
195
|
+
];
|
|
151
196
|
|
|
152
197
|
/**
|
|
153
198
|
* Release gate: check if all P0 sources are within staleness threshold.
|
package/src/cursor/techniques.js
CHANGED
|
@@ -14,11 +14,12 @@
|
|
|
14
14
|
const os = require('os');
|
|
15
15
|
const path = require('path');
|
|
16
16
|
const { CursorProjectContext } = require('./context');
|
|
17
|
-
const { EMBEDDED_SECRET_PATTERNS, containsEmbeddedSecret } = require('../secret-patterns');
|
|
18
|
-
const { attachSourceUrls } = require('../source-urls');
|
|
19
|
-
const { buildStackChecks } = require('../stack-checks');
|
|
20
|
-
const { isApiProject, isDatabaseProject, isAuthProject, isMonitoringRelevant } = require('../supplemental-checks');
|
|
21
|
-
const {
|
|
17
|
+
const { EMBEDDED_SECRET_PATTERNS, containsEmbeddedSecret } = require('../secret-patterns');
|
|
18
|
+
const { attachSourceUrls } = require('../source-urls');
|
|
19
|
+
const { buildStackChecks } = require('../stack-checks');
|
|
20
|
+
const { isApiProject, isDatabaseProject, isAuthProject, isMonitoringRelevant } = require('../supplemental-checks');
|
|
21
|
+
const { hasCostBudgetOrUsageTracking } = require('../cost-tracking');
|
|
22
|
+
const { validateMdcFrontmatter, validateMcpEnvVars } = require('./config-parser');
|
|
22
23
|
|
|
23
24
|
// ─── Shared helpers ─────────────────────────────────────────────────────────
|
|
24
25
|
|
|
@@ -2211,13 +2212,17 @@ const CURSOR_TECHNIQUES = {
|
|
|
2211
2212
|
fix: 'Enable prompt caching in MCP configuration or document caching strategy for repeated prompts.',
|
|
2212
2213
|
template: null, file: () => '.cursor/mcp.json', line: () => null,
|
|
2213
2214
|
},
|
|
2214
|
-
cursorCostBudgetDefined: {
|
|
2215
|
-
id: 'CU-T48', name: 'AI cost budget or usage
|
|
2216
|
-
check: (ctx) => {
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2215
|
+
cursorCostBudgetDefined: {
|
|
2216
|
+
id: 'CU-T48', name: 'AI cost budget or per-run usage tracking documented',
|
|
2217
|
+
check: (ctx) => {
|
|
2218
|
+
const docs = docsBundle(ctx) + (ctx.fileContent('CLAUDE.md') || '');
|
|
2219
|
+
if (!docs.trim() && !hasCostBudgetOrUsageTracking('', ctx)) return null;
|
|
2220
|
+
return hasCostBudgetOrUsageTracking(docs, ctx);
|
|
2221
|
+
},
|
|
2222
|
+
impact: 'low', rating: 2, category: 'cost-optimization',
|
|
2223
|
+
fix: 'Document AI cost guardrails or per-run usage tracking in README.md or Cursor rules so spend is observable, not guessed.',
|
|
2224
|
+
template: null, file: () => 'README.md', line: () => null,
|
|
2225
|
+
},
|
|
2221
2226
|
|
|
2222
2227
|
// ============================================================
|
|
2223
2228
|
// === PYTHON STACK CHECKS (category: 'python') ===============
|
package/src/deep-review.js
CHANGED
|
@@ -11,6 +11,15 @@ const { execFileSync, execSync } = require('child_process');
|
|
|
11
11
|
const { ProjectContext } = require('./context');
|
|
12
12
|
const { STACKS } = require('./techniques');
|
|
13
13
|
const { redactEmbeddedSecrets } = require('./secret-patterns');
|
|
14
|
+
const {
|
|
15
|
+
analyzeBehavioralDrift,
|
|
16
|
+
compareBehavioralLatest,
|
|
17
|
+
formatBehavioralCompare,
|
|
18
|
+
formatBehavioralHistory,
|
|
19
|
+
formatBehavioralReport,
|
|
20
|
+
getBehavioralHistory,
|
|
21
|
+
writeBehavioralSnapshot,
|
|
22
|
+
} = require('./behavioral-drift');
|
|
14
23
|
|
|
15
24
|
const COLORS = {
|
|
16
25
|
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
@@ -236,7 +245,81 @@ async function callClaudeCode(prompt) {
|
|
|
236
245
|
});
|
|
237
246
|
}
|
|
238
247
|
|
|
248
|
+
function renderBehavioralJson(options) {
|
|
249
|
+
if (options.compareView) {
|
|
250
|
+
const comparison = compareBehavioralLatest(options.dir);
|
|
251
|
+
console.log(JSON.stringify(comparison || {
|
|
252
|
+
mode: 'behavioral-drift',
|
|
253
|
+
message: 'Behavioral compare needs two behavioral snapshots.',
|
|
254
|
+
historyCount: getBehavioralHistory(options.dir, 20).length,
|
|
255
|
+
}, null, 2));
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (options.historyView) {
|
|
260
|
+
console.log(JSON.stringify({
|
|
261
|
+
mode: 'behavioral-drift',
|
|
262
|
+
history: getBehavioralHistory(options.dir, 20),
|
|
263
|
+
comparison: compareBehavioralLatest(options.dir),
|
|
264
|
+
}, null, 2));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const report = analyzeBehavioralDrift(options.dir);
|
|
269
|
+
let snapshotArtifact = null;
|
|
270
|
+
if (options.snapshot) {
|
|
271
|
+
snapshotArtifact = writeBehavioralSnapshot(options.dir, report, {
|
|
272
|
+
tags: options.snapshotTags,
|
|
273
|
+
milestone: options.snapshotMilestone,
|
|
274
|
+
sourceCommand: 'deep-review --behavioral',
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
console.log(JSON.stringify({
|
|
279
|
+
...report,
|
|
280
|
+
snapshotArtifact,
|
|
281
|
+
}, null, 2));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function runBehavioralReview(options) {
|
|
285
|
+
if (options.json) {
|
|
286
|
+
renderBehavioralJson(options);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (options.compareView) {
|
|
291
|
+
console.log('');
|
|
292
|
+
console.log(formatBehavioralCompare(options.dir));
|
|
293
|
+
console.log('');
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (options.historyView) {
|
|
298
|
+
console.log('');
|
|
299
|
+
console.log(formatBehavioralHistory(options.dir));
|
|
300
|
+
console.log('');
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const report = analyzeBehavioralDrift(options.dir);
|
|
305
|
+
let snapshotArtifact = null;
|
|
306
|
+
if (options.snapshot) {
|
|
307
|
+
snapshotArtifact = writeBehavioralSnapshot(options.dir, report, {
|
|
308
|
+
tags: options.snapshotTags,
|
|
309
|
+
milestone: options.snapshotMilestone,
|
|
310
|
+
sourceCommand: 'deep-review --behavioral',
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
process.stdout.write(formatBehavioralReport(report, { snapshotArtifact }));
|
|
315
|
+
}
|
|
316
|
+
|
|
239
317
|
async function deepReview(options) {
|
|
318
|
+
if (options.behavioral) {
|
|
319
|
+
runBehavioralReview(options);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
240
323
|
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
241
324
|
const hasClaude = hasClaudeCode();
|
|
242
325
|
|
package/src/diff-only.js
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { spawnSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
const COLORS = {
|
|
7
|
+
reset: '\x1b[0m',
|
|
8
|
+
bold: '\x1b[1m',
|
|
9
|
+
dim: '\x1b[2m',
|
|
10
|
+
red: '\x1b[31m',
|
|
11
|
+
green: '\x1b[32m',
|
|
12
|
+
yellow: '\x1b[33m',
|
|
13
|
+
blue: '\x1b[36m',
|
|
14
|
+
magenta: '\x1b[35m',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const WEIGHTS = { critical: 15, high: 10, medium: 5, low: 2 };
|
|
18
|
+
const IMPACT_ORDER = { critical: 3, high: 2, medium: 1, low: 0 };
|
|
19
|
+
|
|
20
|
+
const CATEGORY_HINTS = [
|
|
21
|
+
{
|
|
22
|
+
pattern: /^(CLAUDE\.md|\.claude\/)/i,
|
|
23
|
+
categories: ['memory', 'workflow', 'security', 'automation', 'prompting', 'features', 'skills', 'agents', 'review', 'tools', 'git', 'quality'],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
pattern: /^(AGENTS\.md|\.codex\/)/i,
|
|
27
|
+
categories: ['memory', 'workflow', 'security', 'automation', 'prompting', 'features', 'skills', 'agents', 'review', 'tools', 'git', 'local'],
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
pattern: /^(GEMINI\.md|\.gemini\/)/i,
|
|
31
|
+
categories: ['memory', 'workflow', 'security', 'automation', 'prompting', 'features', 'skills', 'agents', 'review', 'tools'],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
pattern: /^(\.cursor\/|\.cursorrules$)/i,
|
|
35
|
+
categories: ['workflow', 'security', 'automation', 'prompting', 'features', 'tools', 'review'],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
pattern: /^(\.windsurf\/|\.windsurfrules$|\.cascadeignore$)/i,
|
|
39
|
+
categories: ['workflow', 'security', 'automation', 'prompting', 'features', 'tools', 'review'],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
pattern: /^(\.github\/copilot-instructions\.md|\.github\/instructions\/|\.github\/prompts\/|\.vscode\/mcp\.json$)/i,
|
|
43
|
+
categories: ['workflow', 'prompting', 'tools', 'review', 'devops'],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
pattern: /^(opencode\.jsonc?$|\.opencode\/)/i,
|
|
47
|
+
categories: ['workflow', 'security', 'automation', 'prompting', 'features', 'skills', 'agents', 'tools', 'review'],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
pattern: /^\.mcp\.json$/i,
|
|
51
|
+
categories: ['tools', 'security'],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
pattern: /^(\.gitignore$|package\.json$|pyproject\.toml$|requirements.*\.txt$|\.github\/workflows\/)/i,
|
|
55
|
+
categories: ['hygiene', 'devops', 'security', 'quality', 'tools'],
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
function c(text, color) {
|
|
60
|
+
return `${COLORS[color] || ''}${text}${COLORS.reset}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function normalizeRelativePath(filePath) {
|
|
64
|
+
return String(filePath || '').replace(/\\/g, '/').replace(/^\.\//, '');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function runGit(dir, args) {
|
|
68
|
+
return spawnSync('git', args, {
|
|
69
|
+
cwd: dir,
|
|
70
|
+
encoding: 'utf8',
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function collectOutputLines(result) {
|
|
75
|
+
return `${result.stdout || ''}`
|
|
76
|
+
.split(/\r?\n/)
|
|
77
|
+
.map((line) => normalizeRelativePath(line.trim()))
|
|
78
|
+
.filter(Boolean);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function uniquePaths(paths) {
|
|
82
|
+
return [...new Set((paths || []).map(normalizeRelativePath).filter(Boolean))].sort();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getRangeOptions(options = {}) {
|
|
86
|
+
const base = options.diffBase ||
|
|
87
|
+
process.env.NERVIQ_DIFF_BASE ||
|
|
88
|
+
process.env.GITHUB_BASE_SHA ||
|
|
89
|
+
process.env.GIT_BASE_SHA ||
|
|
90
|
+
null;
|
|
91
|
+
const head = options.diffHead ||
|
|
92
|
+
process.env.NERVIQ_DIFF_HEAD ||
|
|
93
|
+
process.env.GITHUB_SHA ||
|
|
94
|
+
process.env.GIT_HEAD_SHA ||
|
|
95
|
+
'HEAD';
|
|
96
|
+
return { base, head };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function getChangedFiles(dir, options = {}) {
|
|
100
|
+
const repoCheck = runGit(dir, ['rev-parse', '--is-inside-work-tree']);
|
|
101
|
+
if (repoCheck.status !== 0) {
|
|
102
|
+
return {
|
|
103
|
+
mode: 'unavailable',
|
|
104
|
+
changedFiles: [],
|
|
105
|
+
message: 'Diff-only mode requires a git repository.',
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const { base, head } = getRangeOptions(options);
|
|
110
|
+
if (base) {
|
|
111
|
+
const rangeResult = runGit(dir, ['diff', '--name-only', '--diff-filter=ACMRTUXB', `${base}..${head}`]);
|
|
112
|
+
if (rangeResult.status === 0) {
|
|
113
|
+
return {
|
|
114
|
+
mode: 'range',
|
|
115
|
+
base,
|
|
116
|
+
head,
|
|
117
|
+
changedFiles: uniquePaths(collectOutputLines(rangeResult)),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const unstaged = runGit(dir, ['diff', '--name-only', '--diff-filter=ACMRTUXB', 'HEAD', '--']);
|
|
123
|
+
const staged = runGit(dir, ['diff', '--cached', '--name-only', '--diff-filter=ACMRTUXB', '--']);
|
|
124
|
+
const untracked = runGit(dir, ['ls-files', '--others', '--exclude-standard']);
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
mode: 'working-tree',
|
|
128
|
+
changedFiles: uniquePaths([
|
|
129
|
+
...collectOutputLines(unstaged),
|
|
130
|
+
...collectOutputLines(staged),
|
|
131
|
+
...collectOutputLines(untracked),
|
|
132
|
+
]),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function matchesChangedFile(filePath, changedFiles) {
|
|
137
|
+
if (!filePath) return false;
|
|
138
|
+
const normalized = normalizeRelativePath(filePath).replace(/\/$/, '');
|
|
139
|
+
|
|
140
|
+
return changedFiles.some((changed) => {
|
|
141
|
+
const target = changed.replace(/\/$/, '');
|
|
142
|
+
return normalized === target ||
|
|
143
|
+
normalized.startsWith(`${target}/`) ||
|
|
144
|
+
target.startsWith(`${normalized}/`);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function inferRelevantCategories(changedFiles) {
|
|
149
|
+
const categories = new Set();
|
|
150
|
+
|
|
151
|
+
for (const filePath of changedFiles) {
|
|
152
|
+
for (const hint of CATEGORY_HINTS) {
|
|
153
|
+
if (hint.pattern.test(filePath)) {
|
|
154
|
+
hint.categories.forEach((category) => categories.add(category));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return categories;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function isRelevantResult(result, changedFiles, changedCategories) {
|
|
163
|
+
if (!result || result.deprecated) return false;
|
|
164
|
+
if (matchesChangedFile(result.file, changedFiles)) return true;
|
|
165
|
+
if (!result.file && result.category && changedCategories.has(result.category)) return true;
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function buildFallbackActions(failed) {
|
|
170
|
+
return failed
|
|
171
|
+
.slice()
|
|
172
|
+
.sort((a, b) => (IMPACT_ORDER[b.impact] || 0) - (IMPACT_ORDER[a.impact] || 0) || a.name.localeCompare(b.name))
|
|
173
|
+
.slice(0, 5)
|
|
174
|
+
.map((item) => ({
|
|
175
|
+
key: item.key,
|
|
176
|
+
name: item.name,
|
|
177
|
+
impact: item.impact,
|
|
178
|
+
category: item.category,
|
|
179
|
+
fix: item.fix,
|
|
180
|
+
file: item.file || null,
|
|
181
|
+
}));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function buildDiffOnlyAuditView(auditResult, diffInfo) {
|
|
185
|
+
const changedFiles = uniquePaths(diffInfo.changedFiles || []);
|
|
186
|
+
const changedCategories = inferRelevantCategories(changedFiles);
|
|
187
|
+
const relevantResults = (auditResult.results || []).filter((item) => isRelevantResult(item, changedFiles, changedCategories));
|
|
188
|
+
const applicable = relevantResults.filter((item) => item.passed !== null && item.deprecated !== true);
|
|
189
|
+
const skipped = relevantResults.filter((item) => item.passed === null && item.deprecated !== true);
|
|
190
|
+
const passed = applicable.filter((item) => item.passed === true);
|
|
191
|
+
const failed = applicable.filter((item) => item.passed === false);
|
|
192
|
+
const maxScore = applicable.reduce((sum, item) => sum + (WEIGHTS[item.impact] || 5), 0);
|
|
193
|
+
const earnedScore = passed.reduce((sum, item) => sum + (WEIGHTS[item.impact] || 5), 0);
|
|
194
|
+
const score = applicable.length > 0 && maxScore > 0 ? Math.round((earnedScore / maxScore) * 100) : null;
|
|
195
|
+
const relevantKeys = new Set(relevantResults.map((item) => item.key));
|
|
196
|
+
const topNextActions = (auditResult.topNextActions || []).filter((item) => relevantKeys.has(item.key)).slice(0, 5);
|
|
197
|
+
const fallbackActions = buildFallbackActions(failed);
|
|
198
|
+
|
|
199
|
+
const message = changedFiles.length === 0
|
|
200
|
+
? (diffInfo.message || 'No changed files detected for diff-only mode.')
|
|
201
|
+
: 'Diff-only mode filters to changed files plus linked governance/config surfaces. Run `nerviq audit` for complete repo posture.';
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
platform: auditResult.platform,
|
|
205
|
+
platformLabel: auditResult.platformLabel,
|
|
206
|
+
diffOnly: true,
|
|
207
|
+
score,
|
|
208
|
+
scoreType: 'diff-only changed-file audit',
|
|
209
|
+
passed: passed.length,
|
|
210
|
+
failed: failed.length,
|
|
211
|
+
skipped: skipped.length,
|
|
212
|
+
checkCount: applicable.length,
|
|
213
|
+
changedFiles,
|
|
214
|
+
changedFilesCount: changedFiles.length,
|
|
215
|
+
diffMode: diffInfo.mode,
|
|
216
|
+
diffBase: diffInfo.base || null,
|
|
217
|
+
diffHead: diffInfo.head || null,
|
|
218
|
+
fullAuditScore: auditResult.score,
|
|
219
|
+
message,
|
|
220
|
+
results: relevantResults,
|
|
221
|
+
topNextActions: topNextActions.length > 0 ? topNextActions : fallbackActions,
|
|
222
|
+
suggestedNextCommand: 'nerviq audit',
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function printDiffOnlyAudit(result) {
|
|
227
|
+
const lines = [''];
|
|
228
|
+
lines.push(c(' nerviq diff-only audit', 'bold'));
|
|
229
|
+
lines.push(c(' ═══════════════════════════════════════', 'dim'));
|
|
230
|
+
lines.push('');
|
|
231
|
+
|
|
232
|
+
if (result.diffMode === 'range' && result.diffBase) {
|
|
233
|
+
lines.push(c(` Diff range: ${result.diffBase}..${result.diffHead}`, 'dim'));
|
|
234
|
+
} else {
|
|
235
|
+
lines.push(c(' Diff source: working tree vs HEAD', 'dim'));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (result.changedFilesCount === 0) {
|
|
239
|
+
lines.push(c(` ${result.message}`, 'yellow'));
|
|
240
|
+
lines.push(c(' Tip: commit or stage a change first, or provide a PR base SHA via --diff-base.', 'dim'));
|
|
241
|
+
lines.push('');
|
|
242
|
+
return lines.join('\n');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
lines.push(c(` Changed files (${result.changedFilesCount}):`, 'bold'));
|
|
246
|
+
result.changedFiles.slice(0, 8).forEach((filePath) => {
|
|
247
|
+
lines.push(` ${c('•', 'blue')} ${filePath}`);
|
|
248
|
+
});
|
|
249
|
+
if (result.changedFilesCount > 8) {
|
|
250
|
+
lines.push(c(` ...and ${result.changedFilesCount - 8} more`, 'dim'));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
lines.push('');
|
|
254
|
+
lines.push(` Score: ${result.score === null ? c('n/a', 'yellow') : c(`${result.score}/100`, 'bold')}`);
|
|
255
|
+
lines.push(c(` Score type: ${result.scoreType} (not the full repo score of ${result.fullAuditScore}/100).`, 'dim'));
|
|
256
|
+
lines.push(c(` ${result.message}`, 'dim'));
|
|
257
|
+
lines.push('');
|
|
258
|
+
|
|
259
|
+
if (result.topNextActions && result.topNextActions.length > 0) {
|
|
260
|
+
lines.push(c(' Top diff-relevant actions:', 'magenta'));
|
|
261
|
+
result.topNextActions.slice(0, 5).forEach((item, index) => {
|
|
262
|
+
lines.push(` ${index + 1}. ${c(item.name, 'bold')} ${c(`[${item.impact}]`, 'dim')}`);
|
|
263
|
+
if (item.fix) {
|
|
264
|
+
lines.push(c(` ${item.fix}`, 'dim'));
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
lines.push('');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
lines.push(` Relevant checks: ${result.checkCount} | Failed: ${result.failed} | Passed: ${result.passed} | Skipped: ${result.skipped}`);
|
|
271
|
+
lines.push(c(' Run `nerviq audit` for the complete repo posture.', 'dim'));
|
|
272
|
+
lines.push('');
|
|
273
|
+
return lines.join('\n');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
module.exports = {
|
|
277
|
+
getChangedFiles,
|
|
278
|
+
buildDiffOnlyAuditView,
|
|
279
|
+
printDiffOnlyAudit,
|
|
280
|
+
};
|