@nerviq/cli 0.0.1 → 0.9.0-beta.1
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/CHANGELOG.md +181 -0
- package/LICENSE +21 -0
- package/README.md +447 -0
- package/bin/cli.js +749 -0
- package/content/case-study-template.md +91 -0
- package/content/claims-governance.md +37 -0
- package/content/claude-code/audit-repo/SKILL.md +20 -0
- package/content/claude-native-integration.md +60 -0
- package/content/devto-article.json +9 -0
- package/content/launch-posts.md +226 -0
- package/content/pilot-rollout-kit.md +30 -0
- package/content/release-checklist.md +31 -0
- package/package.json +53 -4
- package/src/activity.js +529 -0
- package/src/aider/activity.js +226 -0
- package/src/aider/config-parser.js +166 -0
- package/src/aider/context.js +158 -0
- package/src/aider/deep-review.js +316 -0
- package/src/aider/domain-packs.js +278 -0
- package/src/aider/freshness.js +168 -0
- package/src/aider/governance.js +253 -0
- package/src/aider/interactive.js +334 -0
- package/src/aider/mcp-packs.js +98 -0
- package/src/aider/patch.js +214 -0
- package/src/aider/plans.js +186 -0
- package/src/aider/premium.js +360 -0
- package/src/aider/setup.js +404 -0
- package/src/aider/techniques.js +1323 -0
- package/src/analyze.js +821 -0
- package/src/audit.js +1003 -0
- package/src/badge.js +13 -0
- package/src/benchmark.js +339 -0
- package/src/claudex-sync.json +7 -0
- package/src/codex/activity.js +324 -0
- package/src/codex/config-parser.js +183 -0
- package/src/codex/context.js +221 -0
- package/src/codex/deep-review.js +493 -0
- package/src/codex/domain-packs.js +372 -0
- package/src/codex/freshness.js +167 -0
- package/src/codex/governance.js +192 -0
- package/src/codex/interactive.js +618 -0
- package/src/codex/mcp-packs.js +660 -0
- package/src/codex/patch.js +209 -0
- package/src/codex/plans.js +251 -0
- package/src/codex/premium.js +614 -0
- package/src/codex/setup.js +603 -0
- package/src/codex/techniques.js +2649 -0
- package/src/context.js +272 -0
- package/src/copilot/activity.js +309 -0
- package/src/copilot/config-parser.js +226 -0
- package/src/copilot/context.js +197 -0
- package/src/copilot/deep-review.js +346 -0
- package/src/copilot/domain-packs.js +350 -0
- package/src/copilot/freshness.js +197 -0
- package/src/copilot/governance.js +222 -0
- package/src/copilot/interactive.js +406 -0
- package/src/copilot/mcp-packs.js +572 -0
- package/src/copilot/patch.js +238 -0
- package/src/copilot/plans.js +253 -0
- package/src/copilot/premium.js +450 -0
- package/src/copilot/setup.js +488 -0
- package/src/copilot/techniques.js +1822 -0
- package/src/cursor/activity.js +301 -0
- package/src/cursor/config-parser.js +265 -0
- package/src/cursor/context.js +236 -0
- package/src/cursor/deep-review.js +334 -0
- package/src/cursor/domain-packs.js +346 -0
- package/src/cursor/freshness.js +214 -0
- package/src/cursor/governance.js +229 -0
- package/src/cursor/interactive.js +391 -0
- package/src/cursor/mcp-packs.js +571 -0
- package/src/cursor/patch.js +243 -0
- package/src/cursor/plans.js +254 -0
- package/src/cursor/premium.js +468 -0
- package/src/cursor/setup.js +488 -0
- package/src/cursor/techniques.js +1786 -0
- package/src/deep-review.js +345 -0
- package/src/domain-packs.js +364 -0
- package/src/formatters/sarif.js +115 -0
- package/src/gemini/activity.js +402 -0
- package/src/gemini/config-parser.js +275 -0
- package/src/gemini/context.js +221 -0
- package/src/gemini/deep-review.js +559 -0
- package/src/gemini/domain-packs.js +371 -0
- package/src/gemini/freshness.js +204 -0
- package/src/gemini/governance.js +201 -0
- package/src/gemini/interactive.js +860 -0
- package/src/gemini/mcp-packs.js +658 -0
- package/src/gemini/patch.js +229 -0
- package/src/gemini/plans.js +269 -0
- package/src/gemini/premium.js +759 -0
- package/src/gemini/setup.js +692 -0
- package/src/gemini/techniques.js +2084 -0
- package/src/governance.js +523 -0
- package/src/harmony/advisor.js +383 -0
- package/src/harmony/audit.js +303 -0
- package/src/harmony/canon.js +444 -0
- package/src/harmony/cli.js +331 -0
- package/src/harmony/drift.js +401 -0
- package/src/harmony/governance.js +313 -0
- package/src/harmony/memory.js +238 -0
- package/src/harmony/sync.js +458 -0
- package/src/harmony/watch.js +336 -0
- package/src/index.js +256 -0
- package/src/insights.js +119 -0
- package/src/interactive.js +118 -0
- package/src/mcp-packs.js +597 -0
- package/src/opencode/activity.js +286 -0
- package/src/opencode/config-parser.js +109 -0
- package/src/opencode/context.js +247 -0
- package/src/opencode/deep-review.js +313 -0
- package/src/opencode/domain-packs.js +240 -0
- package/src/opencode/freshness.js +158 -0
- package/src/opencode/governance.js +159 -0
- package/src/opencode/interactive.js +392 -0
- package/src/opencode/mcp-packs.js +474 -0
- package/src/opencode/patch.js +184 -0
- package/src/opencode/plans.js +231 -0
- package/src/opencode/premium.js +413 -0
- package/src/opencode/setup.js +449 -0
- package/src/opencode/techniques.js +1713 -0
- package/src/plans.js +655 -0
- package/src/secret-patterns.js +30 -0
- package/src/setup.js +1274 -0
- package/src/synergy/adaptive.js +261 -0
- package/src/synergy/compensation.js +156 -0
- package/src/synergy/evidence.js +193 -0
- package/src/synergy/learning.js +184 -0
- package/src/synergy/patterns.js +227 -0
- package/src/synergy/ranking.js +83 -0
- package/src/synergy/report.js +163 -0
- package/src/synergy/routing.js +152 -0
- package/src/techniques.js +1354 -0
- package/src/watch.js +229 -0
- package/src/windsurf/activity.js +302 -0
- package/src/windsurf/config-parser.js +267 -0
- package/src/windsurf/context.js +249 -0
- package/src/windsurf/deep-review.js +337 -0
- package/src/windsurf/domain-packs.js +348 -0
- package/src/windsurf/freshness.js +215 -0
- package/src/windsurf/governance.js +231 -0
- package/src/windsurf/interactive.js +388 -0
- package/src/windsurf/mcp-packs.js +535 -0
- package/src/windsurf/patch.js +231 -0
- package/src/windsurf/plans.js +247 -0
- package/src/windsurf/premium.js +467 -0
- package/src/windsurf/setup.js +471 -0
- package/src/windsurf/techniques.js +1758 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harmony Drift Detection Engine
|
|
3
|
+
*
|
|
4
|
+
* Compares configurations ACROSS platforms and finds inconsistencies
|
|
5
|
+
* in instructions, trust posture, MCP servers, rules, and coverage.
|
|
6
|
+
*
|
|
7
|
+
* Drift types:
|
|
8
|
+
* instruction-drift — different platforms say different things
|
|
9
|
+
* trust-drift — sandbox/approval modes differ across platforms
|
|
10
|
+
* mcp-drift — MCP servers not aligned across platforms
|
|
11
|
+
* rule-drift — rule coverage differs between platforms
|
|
12
|
+
* coverage-gap — platform missing instructions entirely
|
|
13
|
+
*
|
|
14
|
+
* Severity: critical, high, medium, low
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const SEVERITY_ORDER = { critical: 3, high: 2, medium: 1, low: 0 };
|
|
18
|
+
|
|
19
|
+
const COLORS = {
|
|
20
|
+
reset: '\x1b[0m',
|
|
21
|
+
bold: '\x1b[1m',
|
|
22
|
+
dim: '\x1b[2m',
|
|
23
|
+
red: '\x1b[31m',
|
|
24
|
+
green: '\x1b[32m',
|
|
25
|
+
yellow: '\x1b[33m',
|
|
26
|
+
blue: '\x1b[36m',
|
|
27
|
+
magenta: '\x1b[35m',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function colorize(text, color) {
|
|
31
|
+
return `${COLORS[color] || ''}${text}${COLORS.reset}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ─── Trust level risk mapping ───────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
const TRUST_RISK = {
|
|
37
|
+
'bypass': 4,
|
|
38
|
+
'full-auto': 4,
|
|
39
|
+
'unrestricted': 4,
|
|
40
|
+
'no-sandbox': 3,
|
|
41
|
+
'standard': 2,
|
|
42
|
+
'safe-write': 2,
|
|
43
|
+
'default': 1,
|
|
44
|
+
'locked-down': 0,
|
|
45
|
+
'unknown': 1,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// ─── Drift detectors ───────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Detect trust posture drift across platforms.
|
|
52
|
+
* Critical if risk gap >= 2 levels between any two platforms.
|
|
53
|
+
*/
|
|
54
|
+
function detectTrustDrift(model) {
|
|
55
|
+
const drifts = [];
|
|
56
|
+
const platforms = Object.keys(model.trustPosture);
|
|
57
|
+
|
|
58
|
+
if (platforms.length < 2) return drifts;
|
|
59
|
+
|
|
60
|
+
const riskLevels = {};
|
|
61
|
+
for (const p of platforms) {
|
|
62
|
+
riskLevels[p] = TRUST_RISK[model.trustPosture[p]] ?? 1;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const maxRisk = Math.max(...Object.values(riskLevels));
|
|
66
|
+
const minRisk = Math.min(...Object.values(riskLevels));
|
|
67
|
+
const gap = maxRisk - minRisk;
|
|
68
|
+
|
|
69
|
+
if (gap === 0) return drifts;
|
|
70
|
+
|
|
71
|
+
const highRiskPlatforms = platforms.filter(p => riskLevels[p] === maxRisk);
|
|
72
|
+
const lowRiskPlatforms = platforms.filter(p => riskLevels[p] === minRisk);
|
|
73
|
+
|
|
74
|
+
let severity = 'low';
|
|
75
|
+
if (gap >= 3) severity = 'critical';
|
|
76
|
+
else if (gap >= 2) severity = 'high';
|
|
77
|
+
else if (gap >= 1) severity = 'medium';
|
|
78
|
+
|
|
79
|
+
drifts.push({
|
|
80
|
+
type: 'trust-drift',
|
|
81
|
+
severity,
|
|
82
|
+
platforms,
|
|
83
|
+
description: `Trust posture gap of ${gap} levels: ` +
|
|
84
|
+
`${highRiskPlatforms.map(p => `${p}=${model.trustPosture[p]}`).join(', ')} vs ` +
|
|
85
|
+
`${lowRiskPlatforms.map(p => `${p}=${model.trustPosture[p]}`).join(', ')}`,
|
|
86
|
+
recommendation: `Align trust posture across platforms. Consider raising ${lowRiskPlatforms.join(', ')} ` +
|
|
87
|
+
`or tightening ${highRiskPlatforms.join(', ')} to reduce risk surface.`,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return drifts;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Detect MCP server drift — servers present on some platforms but not others.
|
|
95
|
+
*/
|
|
96
|
+
function detectMcpDrift(model) {
|
|
97
|
+
const drifts = [];
|
|
98
|
+
const allPlatforms = model.activePlatforms.map(p => p.platform);
|
|
99
|
+
const mcpCapablePlatforms = allPlatforms.filter(p =>
|
|
100
|
+
p === 'claude' || p === 'gemini' || p === 'copilot' || p === 'cursor'
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
if (mcpCapablePlatforms.length < 2) return drifts;
|
|
104
|
+
|
|
105
|
+
for (const [name, server] of Object.entries(model.mcpServers)) {
|
|
106
|
+
const present = server.platforms.filter(p => mcpCapablePlatforms.includes(p));
|
|
107
|
+
const missing = mcpCapablePlatforms.filter(p => !server.platforms.includes(p));
|
|
108
|
+
|
|
109
|
+
if (missing.length > 0 && present.length > 0) {
|
|
110
|
+
drifts.push({
|
|
111
|
+
type: 'mcp-drift',
|
|
112
|
+
severity: missing.length >= 2 ? 'high' : 'medium',
|
|
113
|
+
platforms: [...present, ...missing],
|
|
114
|
+
description: `MCP server "${name}" is configured on ${present.join(', ')} but missing from ${missing.join(', ')}`,
|
|
115
|
+
recommendation: `Add "${name}" to ${missing.join(', ')} MCP configuration to ensure consistent tool access.`,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Check for platforms with zero MCP servers when others have some
|
|
121
|
+
const serverCounts = {};
|
|
122
|
+
for (const p of mcpCapablePlatforms) {
|
|
123
|
+
const detail = model.platformDetails[p];
|
|
124
|
+
serverCounts[p] = detail ? detail.mcpServers.length : 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const withServers = mcpCapablePlatforms.filter(p => serverCounts[p] > 0);
|
|
128
|
+
const withoutServers = mcpCapablePlatforms.filter(p => serverCounts[p] === 0);
|
|
129
|
+
|
|
130
|
+
if (withServers.length > 0 && withoutServers.length > 0) {
|
|
131
|
+
drifts.push({
|
|
132
|
+
type: 'mcp-drift',
|
|
133
|
+
severity: 'medium',
|
|
134
|
+
platforms: [...withServers, ...withoutServers],
|
|
135
|
+
description: `MCP servers configured on ${withServers.join(', ')} (${withServers.map(p => serverCounts[p]).join(', ')} servers) ` +
|
|
136
|
+
`but ${withoutServers.join(', ')} have none`,
|
|
137
|
+
recommendation: `Consider adding MCP servers to ${withoutServers.join(', ')} for consistent tool access.`,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return drifts;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Detect rule drift — platforms with rules configured vs those without.
|
|
146
|
+
*/
|
|
147
|
+
function detectRuleDrift(model) {
|
|
148
|
+
const drifts = [];
|
|
149
|
+
const allPlatforms = model.activePlatforms.map(p => p.platform);
|
|
150
|
+
|
|
151
|
+
// Platforms that support rules: claude (.claude/rules), copilot (.github/instructions), cursor (.cursor/rules)
|
|
152
|
+
const ruleCapable = allPlatforms.filter(p =>
|
|
153
|
+
p === 'claude' || p === 'copilot' || p === 'cursor'
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
if (ruleCapable.length < 2) return drifts;
|
|
157
|
+
|
|
158
|
+
const ruleCounts = {};
|
|
159
|
+
for (const p of ruleCapable) {
|
|
160
|
+
ruleCounts[p] = model.activePlatforms.find(ap => ap.platform === p)?.ruleCount || 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const withRules = ruleCapable.filter(p => ruleCounts[p] > 0);
|
|
164
|
+
const withoutRules = ruleCapable.filter(p => ruleCounts[p] === 0);
|
|
165
|
+
|
|
166
|
+
if (withRules.length > 0 && withoutRules.length > 0) {
|
|
167
|
+
drifts.push({
|
|
168
|
+
type: 'rule-drift',
|
|
169
|
+
severity: 'medium',
|
|
170
|
+
platforms: [...withRules, ...withoutRules],
|
|
171
|
+
description: `Rules configured on ${withRules.map(p => `${p} (${ruleCounts[p]})`).join(', ')} ` +
|
|
172
|
+
`but missing from ${withoutRules.join(', ')}`,
|
|
173
|
+
recommendation: `Add equivalent rules to ${withoutRules.join(', ')} to maintain consistent behavior constraints.`,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Check for large rule count gaps
|
|
178
|
+
const maxRules = Math.max(...Object.values(ruleCounts));
|
|
179
|
+
const minRules = Math.min(...Object.values(ruleCounts));
|
|
180
|
+
|
|
181
|
+
if (maxRules > 0 && minRules > 0 && maxRules > minRules * 3) {
|
|
182
|
+
const richPlatform = ruleCapable.find(p => ruleCounts[p] === maxRules);
|
|
183
|
+
const sparPlatform = ruleCapable.find(p => ruleCounts[p] === minRules);
|
|
184
|
+
drifts.push({
|
|
185
|
+
type: 'rule-drift',
|
|
186
|
+
severity: 'low',
|
|
187
|
+
platforms: [richPlatform, sparPlatform],
|
|
188
|
+
description: `Large rule count gap: ${richPlatform} has ${maxRules} rules vs ${sparPlatform} with ${minRules}`,
|
|
189
|
+
recommendation: `Review whether ${sparPlatform} is missing important rules that ${richPlatform} enforces.`,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return drifts;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Detect instruction coverage gaps — platforms with no instruction files at all.
|
|
198
|
+
*/
|
|
199
|
+
function detectCoverageGaps(model) {
|
|
200
|
+
const drifts = [];
|
|
201
|
+
const allPlatforms = model.activePlatforms;
|
|
202
|
+
|
|
203
|
+
for (const ap of allPlatforms) {
|
|
204
|
+
const detail = model.platformDetails[ap.platform];
|
|
205
|
+
if (!detail) continue;
|
|
206
|
+
|
|
207
|
+
if (detail.instructionFiles.length === 0 || !detail.instructionContent.trim()) {
|
|
208
|
+
drifts.push({
|
|
209
|
+
type: 'coverage-gap',
|
|
210
|
+
severity: 'high',
|
|
211
|
+
platforms: [ap.platform],
|
|
212
|
+
description: `${ap.label} is detected but has no instruction content`,
|
|
213
|
+
recommendation: `Create instruction file for ${ap.label} to ensure consistent agent behavior.`,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (detail.configFiles.length === 0) {
|
|
218
|
+
drifts.push({
|
|
219
|
+
type: 'coverage-gap',
|
|
220
|
+
severity: 'medium',
|
|
221
|
+
platforms: [ap.platform],
|
|
222
|
+
description: `${ap.label} has no configuration file`,
|
|
223
|
+
recommendation: `Add configuration for ${ap.label} to explicitly set trust and behavior parameters.`,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return drifts;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Detect instruction drift — key instruction patterns present in some but not all platforms.
|
|
233
|
+
*/
|
|
234
|
+
function detectInstructionDrift(model) {
|
|
235
|
+
const drifts = [];
|
|
236
|
+
const platforms = model.activePlatforms.map(p => p.platform);
|
|
237
|
+
|
|
238
|
+
if (platforms.length < 2) return drifts;
|
|
239
|
+
|
|
240
|
+
// Check for common instruction patterns that should be shared
|
|
241
|
+
const patterns = [
|
|
242
|
+
{ name: 'test command', regex: /(?:test|pytest|jest|vitest|npm\s+test)/i, severity: 'high' },
|
|
243
|
+
{ name: 'lint command', regex: /(?:lint|eslint|ruff|pylint)/i, severity: 'medium' },
|
|
244
|
+
{ name: 'build command', regex: /(?:build|compile|tsc|webpack|vite build)/i, severity: 'medium' },
|
|
245
|
+
{ name: 'architecture diagram', regex: /(?:mermaid|graph\s+TD|flowchart)/i, severity: 'low' },
|
|
246
|
+
{ name: 'security instructions', regex: /(?:\.env|secrets?|credential|NEVER.*push|deny.*read)/i, severity: 'high' },
|
|
247
|
+
{ name: 'language preference', regex: /(?:hebrew|english|language|speak|respond in)/i, severity: 'low' },
|
|
248
|
+
];
|
|
249
|
+
|
|
250
|
+
for (const pattern of patterns) {
|
|
251
|
+
const has = [];
|
|
252
|
+
const hasNot = [];
|
|
253
|
+
|
|
254
|
+
for (const p of platforms) {
|
|
255
|
+
const detail = model.platformDetails[p];
|
|
256
|
+
if (!detail) continue;
|
|
257
|
+
const content = detail.instructionContent || '';
|
|
258
|
+
if (pattern.regex.test(content)) {
|
|
259
|
+
has.push(p);
|
|
260
|
+
} else {
|
|
261
|
+
hasNot.push(p);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (has.length > 0 && hasNot.length > 0) {
|
|
266
|
+
drifts.push({
|
|
267
|
+
type: 'instruction-drift',
|
|
268
|
+
severity: pattern.severity,
|
|
269
|
+
platforms: [...has, ...hasNot],
|
|
270
|
+
description: `"${pattern.name}" found in ${has.join(', ')} but missing from ${hasNot.join(', ')}`,
|
|
271
|
+
recommendation: `Add ${pattern.name} instructions to ${hasNot.join(', ')} for consistent behavior.`,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return drifts;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ─── Main detection function ────────────────────────────────────────────────
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Detect all types of drift across platforms in a canonical model.
|
|
283
|
+
*
|
|
284
|
+
* @param {object} canonicalModel - Output of buildCanonicalModel()
|
|
285
|
+
* @returns {object} { drifts, summary, harmonyScore }
|
|
286
|
+
*/
|
|
287
|
+
function detectDrift(canonicalModel) {
|
|
288
|
+
const drifts = [
|
|
289
|
+
...detectInstructionDrift(canonicalModel),
|
|
290
|
+
...detectTrustDrift(canonicalModel),
|
|
291
|
+
...detectMcpDrift(canonicalModel),
|
|
292
|
+
...detectRuleDrift(canonicalModel),
|
|
293
|
+
...detectCoverageGaps(canonicalModel),
|
|
294
|
+
];
|
|
295
|
+
|
|
296
|
+
// Sort by severity (critical first)
|
|
297
|
+
drifts.sort((a, b) => (SEVERITY_ORDER[b.severity] || 0) - (SEVERITY_ORDER[a.severity] || 0));
|
|
298
|
+
|
|
299
|
+
// Summary counts
|
|
300
|
+
const summary = {
|
|
301
|
+
total: drifts.length,
|
|
302
|
+
critical: drifts.filter(d => d.severity === 'critical').length,
|
|
303
|
+
high: drifts.filter(d => d.severity === 'high').length,
|
|
304
|
+
medium: drifts.filter(d => d.severity === 'medium').length,
|
|
305
|
+
low: drifts.filter(d => d.severity === 'low').length,
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// Harmony score: start at 100, deduct per drift
|
|
309
|
+
const deductions = {
|
|
310
|
+
critical: 20,
|
|
311
|
+
high: 12,
|
|
312
|
+
medium: 5,
|
|
313
|
+
low: 2,
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
let harmonyScore = 100;
|
|
317
|
+
for (const drift of drifts) {
|
|
318
|
+
harmonyScore -= deductions[drift.severity] || 2;
|
|
319
|
+
}
|
|
320
|
+
harmonyScore = Math.max(0, Math.min(100, harmonyScore));
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
drifts,
|
|
324
|
+
summary,
|
|
325
|
+
harmonyScore,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ─── Report formatter ───────────────────────────────────────────────────────
|
|
330
|
+
|
|
331
|
+
const SEVERITY_ICONS = {
|
|
332
|
+
critical: '\u2718', // ✘
|
|
333
|
+
high: '!',
|
|
334
|
+
medium: '~',
|
|
335
|
+
low: '-',
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const SEVERITY_COLORS = {
|
|
339
|
+
critical: 'red',
|
|
340
|
+
high: 'yellow',
|
|
341
|
+
medium: 'blue',
|
|
342
|
+
low: 'dim',
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Format a drift report for console output.
|
|
347
|
+
*
|
|
348
|
+
* @param {object} driftResult - Output of detectDrift()
|
|
349
|
+
* @param {object} [options] - { color: true, verbose: false }
|
|
350
|
+
* @returns {string} Formatted report
|
|
351
|
+
*/
|
|
352
|
+
function formatDriftReport(driftResult, options = {}) {
|
|
353
|
+
const { color = true, verbose = false } = options;
|
|
354
|
+
const c = color ? colorize : (text) => text;
|
|
355
|
+
const lines = [];
|
|
356
|
+
|
|
357
|
+
lines.push('');
|
|
358
|
+
lines.push(c(' Harmony Drift Report', 'bold'));
|
|
359
|
+
lines.push(c(' ' + '='.repeat(40), 'dim'));
|
|
360
|
+
lines.push('');
|
|
361
|
+
|
|
362
|
+
// Score
|
|
363
|
+
const score = driftResult.harmonyScore;
|
|
364
|
+
const scoreColor = score >= 80 ? 'green' : score >= 50 ? 'yellow' : 'red';
|
|
365
|
+
lines.push(` Harmony Score: ${c(String(score) + '/100', scoreColor)}`);
|
|
366
|
+
lines.push('');
|
|
367
|
+
|
|
368
|
+
// Summary
|
|
369
|
+
const s = driftResult.summary;
|
|
370
|
+
if (s.total === 0) {
|
|
371
|
+
lines.push(c(' No drift detected. All platforms are aligned.', 'green'));
|
|
372
|
+
} else {
|
|
373
|
+
lines.push(` Drift items: ${s.total} total`);
|
|
374
|
+
if (s.critical > 0) lines.push(c(` Critical: ${s.critical}`, 'red'));
|
|
375
|
+
if (s.high > 0) lines.push(c(` High: ${s.high}`, 'yellow'));
|
|
376
|
+
if (s.medium > 0) lines.push(c(` Medium: ${s.medium}`, 'blue'));
|
|
377
|
+
if (s.low > 0) lines.push(c(` Low: ${s.low}`, 'dim'));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
lines.push('');
|
|
381
|
+
|
|
382
|
+
// Individual drifts
|
|
383
|
+
for (const drift of driftResult.drifts) {
|
|
384
|
+
const icon = SEVERITY_ICONS[drift.severity] || '-';
|
|
385
|
+
const dColor = SEVERITY_COLORS[drift.severity] || 'dim';
|
|
386
|
+
|
|
387
|
+
lines.push(c(` ${icon} [${drift.severity.toUpperCase()}] ${drift.type}`, dColor));
|
|
388
|
+
lines.push(` ${drift.description}`);
|
|
389
|
+
if (verbose && drift.recommendation) {
|
|
390
|
+
lines.push(c(` Recommendation: ${drift.recommendation}`, 'dim'));
|
|
391
|
+
}
|
|
392
|
+
lines.push('');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return lines.join('\n');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
module.exports = {
|
|
399
|
+
detectDrift,
|
|
400
|
+
formatDriftReport,
|
|
401
|
+
};
|