@nerviq/cli 1.26.0 → 1.27.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 +4 -4
- package/bin/cli.js +13 -1
- package/package.json +1 -1
- package/src/audit/layers.js +180 -179
- package/src/audit.js +118 -48
- package/src/formatters/csv.js +86 -85
- package/src/formatters/junit.js +123 -103
- package/src/formatters/markdown.js +164 -135
- package/src/shallow-risk/index.js +56 -0
- package/src/shallow-risk/patterns/agent-config-cross-platform-drift.js +50 -0
- package/src/shallow-risk/patterns/agent-config-dangerous-autoapprove.js +46 -0
- package/src/shallow-risk/patterns/agent-config-deprecated-keys.js +46 -0
- package/src/shallow-risk/patterns/agent-config-missing-file.js +72 -0
- package/src/shallow-risk/patterns/agent-config-secret-literal.js +49 -0
- package/src/shallow-risk/patterns/agent-config-stack-contradiction.js +34 -0
- package/src/shallow-risk/patterns/hook-script-missing.js +70 -0
- package/src/shallow-risk/patterns/mcp-server-no-allowlist.js +52 -0
- package/src/shallow-risk/shared.js +520 -0
package/src/audit.js
CHANGED
|
@@ -37,6 +37,7 @@ const { detectDeprecationWarnings } = require('./deprecation');
|
|
|
37
37
|
const { buildWorkspaceHint, formatCount, guardSkippedInstructionFiles, inspectInstructionFiles } = require('./audit/instruction-files');
|
|
38
38
|
const { resolveEvidence } = require('./audit/evidence');
|
|
39
39
|
const { LAYERS, summarizeLayers } = require('./audit/layers');
|
|
40
|
+
const { runShallowRisk, SHALLOW_RISK_BANNER_LINES } = require('./shallow-risk');
|
|
40
41
|
const {
|
|
41
42
|
WEIGHTS,
|
|
42
43
|
buildScoreCoaching,
|
|
@@ -78,6 +79,54 @@ function formatLocation(file, line) {
|
|
|
78
79
|
return line ? `${file}:${line}` : file;
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
function hasShallowRiskData(result) {
|
|
83
|
+
return Boolean(result) && Object.prototype.hasOwnProperty.call(result, 'shallowRiskHints');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function printShallowRiskSection(result) {
|
|
87
|
+
if (!hasShallowRiskData(result)) return;
|
|
88
|
+
|
|
89
|
+
const hints = Array.isArray(result.shallowRiskHints) ? result.shallowRiskHints : [];
|
|
90
|
+
console.log(colorize(' Shallow Risk Hints (experimental, opt-in)', 'yellow'));
|
|
91
|
+
for (const line of SHALLOW_RISK_BANNER_LINES) {
|
|
92
|
+
console.log(colorize(` ${line}`, 'dim'));
|
|
93
|
+
}
|
|
94
|
+
console.log('');
|
|
95
|
+
|
|
96
|
+
if (hints.length === 0) {
|
|
97
|
+
console.log(colorize(' No shallow-risk hints found.', 'green'));
|
|
98
|
+
console.log('');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const hint of hints) {
|
|
103
|
+
const severity = (hint.severity || 'medium').toUpperCase();
|
|
104
|
+
console.log(` ${colorize(`[${severity}]`, 'bold')} ${hint.name}`);
|
|
105
|
+
if (hint.file) {
|
|
106
|
+
console.log(colorize(` at ${formatLocation(hint.file, hint.line)}`, 'dim'));
|
|
107
|
+
}
|
|
108
|
+
if (hint.fix) {
|
|
109
|
+
console.log(colorize(` -> ${hint.fix}`, 'dim'));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
console.log('');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function printShallowRiskOnly(result, dir) {
|
|
116
|
+
console.log('');
|
|
117
|
+
console.log(colorize(' Nerviq Shallow Risk', 'bold'));
|
|
118
|
+
console.log(colorize(' ═══════════════════════════════════════', 'dim'));
|
|
119
|
+
console.log(colorize(` ${t('audit.scanning', { dir })}`, 'dim'));
|
|
120
|
+
console.log('');
|
|
121
|
+
if (result.detectedConfigFiles && result.detectedConfigFiles.length > 0) {
|
|
122
|
+
console.log(colorize(` Found: ${result.detectedConfigFiles.join(', ')}`, 'dim'));
|
|
123
|
+
console.log('');
|
|
124
|
+
}
|
|
125
|
+
printShallowRiskSection(result);
|
|
126
|
+
console.log(` Next: ${colorize('nerviq audit --shallow-risk --full', 'bold')}`);
|
|
127
|
+
console.log('');
|
|
128
|
+
}
|
|
129
|
+
|
|
81
130
|
function getAuditSpec(platform = 'claude') {
|
|
82
131
|
if (platform === 'codex') {
|
|
83
132
|
return {
|
|
@@ -299,6 +348,7 @@ function printLiteAudit(result, dir) {
|
|
|
299
348
|
if (result.failed === 0) {
|
|
300
349
|
const platformLabel = result.platform === 'codex' ? 'Codex' : 'Claude';
|
|
301
350
|
console.log(colorize(` Your ${platformLabel} setup looks solid.`, 'green'));
|
|
351
|
+
printShallowRiskSection(result);
|
|
302
352
|
console.log(` Next: ${colorize(result.suggestedNextCommand, 'bold')}`);
|
|
303
353
|
if (result.platform === 'codex') {
|
|
304
354
|
console.log(colorize(' Note: Codex now supports no-write advisory flows via augment and suggest-only before setup/apply.', 'dim'));
|
|
@@ -333,6 +383,7 @@ function printLiteAudit(result, dir) {
|
|
|
333
383
|
console.log(colorize(` ${item.fix}`, 'dim'));
|
|
334
384
|
});
|
|
335
385
|
console.log('');
|
|
386
|
+
printShallowRiskSection(result);
|
|
336
387
|
const liteTerminology = formatTerminologyLines(collectAuditTerminology(result));
|
|
337
388
|
if (liteTerminology.length > 0) {
|
|
338
389
|
liteTerminology.forEach((line) => {
|
|
@@ -365,8 +416,12 @@ async function audit(options) {
|
|
|
365
416
|
const spec = getAuditSpec(options.platform || 'claude');
|
|
366
417
|
const silent = options.silent || false;
|
|
367
418
|
const ctx = new spec.ContextClass(options.dir);
|
|
368
|
-
const
|
|
369
|
-
|
|
419
|
+
const shallowRiskEnabled = Boolean(options.shallowRisk) && process.env.NERVIQ_SHALLOW_RISK !== 'off';
|
|
420
|
+
const shallowRiskOnly = Boolean(options.shallowRiskOnly) && shallowRiskEnabled;
|
|
421
|
+
const largeInstructionFiles = shallowRiskOnly ? [] : inspectInstructionFiles(spec, ctx);
|
|
422
|
+
if (!shallowRiskOnly) {
|
|
423
|
+
guardSkippedInstructionFiles(ctx, largeInstructionFiles);
|
|
424
|
+
}
|
|
370
425
|
const stacks = ctx.detectStacks(STACKS);
|
|
371
426
|
const results = [];
|
|
372
427
|
const outcomeSummary = getRecommendationOutcomeSummary(options.dir);
|
|
@@ -397,46 +452,48 @@ async function audit(options) {
|
|
|
397
452
|
const includeGenericQuality = options.verbose;
|
|
398
453
|
|
|
399
454
|
// Run all technique checks
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
455
|
+
if (!shallowRiskOnly) {
|
|
456
|
+
for (const [key, technique] of Object.entries(techniques)) {
|
|
457
|
+
// Skip entire stack category if the stack is not detected at a core location
|
|
458
|
+
// Skip generic quality categories unless --verbose is set
|
|
459
|
+
const cat = technique.category;
|
|
460
|
+
if ((!includeGenericQuality && GENERIC_QUALITY_CATEGORIES.has(cat)) ||
|
|
461
|
+
(STACK_CATEGORY_DETECTORS[cat] && !activeStackCategories.has(cat))) {
|
|
462
|
+
results.push({
|
|
463
|
+
key,
|
|
464
|
+
...technique,
|
|
465
|
+
file: null,
|
|
466
|
+
line: null,
|
|
467
|
+
passed: null, // not applicable
|
|
468
|
+
});
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const passed = technique.check(ctx);
|
|
473
|
+
let file = typeof technique.file === 'function' ? (technique.file(ctx) ?? null) : (technique.file ?? null);
|
|
474
|
+
let line = typeof technique.line === 'function' ? (technique.line(ctx) ?? null) : (technique.line ?? null);
|
|
475
|
+
let snippet = null;
|
|
476
|
+
// CTO-04: only compute evidence on failed checks (cheap, and only where it adds trust).
|
|
477
|
+
if (passed === false) {
|
|
478
|
+
const evidence = resolveEvidence(key, ctx, { file, line });
|
|
479
|
+
if (evidence) {
|
|
480
|
+
file = evidence.file;
|
|
481
|
+
line = evidence.line;
|
|
482
|
+
snippet = evidence.snippet;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
406
485
|
results.push({
|
|
407
486
|
key,
|
|
408
487
|
...technique,
|
|
409
|
-
file
|
|
410
|
-
line: null,
|
|
411
|
-
|
|
488
|
+
file,
|
|
489
|
+
line: Number.isFinite(line) ? line : null,
|
|
490
|
+
snippet,
|
|
491
|
+
passed,
|
|
412
492
|
});
|
|
413
|
-
continue;
|
|
414
493
|
}
|
|
415
|
-
|
|
416
|
-
const passed = technique.check(ctx);
|
|
417
|
-
let file = typeof technique.file === 'function' ? (technique.file(ctx) ?? null) : (technique.file ?? null);
|
|
418
|
-
let line = typeof technique.line === 'function' ? (technique.line(ctx) ?? null) : (technique.line ?? null);
|
|
419
|
-
let snippet = null;
|
|
420
|
-
// CTO-04: only compute evidence on failed checks (cheap, and only where it adds trust).
|
|
421
|
-
if (passed === false) {
|
|
422
|
-
const evidence = resolveEvidence(key, ctx, { file, line });
|
|
423
|
-
if (evidence) {
|
|
424
|
-
file = evidence.file;
|
|
425
|
-
line = evidence.line;
|
|
426
|
-
snippet = evidence.snippet;
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
results.push({
|
|
430
|
-
key,
|
|
431
|
-
...technique,
|
|
432
|
-
file,
|
|
433
|
-
line: Number.isFinite(line) ? line : null,
|
|
434
|
-
snippet,
|
|
435
|
-
passed,
|
|
436
|
-
});
|
|
437
494
|
}
|
|
438
495
|
|
|
439
|
-
if (largeInstructionFiles.length > 0) {
|
|
496
|
+
if (!shallowRiskOnly && largeInstructionFiles.length > 0) {
|
|
440
497
|
results.push({
|
|
441
498
|
key: 'largeInstructionFile',
|
|
442
499
|
id: null,
|
|
@@ -505,8 +562,10 @@ async function audit(options) {
|
|
|
505
562
|
const scaffoldedPassed = passed.filter(r => scaffoldedKeys.has(r.key));
|
|
506
563
|
const organicEarned = organicPassed.reduce((sum, r) => sum + (WEIGHTS[r.impact] || 5), 0);
|
|
507
564
|
const organicScore = maxScore > 0 ? Math.round((organicEarned / maxScore) * 100) : 0;
|
|
508
|
-
const quickWins = getQuickWins(failed, { platform: spec.platform });
|
|
509
|
-
const topNextActions =
|
|
565
|
+
const quickWins = shallowRiskOnly ? [] : getQuickWins(failed, { platform: spec.platform });
|
|
566
|
+
const topNextActions = shallowRiskOnly
|
|
567
|
+
? []
|
|
568
|
+
: buildTopNextActions(failed, 5, outcomeSummary.byKey, { platform: spec.platform, fpFeedbackByKey: fpFeedback.byKey });
|
|
510
569
|
|
|
511
570
|
// CTO-04: enrich top actions with file/line/snippet from the corresponding
|
|
512
571
|
// result record (evidence was resolved above during the check loop).
|
|
@@ -536,10 +595,10 @@ async function audit(options) {
|
|
|
536
595
|
action.projectedOrganicScoreDelta = 0;
|
|
537
596
|
}
|
|
538
597
|
}
|
|
539
|
-
const categoryScores = computeCategoryScores(applicable, passed);
|
|
540
|
-
const platformScopeNote = getPlatformScopeNote(spec, ctx);
|
|
541
|
-
const platformCaveats = getPlatformCaveats(spec, ctx);
|
|
542
|
-
const deprecationWarnings = detectDeprecationWarnings(failed, packageVersion);
|
|
598
|
+
const categoryScores = shallowRiskOnly ? {} : computeCategoryScores(applicable, passed);
|
|
599
|
+
const platformScopeNote = shallowRiskOnly ? null : getPlatformScopeNote(spec, ctx);
|
|
600
|
+
const platformCaveats = shallowRiskOnly ? [] : getPlatformCaveats(spec, ctx);
|
|
601
|
+
const deprecationWarnings = shallowRiskOnly ? [] : detectDeprecationWarnings(failed, packageVersion);
|
|
543
602
|
const warnings = [
|
|
544
603
|
...largeInstructionFiles.map((item) => ({
|
|
545
604
|
kind: 'large-instruction-file',
|
|
@@ -557,7 +616,7 @@ async function audit(options) {
|
|
|
557
616
|
...item,
|
|
558
617
|
})),
|
|
559
618
|
];
|
|
560
|
-
const recommendedDomainPacks = spec.platform === 'codex'
|
|
619
|
+
const recommendedDomainPacks = !shallowRiskOnly && spec.platform === 'codex'
|
|
561
620
|
? detectCodexDomainPacks(ctx, stacks, getCodexDomainPackSignals(ctx))
|
|
562
621
|
: [];
|
|
563
622
|
|
|
@@ -568,7 +627,7 @@ async function audit(options) {
|
|
|
568
627
|
stackKeys.has('nextjs') || stackKeys.has('angular') || stackKeys.has('svelte') ||
|
|
569
628
|
stackKeys.has('nestjs') || stackKeys.has('remix') || stackKeys.has('astro') ||
|
|
570
629
|
stackKeys.has('typescript') || stackKeys.has('deno') || stackKeys.has('bun');
|
|
571
|
-
if (!hasNodeStack) {
|
|
630
|
+
if (!shallowRiskOnly && !hasNodeStack) {
|
|
572
631
|
let preferredTest = null;
|
|
573
632
|
let preferredInstall = null;
|
|
574
633
|
if (stackKeys.has('python') || stackKeys.has('django') || stackKeys.has('fastapi')) {
|
|
@@ -620,7 +679,7 @@ async function audit(options) {
|
|
|
620
679
|
sunsetDate: r.sunsetDate || null,
|
|
621
680
|
})),
|
|
622
681
|
categoryScores,
|
|
623
|
-
scoreCoaching: buildScoreCoaching({
|
|
682
|
+
scoreCoaching: shallowRiskOnly ? null : buildScoreCoaching({
|
|
624
683
|
score,
|
|
625
684
|
earnedPoints: earnedScore,
|
|
626
685
|
maxPoints: maxScore,
|
|
@@ -645,6 +704,9 @@ async function audit(options) {
|
|
|
645
704
|
// CTO-08: per-layer coverage summary (governance/drift/hygiene/shallow-risk).
|
|
646
705
|
layerSummary: summarizeLayers(activeResults),
|
|
647
706
|
};
|
|
707
|
+
if (shallowRiskEnabled) {
|
|
708
|
+
result.shallowRiskHints = runShallowRisk(ctx);
|
|
709
|
+
}
|
|
648
710
|
// Detect which AI config files are present
|
|
649
711
|
const configFiles = [];
|
|
650
712
|
const configChecks = [
|
|
@@ -661,7 +723,9 @@ async function audit(options) {
|
|
|
661
723
|
}
|
|
662
724
|
result.detectedConfigFiles = configFiles;
|
|
663
725
|
|
|
664
|
-
result.suggestedNextCommand =
|
|
726
|
+
result.suggestedNextCommand = shallowRiskOnly
|
|
727
|
+
? 'nerviq audit --shallow-risk --full'
|
|
728
|
+
: inferSuggestedNextCommand(result);
|
|
665
729
|
result.liteSummary = {
|
|
666
730
|
topNextActions: topNextActions.slice(0, 3),
|
|
667
731
|
nextCommand: result.suggestedNextCommand,
|
|
@@ -710,6 +774,11 @@ async function audit(options) {
|
|
|
710
774
|
return result;
|
|
711
775
|
}
|
|
712
776
|
|
|
777
|
+
if (shallowRiskOnly) {
|
|
778
|
+
printShallowRiskOnly(result, options.dir);
|
|
779
|
+
return result;
|
|
780
|
+
}
|
|
781
|
+
|
|
713
782
|
if (options.lite) {
|
|
714
783
|
printLiteAudit(result, options.dir);
|
|
715
784
|
sendInsights(result);
|
|
@@ -798,9 +867,8 @@ async function audit(options) {
|
|
|
798
867
|
const layerOrder = [LAYERS.GOVERNANCE, LAYERS.DRIFT, LAYERS.HYGIENE, LAYERS.SHALLOW_RISK];
|
|
799
868
|
for (const layer of layerOrder) {
|
|
800
869
|
const b = layerSummary[layer] || { total: 0, passed: 0, failed: 0, skipped: 0 };
|
|
801
|
-
const
|
|
802
|
-
|
|
803
|
-
console.log(colorize(` ${layer}: ${b.total} checks (${b.passed} passed, ${b.failed} failed)${reservedNote}`, 'dim'));
|
|
870
|
+
const layerNote = layer === LAYERS.SHALLOW_RISK ? ' (parallel, opt-in, not scored)' : '';
|
|
871
|
+
console.log(colorize(` ${layer}: ${b.total} checks (${b.passed} passed, ${b.failed} failed)${layerNote}`, 'dim'));
|
|
804
872
|
}
|
|
805
873
|
console.log('');
|
|
806
874
|
|
|
@@ -895,6 +963,8 @@ async function audit(options) {
|
|
|
895
963
|
console.log('');
|
|
896
964
|
}
|
|
897
965
|
|
|
966
|
+
printShallowRiskSection(result);
|
|
967
|
+
|
|
898
968
|
const terminology = formatTerminologyLines(collectAuditTerminology(result));
|
|
899
969
|
if (terminology.length > 0) {
|
|
900
970
|
terminology.forEach((line) => {
|
package/src/formatters/csv.js
CHANGED
|
@@ -1,85 +1,86 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CSV Formatter (RFC 4180)
|
|
3
|
-
*
|
|
4
|
-
* One row per check in a nerviq audit result.
|
|
5
|
-
* Columns: key,id,name,category,layer,rating,severity,passed,file,line,sourceUrl,fix
|
|
6
|
-
*
|
|
7
|
-
* The `layer` column (added in CTO-08) is one of 'governance',
|
|
8
|
-
* 'drift', 'hygiene', or 'shallow-risk' — see docs/integration-contracts.md §8.
|
|
9
|
-
*
|
|
10
|
-
* Quoting rules (RFC 4180):
|
|
11
|
-
* - Fields containing comma, double-quote, CR, or LF are wrapped in
|
|
12
|
-
* double-quotes.
|
|
13
|
-
* - Internal double-quotes are escaped by doubling them.
|
|
14
|
-
* - Header row is emitted first.
|
|
15
|
-
* - No UTF-8 BOM (some consumers mishandle it).
|
|
16
|
-
* - Line separator: LF (consumers accept LF; JUnit/XLSX/csv parsers
|
|
17
|
-
* normalize both).
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
'use strict';
|
|
21
|
-
|
|
22
|
-
const COLUMNS = [
|
|
23
|
-
'key',
|
|
24
|
-
'id',
|
|
25
|
-
'name',
|
|
26
|
-
'category',
|
|
27
|
-
'layer',
|
|
28
|
-
'rating',
|
|
29
|
-
'severity',
|
|
30
|
-
'passed',
|
|
31
|
-
'file',
|
|
32
|
-
'line',
|
|
33
|
-
'sourceUrl',
|
|
34
|
-
'fix',
|
|
35
|
-
'projectedScoreDelta',
|
|
36
|
-
'projectedScoreAfter',
|
|
37
|
-
];
|
|
38
|
-
|
|
39
|
-
function csvEscape(value) {
|
|
40
|
-
if (value === null || value === undefined) return '';
|
|
41
|
-
const s = String(value);
|
|
42
|
-
if (/[",\r\n]/.test(s)) {
|
|
43
|
-
return `"${s.replace(/"/g, '""')}"`;
|
|
44
|
-
}
|
|
45
|
-
return s;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function rowFor(r, projections = null) {
|
|
49
|
-
const severity = r.severity || r.impact || '';
|
|
50
|
-
const proj = projections && projections.get(r.key);
|
|
51
|
-
const cells = [
|
|
52
|
-
r.key ?? '',
|
|
53
|
-
r.id ?? '',
|
|
54
|
-
r.name ?? '',
|
|
55
|
-
r.category ?? '',
|
|
56
|
-
r.layer ?? '',
|
|
57
|
-
r.rating ?? '',
|
|
58
|
-
severity,
|
|
59
|
-
r.passed === null || r.passed === undefined ? '' : String(r.passed),
|
|
60
|
-
r.file ?? '',
|
|
61
|
-
r.line ?? '',
|
|
62
|
-
r.sourceUrl ?? '',
|
|
63
|
-
r.fix ?? '',
|
|
64
|
-
proj && Number.isFinite(proj.projectedScoreDelta) ? String(proj.projectedScoreDelta) : '',
|
|
65
|
-
proj && Number.isFinite(proj.projectedScoreAfter) ? String(proj.projectedScoreAfter) : '',
|
|
66
|
-
];
|
|
67
|
-
return cells.map(csvEscape).join(',');
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function formatCsv(auditResult) {
|
|
71
|
-
const results = Array.isArray(auditResult.results) ? auditResult.results : [];
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
1
|
+
/**
|
|
2
|
+
* CSV Formatter (RFC 4180)
|
|
3
|
+
*
|
|
4
|
+
* One row per check in a nerviq audit result.
|
|
5
|
+
* Columns: key,id,name,category,layer,rating,severity,passed,file,line,sourceUrl,fix
|
|
6
|
+
*
|
|
7
|
+
* The `layer` column (added in CTO-08) is one of 'governance',
|
|
8
|
+
* 'drift', 'hygiene', or 'shallow-risk' — see docs/integration-contracts.md §8.
|
|
9
|
+
*
|
|
10
|
+
* Quoting rules (RFC 4180):
|
|
11
|
+
* - Fields containing comma, double-quote, CR, or LF are wrapped in
|
|
12
|
+
* double-quotes.
|
|
13
|
+
* - Internal double-quotes are escaped by doubling them.
|
|
14
|
+
* - Header row is emitted first.
|
|
15
|
+
* - No UTF-8 BOM (some consumers mishandle it).
|
|
16
|
+
* - Line separator: LF (consumers accept LF; JUnit/XLSX/csv parsers
|
|
17
|
+
* normalize both).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
const COLUMNS = [
|
|
23
|
+
'key',
|
|
24
|
+
'id',
|
|
25
|
+
'name',
|
|
26
|
+
'category',
|
|
27
|
+
'layer',
|
|
28
|
+
'rating',
|
|
29
|
+
'severity',
|
|
30
|
+
'passed',
|
|
31
|
+
'file',
|
|
32
|
+
'line',
|
|
33
|
+
'sourceUrl',
|
|
34
|
+
'fix',
|
|
35
|
+
'projectedScoreDelta',
|
|
36
|
+
'projectedScoreAfter',
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
function csvEscape(value) {
|
|
40
|
+
if (value === null || value === undefined) return '';
|
|
41
|
+
const s = String(value);
|
|
42
|
+
if (/[",\r\n]/.test(s)) {
|
|
43
|
+
return `"${s.replace(/"/g, '""')}"`;
|
|
44
|
+
}
|
|
45
|
+
return s;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function rowFor(r, projections = null) {
|
|
49
|
+
const severity = r.severity || r.impact || '';
|
|
50
|
+
const proj = r.layer === 'shallow-risk' ? null : (projections && projections.get(r.key));
|
|
51
|
+
const cells = [
|
|
52
|
+
r.key ?? '',
|
|
53
|
+
r.id ?? '',
|
|
54
|
+
r.name ?? '',
|
|
55
|
+
r.category ?? '',
|
|
56
|
+
r.layer ?? '',
|
|
57
|
+
r.rating ?? '',
|
|
58
|
+
severity,
|
|
59
|
+
r.passed === null || r.passed === undefined ? '' : String(r.passed),
|
|
60
|
+
r.file ?? '',
|
|
61
|
+
r.line ?? '',
|
|
62
|
+
r.sourceUrl ?? '',
|
|
63
|
+
r.fix ?? '',
|
|
64
|
+
proj && Number.isFinite(proj.projectedScoreDelta) ? String(proj.projectedScoreDelta) : '',
|
|
65
|
+
proj && Number.isFinite(proj.projectedScoreAfter) ? String(proj.projectedScoreAfter) : '',
|
|
66
|
+
];
|
|
67
|
+
return cells.map(csvEscape).join(',');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function formatCsv(auditResult) {
|
|
71
|
+
const results = Array.isArray(auditResult.results) ? auditResult.results : [];
|
|
72
|
+
const shallowRiskHints = Array.isArray(auditResult.shallowRiskHints) ? auditResult.shallowRiskHints : [];
|
|
73
|
+
const projections = new Map();
|
|
74
|
+
if (Array.isArray(auditResult.topNextActions)) {
|
|
75
|
+
for (const item of auditResult.topNextActions) {
|
|
76
|
+
if (item && item.key) projections.set(item.key, item);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const lines = [COLUMNS.join(',')];
|
|
80
|
+
for (const r of [...results, ...shallowRiskHints]) {
|
|
81
|
+
lines.push(rowFor(r, projections));
|
|
82
|
+
}
|
|
83
|
+
return lines.join('\n');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = { formatCsv, CSV_COLUMNS: COLUMNS };
|